diff --git a/.circleci/config.yml b/.circleci/config.yml index ef19b8ee7..e6ce2e242 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ jobs: build: working_directory: ~/helm.sh/helm docker: - - image: circleci/golang:1.13 + - image: circleci/golang:1.14 environment: GOCACHE: "/tmp/go/cache" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..595b50218 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ + + +**What this PR does / why we need it**: + +**Special notes for your reviewer**: + +**If applicable**: +- [ ] this PR contains documentation +- [ ] this PR contains unit tests +- [ ] this PR has been tested for backwards compatibility diff --git a/ADOPTERS.md b/ADOPTERS.md index 46b42b8a0..9d5365b72 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -10,6 +10,7 @@ - [Microsoft](https://microsoft.com) - [Qovery](https://www.qovery.com/) - [Samsung SDS](https://www.samsungsds.com/) +- [Softonic](https://hello.softonic.com/) - [Ville de Montreal](https://montreal.ca) _This file is part of the CNCF official documentation for projects._ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63780365e..a637f9255 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,9 +184,9 @@ contributing to Helm. All issue types follow the same general lifecycle. Differe ## How to Contribute a Patch -1. If you haven't already done so, sign a Contributor License Agreement (see details above). -2. Fork the desired repo, develop and test your code changes. -3. Submit a pull request. +1. Identify or create the related issue. +2. Fork the desired repo; develop and test your code changes. +3. Submit a pull request, making sure to sign your work and link the related issue. Coding conventions and standards are explained in the [official developer docs](https://helm.sh/docs/developers/). diff --git a/Makefile b/Makefile index 27404bca8..446fbc0ed 100644 --- a/Makefile +++ b/Makefile @@ -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/README.md b/README.md index 745a60c2b..bb6908fdb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ If you want to use a package manager: - [Chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`. - [Scoop](https://scoop.sh/) users can use `scoop install helm`. - [GoFish](https://gofi.sh/) users can use `gofish install helm`. +- [Snapcraft](https://snapcraft.io/) users can use `snap install helm --classic` To rapidly get Helm up and running, start with the [Quick Start Guide](https://docs.helm.sh/using_helm/#quickstart-guide). diff --git a/cmd/helm/completion.go b/cmd/helm/completion.go index 1601cb448..c1f7790bc 100644 --- a/cmd/helm/completion.go +++ b/cmd/helm/completion.go @@ -52,7 +52,7 @@ func newCompletionCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "completion SHELL", - Short: "Generate autocompletions script for the specified shell (bash or zsh)", + Short: "generate autocompletions script for the specified shell (bash or zsh)", Long: completionDesc, RunE: func(cmd *cobra.Command, args []string) error { return runCompletion(out, cmd, args) diff --git a/cmd/helm/create.go b/cmd/helm/create.go index e8ff757cf..5bdda05cb 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -71,7 +71,7 @@ func newCreateCmd(out io.Writer) *cobra.Command { }, } - cmd.Flags().StringVarP(&o.starter, "starter", "p", "", "The name or absolute path to Helm starter scaffold") + cmd.Flags().StringVarP(&o.starter, "starter", "p", "", "the name or absolute path to Helm starter scaffold") return cmd } diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 0a9b7b9a5..bbb8d394a 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -106,7 +106,7 @@ func TestCreateStarterCmd(t *testing.T) { t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } - expectedNumberOfTemplates := 8 + expectedNumberOfTemplates := 9 if l := len(c.Templates); l != expectedNumberOfTemplates { t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) } @@ -174,7 +174,7 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) { t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } - expectedNumberOfTemplates := 8 + expectedNumberOfTemplates := 9 if l := len(c.Templates); l != expectedNumberOfTemplates { t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) } diff --git a/cmd/helm/docs.go b/cmd/helm/docs.go index 2c9020fb9..c974d4014 100644 --- a/cmd/helm/docs.go +++ b/cmd/helm/docs.go @@ -48,7 +48,7 @@ func newDocsCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "docs", - Short: "Generate documentation as markdown or man pages", + Short: "generate documentation as markdown or man pages", Long: docsDesc, Hidden: true, Args: require.NoArgs, diff --git a/cmd/helm/env.go b/cmd/helm/env.go index 2687272ba..0fbfb9da4 100644 --- a/cmd/helm/env.go +++ b/cmd/helm/env.go @@ -21,53 +21,35 @@ import ( "io" "sort" - "helm.sh/helm/v3/pkg/cli" - "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" ) -var ( - envHelp = ` +var envHelp = ` Env prints out all the environment information in use by Helm. ` -) func newEnvCmd(out io.Writer) *cobra.Command { - o := &envOptions{} - o.settings = cli.New() - cmd := &cobra.Command{ Use: "env", - Short: "Helm client environment information", + Short: "helm client environment information", Long: envHelp, Args: require.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return o.run(out) + Run: func(cmd *cobra.Command, args []string) { + envVars := settings.EnvVars() + + // Sort the variables by alphabetical order. + // This allows for a constant output across calls to 'helm env'. + var keys []string + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + fmt.Fprintf(out, "%s=\"%s\"\n", k, envVars[k]) + } }, } - return cmd } - -type envOptions struct { - settings *cli.EnvSettings -} - -func (o *envOptions) run(out io.Writer) error { - envVars := o.settings.EnvVars() - - // Sort the variables by alphabetical order. - // This allows for a constant output across calls to 'helm env'. - var keys []string - for k := range envVars { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - fmt.Printf("%s=\"%s\"\n", k, envVars[k]) - } - return nil -} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 257387547..fcc7315f5 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -32,7 +32,6 @@ import ( // 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" @@ -44,9 +43,7 @@ import ( // FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI") -var ( - settings = cli.New() -) +var settings = cli.New() func init() { log.SetFlags(log.Lshortfile) @@ -73,23 +70,16 @@ func main() { actionConfig := new(action.Configuration) cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) - if calledCmd, _, err := cmd.Find(os.Args[1:]); err == nil && calledCmd.Name() == completion.CompRequestCmd { - // If completion is being called, we have to check if the completion is for the "--kube-context" - // value; if it is, we cannot call the action.Init() method with an incomplete kube-context value - // or else it will fail immediately. So, we simply unset the invalid kube-context value. - if args := os.Args[1:]; len(args) > 2 && args[len(args)-2] == "--kube-context" { - // We are completing the kube-context value! Reset it as the current value is not valid. - settings.KubeContext = "" + // run when each command's execute method is called + cobra.OnInitialize(func() { + helmDriver := os.Getenv("HELM_DRIVER") + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { + log.Fatal(err) } - } - - 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 helmDriver == "memory" { + loadReleasesInMemory(actionConfig) + } + }) if err := cmd.Execute(); err != nil { debug("%+v", err) diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 94646a5a3..9ba9d78fb 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -97,10 +97,15 @@ func storageFixture() *storage.Storage { } func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { + return executeActionCommandStdinC(store, nil, cmd) +} + +func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err } + buf := new(bytes.Buffer) actionConfig := &action.Configuration{ @@ -111,15 +116,26 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, } root := newRootCmd(actionConfig, buf, args) - root.SetOutput(buf) + root.SetOut(buf) + root.SetErr(buf) root.SetArgs(args) + oldStdin := os.Stdin + if in != nil { + root.SetIn(in) + os.Stdin = in + } + if mem, ok := store.Driver.(*driver.Memory); ok { mem.SetNamespace(settings.Namespace()) } c, err := root.ExecuteC() - return c, buf.String(), err + result := buf.String() + + os.Stdin = oldStdin + + return c, result, err } // cmdTestCase describes a test case that works with releases. diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 719dc9014..21a41b9f9 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -148,7 +148,7 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema") - f.BoolVar(&client.Atomic, "atomic", false, "if set, installation process purges chart on fail. The --wait flag will be set automatically if --atomic is used") + f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") addValueOptionsFlags(f, valueOpts) @@ -210,10 +210,15 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options Getters: p, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, } if err := man.Update(); err != nil { return nil, err } + // Reload the chart with the updated Chart.lock file. + if chartRequested, err = loader.Load(cp); err != nil { + return nil, errors.Wrap(err, "failed reloading chart after repo update") + } } else { return nil, err } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 57972024f..7a101940f 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -111,6 +111,12 @@ func TestInstall(t *testing.T) { cmd: "install nodeps testdata/testcharts/chart-missing-deps", wantError: true, }, + // Install chart with update-dependency + { + name: "install chart with missing dependencies", + cmd: "install --dependency-update updeps testdata/testcharts/chart-with-subchart-update", + golden: "output/chart-with-subchart-update.txt", + }, // Install, chart with bad dependencies in Chart.yaml in /charts { name: "install chart with bad dependencies in Chart.yaml", @@ -189,6 +195,11 @@ func TestInstall(t *testing.T) { cmd: "install aeneas testdata/testcharts/deprecated --namespace default", golden: "output/deprecated-chart.txt", }, + // Install chart with only crds + { + name: "install chart with only crds", + cmd: "install crd-test testdata/testcharts/chart-with-only-crds --namespace default", + }, } runTestActionCmd(t, tests) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index fe39a5741..a7aac172a 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -46,7 +46,7 @@ func newLintCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "lint PATH", - Short: "examines a chart for possible issues", + Short: "examine a chart for possible issues", Long: longLintHelp, RunE: func(cmd *cobra.Command, args []string) error { paths := []string{"."} diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 4b652088d..08d6beb79 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -83,10 +83,29 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if client.Short { + + names := make([]string, 0) for _, res := range results { - fmt.Fprintln(out, res.Name) + names = append(names, res.Name) + } + + outputFlag := cmd.Flag("output") + + switch outputFlag.Value.String() { + case "json": + output.EncodeJSON(out, names) + return nil + case "yaml": + output.EncodeYAML(out, names) + return nil + case "table": + for _, res := range results { + fmt.Fprintln(out, res.Name) + } + return nil + default: + return outfmt.Write(out, newReleaseListWriter(results)) } - return nil } return outfmt.Write(out, newReleaseListWriter(results)) diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index fe773a803..dadb57b94 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -198,6 +198,16 @@ func TestListCmd(t *testing.T) { cmd: "list --short", golden: "output/list-short.txt", rels: releaseFixture, + }, { + name: "list releases in short output format", + cmd: "list --short --output yaml", + golden: "output/list-short-yaml.txt", + rels: releaseFixture, + }, { + name: "list releases in short output format", + cmd: "list --short --output json", + golden: "output/list-short-json.txt", + rels: releaseFixture, }, { name: "list superseded releases", cmd: "list --superseded", diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index e56feab40..a23a067fb 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -58,7 +58,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { return } - found, err := findPlugins(settings.PluginsDirectory) + found, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) return @@ -238,20 +238,6 @@ func manuallyProcessArgs(args []string) ([]string, []string) { return known, unknown } -// findPlugins returns a list of YAML files that describe plugins. -func findPlugins(plugdirs string) ([]*plugin.Plugin, error) { - found := []*plugin.Plugin{} - // Let's get all UNIXy and allow path separators - for _, p := range filepath.SplitList(plugdirs) { - matches, err := plugin.LoadAll(p) - if err != nil { - return matches, err - } - found = append(found, matches...) - } - return found, nil -} - // pluginCommand represents the optional completion.yaml file of a plugin type pluginCommand struct { Name string `json:"name"` diff --git a/cmd/helm/plugin_list.go b/cmd/helm/plugin_list.go index 0440b0b5e..49a454963 100644 --- a/cmd/helm/plugin_list.go +++ b/cmd/helm/plugin_list.go @@ -22,6 +22,8 @@ import ( "github.com/gosuri/uitable" "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/plugin" ) func newPluginListCmd(out io.Writer) *cobra.Command { @@ -31,7 +33,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command { Short: "list installed Helm plugins", RunE: func(cmd *cobra.Command, args []string) error { debug("pluginDirs: %s", settings.PluginsDirectory) - plugins, err := findPlugins(settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { return err } @@ -51,7 +53,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command { // Provide dynamic auto-completion for plugin names func compListPlugins(toComplete string) []string { var pNames []string - plugins, err := findPlugins(settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err == nil { for _, p := range plugins { if strings.HasPrefix(p.Metadata.Name, toComplete) { diff --git a/cmd/helm/plugin_uninstall.go b/cmd/helm/plugin_uninstall.go index f703ddcfb..66cdaccdc 100644 --- a/cmd/helm/plugin_uninstall.go +++ b/cmd/helm/plugin_uninstall.go @@ -68,7 +68,7 @@ func (o *pluginUninstallOptions) complete(args []string) error { func (o *pluginUninstallOptions) run(out io.Writer) error { debug("loading installed plugins from %s", settings.PluginsDirectory) - plugins, err := findPlugins(settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { return err } diff --git a/cmd/helm/plugin_update.go b/cmd/helm/plugin_update.go index a24e80518..23f840b4a 100644 --- a/cmd/helm/plugin_update.go +++ b/cmd/helm/plugin_update.go @@ -70,7 +70,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { func (o *pluginUpdateOptions) run(out io.Writer) error { installer.Debug = settings.Debug debug("loading installed plugins from %s", settings.PluginsDirectory) - plugins, err := findPlugins(settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { return err } diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index e4690b9d4..036d96794 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -82,7 +82,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command f := cmd.Flags() f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&outputLogs, "logs", false, "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") + f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") return cmd } diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 25316bafc..ed1c9573c 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -38,7 +38,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command { Args: require.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { f, err := repo.LoadFile(settings.RepositoryConfig) - if isNotExist(err) || len(f.Repositories) == 0 { + if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) { return errors.New("no repositories to show") } @@ -97,13 +97,38 @@ func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) err return nil } +// Returns all repos from repos, except those with names matching ignoredRepoNames +// Inspired by https://stackoverflow.com/a/28701031/893211 +func filterRepos(repos []*repo.Entry, ignoredRepoNames []string) []*repo.Entry { + // if ignoredRepoNames is nil, just return repo + if ignoredRepoNames == nil { + return repos + } + + filteredRepos := make([]*repo.Entry, 0) + + ignored := make(map[string]bool, len(ignoredRepoNames)) + for _, repoName := range ignoredRepoNames { + ignored[repoName] = true + } + + for _, repo := range repos { + if _, removed := ignored[repo.Name]; !removed { + filteredRepos = append(filteredRepos, repo) + } + } + + return filteredRepos +} + // Provide dynamic auto-completion for repo names -func compListRepos(prefix string) []string { +func compListRepos(prefix string, ignoredRepoNames []string) []string { var rNames []string f, err := repo.LoadFile(settings.RepositoryConfig) if err == nil && len(f.Repositories) > 0 { - for _, repo := range f.Repositories { + filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) + for _, repo := range filteredRepos { if strings.HasPrefix(repo.Name, prefix) { rNames = append(rNames, repo.Name) } diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index e8c0ec027..5dad4e5e0 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -32,7 +32,7 @@ import ( ) type repoRemoveOptions struct { - name string + names []string repoFile string repoCache string } @@ -41,24 +41,21 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command { o := &repoRemoveOptions{} cmd := &cobra.Command{ - Use: "remove [NAME]", + Use: "remove [REPO1 [REPO2 ...]]", Aliases: []string{"rm"}, - Short: "remove a chart repository", - Args: require.ExactArgs(1), + Short: "remove one or more chart repositories", + Args: require.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { o.repoFile = settings.RepositoryConfig o.repoCache = settings.RepositoryCache - o.name = args[0] + o.names = args return o.run(out) }, } // Function providing dynamic auto-completion completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) { - if len(args) != 0 { - return nil, completion.BashCompDirectiveNoFileComp - } - return compListRepos(toComplete), completion.BashCompDirectiveNoFileComp + return compListRepos(toComplete, args), completion.BashCompDirectiveNoFileComp }) return cmd @@ -70,18 +67,20 @@ func (o *repoRemoveOptions) run(out io.Writer) error { return errors.New("no repositories configured") } - if !r.Remove(o.name) { - return errors.Errorf("no repo named %q found", o.name) - } - if err := r.WriteFile(o.repoFile, 0644); err != nil { - return err - } + for _, name := range o.names { + if !r.Remove(name) { + return errors.Errorf("no repo named %q found", name) + } + if err := r.WriteFile(o.repoFile, 0644); err != nil { + return err + } - if err := removeRepoCache(o.repoCache, o.name); err != nil { - return err + if err := removeRepoCache(o.repoCache, name); err != nil { + return err + } + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) } - fmt.Fprintf(out, "%q has been removed from your repositories\n", o.name) return nil } diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go index 85c76bb92..f7d50140e 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -44,7 +44,7 @@ func TestRepoRemove(t *testing.T) { b := bytes.NewBuffer(nil) rmOpts := repoRemoveOptions{ - name: testRepoName, + names: []string{testRepoName}, repoFile: repoFile, repoCache: rootDir, } @@ -62,14 +62,9 @@ func TestRepoRemove(t *testing.T) { t.Error(err) } - idx := filepath.Join(rootDir, helmpath.CacheIndexFile(testRepoName)) - mf, _ := os.Create(idx) - mf.Close() - - idx2 := filepath.Join(rootDir, helmpath.CacheChartsFile(testRepoName)) - mf, _ = os.Create(idx2) - mf.Close() + cacheIndexFile, cacheChartsFile := createCacheFiles(rootDir, testRepoName) + // Reset the buffer before running repo remove b.Reset() if err := rmOpts.run(b); err != nil { @@ -79,13 +74,7 @@ func TestRepoRemove(t *testing.T) { t.Errorf("Unexpected output: %s", b.String()) } - if _, err := os.Stat(idx); err == nil { - t.Errorf("Error cache index file was not removed for repository %s", testRepoName) - } - - if _, err := os.Stat(idx2); err == nil { - t.Errorf("Error cache chart file was not removed for repository %s", testRepoName) - } + testCacheFiles(t, cacheIndexFile, cacheChartsFile, testRepoName) f, err := repo.LoadFile(repoFile) if err != nil { @@ -95,4 +84,79 @@ func TestRepoRemove(t *testing.T) { if f.Has(testRepoName) { t.Errorf("%s was not successfully removed from repositories list", testRepoName) } + + // Test removal of multiple repos in one go + var testRepoNames = []string{"foo", "bar", "baz"} + cacheFiles := make(map[string][]string, len(testRepoNames)) + + // Add test repos + for _, repoName := range testRepoNames { + o := &repoAddOptions{ + name: repoName, + url: ts.URL(), + repoFile: repoFile, + } + + if err := o.run(os.Stderr); err != nil { + t.Error(err) + } + + cacheIndex, cacheChart := createCacheFiles(rootDir, repoName) + cacheFiles[repoName] = []string{cacheIndex, cacheChart} + + } + + // Create repo remove command + multiRmOpts := repoRemoveOptions{ + names: testRepoNames, + repoFile: repoFile, + repoCache: rootDir, + } + + // Reset the buffer before running repo remove + b.Reset() + + // Run repo remove command + if err := multiRmOpts.run(b); err != nil { + t.Errorf("Error removing list of repos from repositories: %q", testRepoNames) + } + + // Check that stuff were removed + if !strings.Contains(b.String(), "has been removed") { + t.Errorf("Unexpected output: %s", b.String()) + } + + for _, repoName := range testRepoNames { + f, err := repo.LoadFile(repoFile) + if err != nil { + t.Error(err) + } + if f.Has(repoName) { + t.Errorf("%s was not successfully removed from repositories list", repoName) + } + cacheIndex := cacheFiles[repoName][0] + cacheChart := cacheFiles[repoName][1] + testCacheFiles(t, cacheIndex, cacheChart, repoName) + } +} + +func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, cacheChartsFile string) { + cacheIndexFile = filepath.Join(rootDir, helmpath.CacheIndexFile(repoName)) + mf, _ := os.Create(cacheIndexFile) + mf.Close() + + cacheChartsFile = filepath.Join(rootDir, helmpath.CacheChartsFile(repoName)) + mf, _ = os.Create(cacheChartsFile) + mf.Close() + + return cacheIndexFile, cacheChartsFile +} + +func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) { + if _, err := os.Stat(cacheIndexFile); err == nil { + t.Errorf("Error cache index file was not removed for repository %s", repoName) + } + if _, err := os.Stat(cacheChartsFile); err == nil { + t.Errorf("Error cache chart file was not removed for repository %s", repoName) + } } diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 3ebea3bae..ff4a01ad7 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -17,6 +17,7 @@ limitations under the License. package main // import "helm.sh/helm/v3/cmd/helm" import ( + "context" "fmt" "io" "strings" @@ -25,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" + "k8s.io/kubectl/pkg/util/templates" "helm.sh/helm/v3/internal/completion" "helm.sh/helm/v3/internal/experimental/registry" @@ -42,32 +44,29 @@ Common actions for Helm: Environment variables: -+------------------+-----------------------------------------------------------------------------+ -| Name | Description | -+------------------+-----------------------------------------------------------------------------+ -| $XDG_CACHE_HOME | set an alternative location for storing cached files. | -| $XDG_CONFIG_HOME | set an alternative location for storing Helm configuration. | -| $XDG_DATA_HOME | set an alternative location for storing Helm data. | -| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory | -| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | -| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | -+------------------+-----------------------------------------------------------------------------+ +| Name | Description | +|------------------------------------|-----------------------------------------------------------------------------------| +| $HELM_CACHE_HOME | set an alternative location for storing cached files. | +| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | +| $HELM_DATA_HOME | set an alternative location for storing Helm data. | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres | +| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | +| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | -Helm stores configuration based on the XDG base directory specification, so +Helm stores cache, configuration, and data based on the following configuration order: -- cached files are stored in $XDG_CACHE_HOME/helm -- configuration is stored in $XDG_CONFIG_HOME/helm -- data is stored in $XDG_DATA_HOME/helm +- If a HELM_*_HOME environment variable is set, it will be used +- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used +- When no other location is set a default location will be used based on the operating system By default, the default directories depend on the Operating System. The defaults are listed below: -+------------------+---------------------------+--------------------------------+-------------------------+ | Operating System | Cache Path | Configuration Path | Data Path | -+------------------+---------------------------+--------------------------------+-------------------------+ +|------------------|---------------------------|--------------------------------|-------------------------| | Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | | macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | | Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | -+------------------+---------------------------+--------------------------------+-------------------------+ ` func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command { @@ -92,7 +91,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string completion.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to)) nsNames := []string{} - if namespaces, err := client.CoreV1().Namespaces().List(metav1.ListOptions{TimeoutSeconds: &to}); err == nil { + if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { for _, ns := range namespaces.Items { if strings.HasPrefix(ns.Name, toComplete) { nsNames = append(nsNames, ns.Name) @@ -134,31 +133,47 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string flags.ParseErrorsWhitelist.UnknownFlags = true flags.Parse(args) + commandGroups := templates.CommandGroups{ + { + Message: "Release Management Commands:", + Commands: []*cobra.Command{ + newInstallCmd(actionConfig, out), + newListCmd(actionConfig, out), + newGetCmd(actionConfig, out), + newStatusCmd(actionConfig, out), + newUpgradeCmd(actionConfig, out), + newHistoryCmd(actionConfig, out), + newRollbackCmd(actionConfig, out), + newReleaseTestCmd(actionConfig, out), + newUninstallCmd(actionConfig, out), + }, + }, + { + Message: "Chart Commands:", + Commands: []*cobra.Command{ + newCreateCmd(out), + newDependencyCmd(out), + newPackageCmd(out), + newTemplateCmd(actionConfig, out), + newLintCmd(out), + newVerifyCmd(out), + }, + }, + { + Message: "Chart Repository Commands:", + Commands: []*cobra.Command{ + newRepoCmd(out), + newSearchCmd(out), + newPullCmd(out), + newShowCmd(out), + }, + }, + } + commandGroups.Add(cmd) + templates.ActsAsRootCommand(cmd, []string{"options"}, commandGroups...) + // Add subcommands cmd.AddCommand( - // chart commands - newCreateCmd(out), - newDependencyCmd(out), - newPullCmd(out), - newShowCmd(out), - newLintCmd(out), - newPackageCmd(out), - newRepoCmd(out), - newSearchCmd(out), - newVerifyCmd(out), - - // release commands - newGetCmd(actionConfig, out), - newHistoryCmd(actionConfig, out), - newInstallCmd(actionConfig, out), - newListCmd(actionConfig, out), - newReleaseTestCmd(actionConfig, out), - newRollbackCmd(actionConfig, out), - newStatusCmd(actionConfig, out), - newTemplateCmd(actionConfig, out), - newUninstallCmd(actionConfig, out), - newUpgradeCmd(actionConfig, out), - newCompletionCmd(out), newEnvCmd(out), newPluginCmd(out), diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go index e1fa1fc27..891bb698e 100644 --- a/cmd/helm/root_test.go +++ b/cmd/helm/root_test.go @@ -55,6 +55,24 @@ func TestRootCmd(t *testing.T) { envvars: map[string]string{xdg.DataHomeEnvVar: "/bar"}, dataPath: "/bar/helm", }, + { + name: "with $HELM_CACHE_HOME set", + args: "env", + envvars: map[string]string{helmpath.CacheHomeEnvVar: "/foo/helm"}, + cachePath: "/foo/helm", + }, + { + name: "with $HELM_CONFIG_HOME set", + args: "env", + envvars: map[string]string{helmpath.ConfigHomeEnvVar: "/foo/helm"}, + configPath: "/foo/helm", + }, + { + name: "with $HELM_DATA_HOME set", + args: "env", + envvars: map[string]string{helmpath.DataHomeEnvVar: "/foo/helm"}, + dataPath: "/foo/helm", + }, } for _, tt := range tests { diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index 8a8ac379d..c7f0da974 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -298,7 +298,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, completion. var completions []string // First check completions for repos - repos := compListRepos("") + repos := compListRepos("", nil) for _, repo := range repos { repoWithSlash := fmt.Sprintf("%s/", repo) if strings.HasPrefix(toComplete, repoWithSlash) { diff --git a/cmd/helm/show.go b/cmd/helm/show.go index a82ad2777..b335c5f76 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -72,16 +72,12 @@ func newShowCmd(out io.Writer) *cobra.Command { all := &cobra.Command{ Use: "all [CHART]", - Short: "shows all information of the chart", + Short: "show all information of the chart", Long: showAllDesc, 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 } @@ -92,16 +88,12 @@ func newShowCmd(out io.Writer) *cobra.Command { valuesSubCmd := &cobra.Command{ Use: "values [CHART]", - Short: "shows the chart's values", + Short: "show the chart's values", Long: showValuesDesc, 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 } @@ -112,16 +104,12 @@ func newShowCmd(out io.Writer) *cobra.Command { chartSubCmd := &cobra.Command{ Use: "chart [CHART]", - Short: "shows the chart's definition", + Short: "show the chart's definition", Long: showChartDesc, 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 } @@ -132,16 +120,12 @@ func newShowCmd(out io.Writer) *cobra.Command { readmeSubCmd := &cobra.Command{ Use: "readme [CHART]", - Short: "shows the chart's README", + Short: "show the chart's README", Long: readmeChartDesc, 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/status.go b/cmd/helm/status.go index 6c0ca6593..2a8d29a50 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -50,7 +50,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "status RELEASE_NAME", - Short: "displays the status of the named release", + Short: "display the status of the named release", Long: statusHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/helm/template.go b/cmd/helm/template.go index bd14cde1d..a4438b50c 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -22,6 +22,7 @@ import ( "io" "path/filepath" "regexp" + "sort" "strings" "github.com/spf13/cobra" @@ -86,12 +87,21 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // 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 { + // This is necessary to ensure consistent manifest ordering when using --show-only + // with globs or directory names. splitManifests := releaseutil.SplitManifests(manifests.String()) + manifestsKeys := make([]string, 0, len(splitManifests)) + for k := range splitManifests { + manifestsKeys = append(manifestsKeys, k) + } + sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys)) + manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)") var manifestsToRender []string for _, f := range showFiles { missing := true - for _, manifest := range splitManifests { + for _, manifestKey := range manifestsKeys { + manifest := splitManifests[manifestKey] submatch := manifestNameRegex.FindStringSubmatch(manifest) if len(submatch) == 0 { continue @@ -104,10 +114,11 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // if the filepath provided matches a manifest path in the // chart, render that manifest - if f == manifestPath { - manifestsToRender = append(manifestsToRender, manifest) - missing = false + if matched, _ := filepath.Match(f, manifestPath); !matched { + continue } + manifestsToRender = append(manifestsToRender, manifest) + missing = false } if missing { return fmt.Errorf("could not find template %s in chart", f) diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index 3fd139fad..87ee79e5a 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -94,6 +94,13 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template '%s' --show-only templates/service.yaml --show-only charts/subcharta/templates/service.yaml", chartPath), golden: "output/template-show-only-multiple.txt", }, + { + name: "template with show-only glob", + cmd: fmt.Sprintf("template '%s' --show-only templates/subdir/role*", chartPath), + golden: "output/template-show-only-glob.txt", + // Repeat to ensure manifest ordering regressions are caught + repeat: 10, + }, { name: "sorted output of manifests (order of filenames, then order of objects within each YAML file)", cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/object-order"), diff --git a/cmd/helm/testdata/output/chart-with-subchart-update.txt b/cmd/helm/testdata/output/chart-with-subchart-update.txt new file mode 100644 index 000000000..a4135c782 --- /dev/null +++ b/cmd/helm/testdata/output/chart-with-subchart-update.txt @@ -0,0 +1,8 @@ +NAME: updeps +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +PARENT NOTES diff --git a/cmd/helm/testdata/output/dependency-list-archive.txt b/cmd/helm/testdata/output/dependency-list-archive.txt index a0fc13cd0..ffd4542b0 100644 --- a/cmd/helm/testdata/output/dependency-list-archive.txt +++ b/cmd/helm/testdata/output/dependency-list-archive.txt @@ -1,5 +1,5 @@ -NAME VERSION REPOSITORY STATUS -reqsubchart 0.1.0 https://example.com/charts missing -reqsubchart2 0.2.0 https://example.com/charts missing -reqsubchart3 >=0.1.0 https://example.com/charts missing +NAME VERSION REPOSITORY STATUS +reqsubchart 0.1.0 https://example.com/charts unpacked +reqsubchart2 0.2.0 https://example.com/charts unpacked +reqsubchart3 >=0.1.0 https://example.com/charts unpacked diff --git a/cmd/helm/testdata/output/list-short-json.txt b/cmd/helm/testdata/output/list-short-json.txt new file mode 100644 index 000000000..acbf1e44d --- /dev/null +++ b/cmd/helm/testdata/output/list-short-json.txt @@ -0,0 +1 @@ +["hummingbird","iguana","rocket","starlord"] diff --git a/cmd/helm/testdata/output/list-short-yaml.txt b/cmd/helm/testdata/output/list-short-yaml.txt new file mode 100644 index 000000000..86fb3d670 --- /dev/null +++ b/cmd/helm/testdata/output/list-short-yaml.txt @@ -0,0 +1,4 @@ +- hummingbird +- iguana +- rocket +- starlord diff --git a/cmd/helm/testdata/output/template-name-template.txt b/cmd/helm/testdata/output/template-name-template.txt index acba50360..0130a2a92 100644 --- a/cmd/helm/testdata/output/template-name-template.txt +++ b/cmd/helm/testdata/output/template-name-template.txt @@ -1,4 +1,33 @@ --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/template-set.txt b/cmd/helm/testdata/output/template-set.txt index b0924b5b6..ddaa8886b 100644 --- a/cmd/helm/testdata/output/template-set.txt +++ b/cmd/helm/testdata/output/template-set.txt @@ -1,4 +1,33 @@ --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/template-show-only-glob.txt b/cmd/helm/testdata/output/template-show-only-glob.txt new file mode 100644 index 000000000..0970e6cd3 --- /dev/null +++ b/cmd/helm/testdata/output/template-show-only-glob.txt @@ -0,0 +1,23 @@ +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default diff --git a/cmd/helm/testdata/output/template-values-files.txt b/cmd/helm/testdata/output/template-values-files.txt index b0924b5b6..ddaa8886b 100644 --- a/cmd/helm/testdata/output/template-values-files.txt +++ b/cmd/helm/testdata/output/template-values-files.txt @@ -1,4 +1,33 @@ --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/template-with-api-version.txt b/cmd/helm/testdata/output/template-with-api-version.txt index da3559082..7a2a4d5bf 100644 --- a/cmd/helm/testdata/output/template-with-api-version.txt +++ b/cmd/helm/testdata/output/template-with-api-version.txt @@ -1,4 +1,33 @@ --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt index 9fa1c7e6d..b04a4d5d2 100644 --- a/cmd/helm/testdata/output/template-with-crds.txt +++ b/cmd/helm/testdata/output/template-with-crds.txt @@ -15,6 +15,35 @@ spec: singular: authconfig --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/template.txt b/cmd/helm/testdata/output/template.txt index 080be618c..8301c2231 100644 --- a/cmd/helm/testdata/output/template.txt +++ b/cmd/helm/testdata/output/template.txt @@ -1,4 +1,33 @@ --- +# Source: subchart1/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart1-sa +--- +# Source: subchart1/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart1-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] +--- +# Source: subchart1/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart1-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart1-role +subjects: +- kind: ServiceAccount + name: subchart1-sa + namespace: default +--- # Source: subchart1/charts/subcharta/templates/service.yaml apiVersion: v1 kind: Service diff --git a/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt b/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt new file mode 100644 index 000000000..8f24574a6 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt @@ -0,0 +1 @@ +Error: UPGRADE FAILED: "funny-bunny" has no deployed releases diff --git a/cmd/helm/testdata/output/version-client-shorthand.txt b/cmd/helm/testdata/output/version-client-shorthand.txt index 8f9ed6136..d613309fe 100644 --- a/cmd/helm/testdata/output/version-client-shorthand.txt +++ b/cmd/helm/testdata/output/version-client-shorthand.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.2", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-client.txt b/cmd/helm/testdata/output/version-client.txt index 8f9ed6136..d613309fe 100644 --- a/cmd/helm/testdata/output/version-client.txt +++ b/cmd/helm/testdata/output/version-client.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.2", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-short.txt b/cmd/helm/testdata/output/version-short.txt index 861668947..4d5034cea 100644 --- a/cmd/helm/testdata/output/version-short.txt +++ b/cmd/helm/testdata/output/version-short.txt @@ -1 +1 @@ -v3.1 +v3.2 diff --git a/cmd/helm/testdata/output/version-template.txt b/cmd/helm/testdata/output/version-template.txt index e5a779bbf..7c09e8d57 100644 --- a/cmd/helm/testdata/output/version-template.txt +++ b/cmd/helm/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v3.1 \ No newline at end of file +Version: v3.2 \ No newline at end of file diff --git a/cmd/helm/testdata/output/version.txt b/cmd/helm/testdata/output/version.txt index 8f9ed6136..d613309fe 100644 --- a/cmd/helm/testdata/output/version.txt +++ b/cmd/helm/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.2", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore b/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml new file mode 100644 index 000000000..a8b4c2022 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: crd-test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. +appVersion: 1.16.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml b/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml new file mode 100644 index 000000000..1d7350f1d --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml new file mode 100644 index 000000000..90545a6a3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +description: Chart with subchart notes +name: chart-with-subchart-notes +version: 0.0.1 +dependencies: + - name: subchart-with-notes + version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml new file mode 100644 index 000000000..f0fead9ee --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Subchart with notes +name: subchart-with-notes +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt new file mode 100644 index 000000000..1f61a294e --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt @@ -0,0 +1 @@ +SUBCHART NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt new file mode 100644 index 000000000..9e166d370 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt @@ -0,0 +1 @@ +PARENT NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml new file mode 100644 index 000000000..1bc230200 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +description: Chart with subchart that needs to be fetched +name: chart-with-subchart-update +version: 0.0.1 +dependencies: + - name: subchart-with-notes + version: 0.0.1 + repository: file://../chart-with-subchart-notes/charts diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml new file mode 100644 index 000000000..f0fead9ee --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Subchart with notes +name: subchart-with-notes +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt new file mode 100644 index 000000000..1f61a294e --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt @@ -0,0 +1 @@ +SUBCHART NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt new file mode 100644 index 000000000..9e166d370 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt @@ -0,0 +1 @@ +PARENT NOTES diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index dea866e4d..c263d32e7 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -75,24 +75,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client.Namespace = settings.Namespace() - if client.Version == "" && client.Devel { - debug("setting version to >0.0.0-0") - client.Version = ">0.0.0-0" - } - - vals, err := valueOpts.MergeValues(getter.All(settings)) - if err != nil { - return err - } - - chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings) - if err != nil { - return err - } - + // Fixes #7002 - Support reading values from STDIN for `upgrade` command + // Must load values AFTER determining if we have to call install so that values loaded from stdin are are not read twice 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 { @@ -105,6 +91,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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 @@ -112,15 +99,33 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Atomic = client.Atomic instClient.PostRenderer = client.PostRenderer instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation + instClient.SubNotes = client.SubNotes 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 } } + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" + } + + chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings) + if err != nil { + return err + } + + vals, err := valueOpts.MergeValues(getter.All(settings)) + if err != nil { + return err + } + // Check chart dependencies to make sure all are present in /charts ch, err := loader.Load(chartPath) if err != nil { @@ -170,6 +175,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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/upgrade_test.go b/cmd/helm/upgrade_test.go index 3cecbe6d3..6f260ae57 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -19,6 +19,7 @@ package main import ( "fmt" "io/ioutil" + "os" "path/filepath" "strings" "testing" @@ -79,6 +80,10 @@ func TestUpgradeCmd(t *testing.T) { missingDepsPath := "testdata/testcharts/chart-missing-deps" badDepsPath := "testdata/testcharts/chart-bad-requirements" + relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) + } + relMock := func(n string, v int, ch *chart.Chart) *release.Release { return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) } @@ -138,6 +143,25 @@ func TestUpgradeCmd(t *testing.T) { golden: "output/upgrade-with-bad-dependencies.txt", wantError: true, }, + { + name: "upgrade a non-existent release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade-with-bad-or-missing-existing-release.txt", + wantError: true, + }, + { + name: "upgrade a failed release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade.txt", + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusFailed)}, + }, + { + name: "upgrade a pending install release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade-with-bad-or-missing-existing-release.txt", + wantError: true, + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusPendingInstall)}, + }, } runTestCmd(t, tests) } @@ -196,6 +220,38 @@ func TestUpgradeWithStringValue(t *testing.T) { } +func TestUpgradeInstallWithSubchartNotes(t *testing.T) { + + releaseName := "wacky-bunny-v1" + relMock, ch, _ := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 1, ch)) + + cmd := fmt.Sprintf("upgrade %s -i --render-subchart-notes '%s'", releaseName, "testdata/testcharts/chart-with-subchart-notes") + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + upgradedRel, err := store.Get(releaseName, 2) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(upgradedRel.Info.Notes, "PARENT NOTES") { + t.Errorf("The parent notes are not set correctly. NOTES: %s", upgradedRel.Info.Notes) + } + + if !strings.Contains(upgradedRel.Info.Notes, "SUBCHART NOTES") { + t.Errorf("The subchart notes are not set correctly. NOTES: %s", upgradedRel.Info.Notes) + } + +} + func TestUpgradeWithValuesFile(t *testing.T) { releaseName := "funny-bunny-v4" @@ -224,6 +280,69 @@ func TestUpgradeWithValuesFile(t *testing.T) { } +func TestUpgradeWithValuesFromStdin(t *testing.T) { + + releaseName := "funny-bunny-v5" + relMock, ch, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + in, err := os.Open("testdata/testcharts/upgradetest/values.yaml") + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + cmd := fmt.Sprintf("upgrade %s --values - '%s'", releaseName, chartPath) + _, _, err = executeActionCommandStdinC(store, in, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: beer") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } +} + +func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { + + releaseName := "funny-bunny-v6" + _, _, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + in, err := os.Open("testdata/testcharts/upgradetest/values.yaml") + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + cmd := fmt.Sprintf("upgrade %s -f - --install '%s'", releaseName, chartPath) + _, _, err = executeActionCommandStdinC(store, in, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 1) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: beer") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } + +} + func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { tmpChart := ensure.TempDir(t) configmapData, err := ioutil.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml") diff --git a/cmd/helm/version.go b/cmd/helm/version.go index 3067f7289..b3f831e07 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -39,6 +39,14 @@ version.BuildInfo{Version:"v2.0.0", GitCommit:"ff52399e51bb880526e9cd0ed8386f643 - GitCommit is the SHA for the commit that this version was built from. - GitTreeState is "clean" if there are no local code changes when this binary was built, and "dirty" if the binary was built from locally modified code. + +When using the --template flag the following properties are available to use in +the template: + +- .Version contains the semantic version of Helm +- .GitCommit is the git commit +- .GitTreeState is the state of the git tree when Helm was built +- .GoVersion contains the version of Go that Helm was compiled with ` type versionOptions struct { diff --git a/go.mod b/go.mod index 7ba7a5542..64ebfe307 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.13 require ( github.com/BurntSushi/toml v0.3.1 - github.com/Masterminds/semver/v3 v3.0.3 - github.com/Masterminds/sprig/v3 v3.0.2 + github.com/DATA-DOG/go-sqlmock v1.4.1 + github.com/Masterminds/semver/v3 v3.1.0 + github.com/Masterminds/sprig/v3 v3.1.0 + github.com/Masterminds/squirrel v1.2.0 github.com/Masterminds/vcs v1.13.1 github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 github.com/containerd/containerd v1.3.2 @@ -18,25 +20,28 @@ require ( github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.7.1 github.com/gosuri/uitable v0.0.4 + github.com/jmoiron/sqlx v1.2.0 + github.com/lib/pq v1.3.0 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 github.com/pkg/errors v0.9.1 + github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3 github.com/sirupsen/logrus v1.4.2 - github.com/spf13/cobra v0.0.5 + github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.5.1 github.com/xeipuuv/gojsonschema v1.1.0 - golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d - 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 + golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 + k8s.io/api v0.18.0 + k8s.io/apiextensions-apiserver v0.18.0 + k8s.io/apimachinery v0.18.0 + k8s.io/cli-runtime v0.18.0 + k8s.io/client-go v0.18.0 k8s.io/klog v1.0.0 - k8s.io/kubectl v0.17.3 - sigs.k8s.io/yaml v1.1.0 + k8s.io/kubectl v0.18.0 + sigs.k8s.io/yaml v1.2.0 ) replace ( diff --git a/go.sum b/go.sum index 140a74761..37fba6fbb 100644 --- a/go.sum +++ b/go.sum @@ -23,15 +23,20 @@ github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VY github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= +github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+BQa3B8= -github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU= +github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y= +github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= +github.com/Masterminds/squirrel v1.2.0 h1:K1NhbTO21BWG47IVR0OnIZuE0LZcXAYqywrC3Ko53KI= +github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/Masterminds/vcs v1.13.1 h1:NL3G1X7/7xduQtA2sJLpVpfHTNBALVNSjob6KEjPXNQ= github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= @@ -39,6 +44,7 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tT github.com/Microsoft/hcsshim v0.8.7 h1:ptnOoufxGSzauVTsdE+wMYnCWA301PdoN4xg5oRdZpg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -53,6 +59,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= @@ -79,6 +86,7 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -95,6 +103,7 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -104,22 +113,26 @@ github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c= github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw= github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -145,6 +158,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -170,6 +185,7 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0 github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= @@ -218,7 +234,19 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -226,13 +254,15 @@ github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -250,9 +280,10 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -261,6 +292,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -274,11 +307,15 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -287,15 +324,22 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= @@ -306,8 +350,9 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -315,6 +360,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= @@ -329,16 +382,20 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -352,7 +409,6 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -363,11 +419,16 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -394,12 +455,13 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -407,20 +469,36 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3 h1:xkBtI5JktwbW/vf4vopBbhYsRFTGfQWHYXzC0/qYwxI= +github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -429,14 +507,19 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -445,17 +528,21 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -476,6 +563,9 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMzt github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -485,6 +575,7 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -493,21 +584,22 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -521,6 +613,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -540,6 +634,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -548,17 +643,19 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -574,41 +671,39 @@ golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -618,9 +713,12 @@ gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= @@ -631,8 +729,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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.5/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= @@ -640,44 +738,43 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81 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.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/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= +k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= +k8s.io/apiextensions-apiserver v0.18.0 h1:HN4/P8vpGZFvB5SOMuPPH2Wt9Y/ryX+KRvIyAkchu1Q= +k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= +k8s.io/apimachinery v0.18.0 h1:fuPfYpk3cs1Okp/515pAf0dNhL66+8zk8RLbSX+EgAE= +k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= +k8s.io/cli-runtime v0.18.0 h1:jG8XpSqQ5TrV0N+EZ3PFz6+gqlCk71dkggWCCq9Mq34= +k8s.io/cli-runtime v0.18.0/go.mod h1:1eXfmBsIJosjn9LjEBUd2WVPoPAY9XGTqTFcPMIBsUQ= +k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= +k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= +k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/component-base v0.18.0 h1:I+lP0fNfsEdTDpHaL61bCAqTZLoiWjEEP304Mo5ZQgE= +k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= 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/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 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.3 h1:9HHYj07kuFkM+sMJMOyQX29CKWq4lvKAG1UIPxNPMQ4= -k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kubectl v0.18.0 h1:hu52Ndq/d099YW+3sS3VARxFz61Wheiq8K9S7oa82Dk= +k8s.io/kubectl v0.18.0/go.mod h1:LOkWx9Z5DXMEg5KtOjHhRiC1fqJPLyCr3KtQgEolCkU= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -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= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +k8s.io/metrics v0.18.0/go.mod h1:8aYTW18koXqjLVKL7Ds05RPMX9ipJZI3mywYvBOxXd4= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc h1:MksmcCZQWAQJCTA5T0jgI/0sJ51AVm4Z41MrmfczEoc= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/internal/fileutil/fileutil.go b/internal/fileutil/fileutil.go new file mode 100644 index 000000000..739093f3b --- /dev/null +++ b/internal/fileutil/fileutil.go @@ -0,0 +1,51 @@ +/* +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 fileutil + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + + "helm.sh/helm/v3/internal/third_party/dep/fs" +) + +// AtomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a +// disk. +func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error { + tempFile, err := ioutil.TempFile(filepath.Split(filename)) + if err != nil { + return err + } + tempName := tempFile.Name() + + if _, err := io.Copy(tempFile, reader); err != nil { + tempFile.Close() // return value is ignored as we are already on error path + return err + } + + if err := tempFile.Close(); err != nil { + return err + } + + if err := os.Chmod(tempName, mode); err != nil { + return err + } + + return fs.RenameWithFallback(tempName, filename) +} diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go new file mode 100644 index 000000000..9a4bc32c9 --- /dev/null +++ b/internal/fileutil/fileutil_test.go @@ -0,0 +1,62 @@ +/* +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 fileutil + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestAtomicWriteFile(t *testing.T) { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + testpath := filepath.Join(dir, "test") + stringContent := "Test content" + reader := bytes.NewReader([]byte(stringContent)) + mode := os.FileMode(0644) + + err = AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error: %s", err) + } + + got, err := ioutil.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if stringContent != string(got) { + t.Fatalf("expected: %s, got: %s", stringContent, string(got)) + } + + gotinfo, err := os.Stat(testpath) + if err != nil { + t.Fatal(err) + } + + if mode != gotinfo.Mode() { + t.Fatalf("expected %s: to be the same mode as %s", + mode, gotinfo.Mode()) + } +} diff --git a/internal/ignore/rules.go b/internal/ignore/rules.go index 9049aff0d..a80923baf 100644 --- a/internal/ignore/rules.go +++ b/internal/ignore/rules.go @@ -18,6 +18,7 @@ package ignore import ( "bufio" + "bytes" "io" "log" "os" @@ -65,8 +66,18 @@ func Parse(file io.Reader) (*Rules, error) { r := &Rules{patterns: []*pattern{}} s := bufio.NewScanner(file) + currentLine := 0 + utf8bom := []byte{0xEF, 0xBB, 0xBF} for s.Scan() { - if err := r.parseRule(s.Text()); err != nil { + scannedBytes := s.Bytes() + // We trim UTF8 BOM + if currentLine == 0 { + scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) + } + line := string(scannedBytes) + currentLine++ + + if err := r.parseRule(line); err != nil { return r, err } } diff --git a/internal/test/ensure/ensure.go b/internal/test/ensure/ensure.go index b4775df80..6219ad626 100644 --- a/internal/test/ensure/ensure.go +++ b/internal/test/ensure/ensure.go @@ -21,6 +21,7 @@ import ( "os" "testing" + "helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath/xdg" ) @@ -31,6 +32,9 @@ func HelmHome(t *testing.T) func() { os.Setenv(xdg.CacheHomeEnvVar, base) os.Setenv(xdg.ConfigHomeEnvVar, base) os.Setenv(xdg.DataHomeEnvVar, base) + os.Setenv(helmpath.CacheHomeEnvVar, "") + os.Setenv(helmpath.ConfigHomeEnvVar, "") + os.Setenv(helmpath.DataHomeEnvVar, "") return func() { os.RemoveAll(base) } diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go index a9678d8c1..98a31aec6 100644 --- a/internal/third_party/dep/fs/fs_test.go +++ b/internal/third_party/dep/fs/fs_test.go @@ -35,7 +35,6 @@ import ( "io/ioutil" "os" "os/exec" - "os/user" "path/filepath" "runtime" "sync" @@ -175,13 +174,9 @@ func TestCopyDirFail_SrcInaccessible(t *testing.T) { t.Skip("skipping on windows") } - var currentUser, err = user.Current() + var currentUID = os.Getuid() - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } - - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } @@ -214,13 +209,9 @@ func TestCopyDirFail_DstInaccessible(t *testing.T) { t.Skip("skipping on windows") } - var currentUser, err = user.Current() + var currentUID = os.Getuid() - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } - - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } @@ -314,13 +305,9 @@ func TestCopyDirFailOpen(t *testing.T) { t.Skip("skipping on windows") } - var currentUser, err = user.Current() - - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } + var currentUID = os.Getuid() - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } @@ -483,13 +470,9 @@ func TestCopyFileFail(t *testing.T) { t.Skip("skipping on windows") } - var currentUser, err = user.Current() + var currentUID = os.Getuid() - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } - - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } @@ -574,13 +557,9 @@ func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() { func TestIsDir(t *testing.T) { - var currentUser, err = user.Current() + var currentUID = os.Getuid() - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } - - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } @@ -631,13 +610,9 @@ func TestIsDir(t *testing.T) { func TestIsSymlink(t *testing.T) { - var currentUser, err = user.Current() - - if err != nil { - t.Fatalf("Failed to get name of current user: %s", err) - } + var currentUID = os.Getuid() - if currentUser.Name == "root" { + if currentUID == 0 { // Skipping if root, because all files are accessible t.Skip("Skipping for root user") } diff --git a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go index da93a6910..103db35c4 100644 --- a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go +++ b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go @@ -17,6 +17,7 @@ limitations under the License. package util import ( + "context" "sort" apps "k8s.io/api/apps/v1" @@ -116,7 +117,7 @@ func GetNewReplicaSet(deployment *apps.Deployment, c appsclient.AppsV1Interface) // RsListFromClient returns an rsListFunc that wraps the given client. func RsListFromClient(c appsclient.AppsV1Interface) RsListFunc { return func(namespace string, options metav1.ListOptions) ([]*apps.ReplicaSet, error) { - rsList, err := c.ReplicaSets(namespace).List(options) + rsList, err := c.ReplicaSets(namespace).List(context.Background(), options) if err != nil { return nil, err } diff --git a/internal/version/version.go b/internal/version/version.go index fd0616920..baa65a028 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -30,7 +30,7 @@ var ( // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. // Increment patch number for critical fixes to existing releases. - version = "v3.1" + version = "v3.2" // metadata is extra build time data metadata = "" diff --git a/pkg/action/action.go b/pkg/action/action.go index e4db942c8..bb9ef5f71 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -17,8 +17,13 @@ limitations under the License. package action import ( + "bytes" + "fmt" + "os" "path" + "path/filepath" "regexp" + "strings" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -28,9 +33,13 @@ import ( "k8s.io/client-go/rest" "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/time" @@ -53,16 +62,17 @@ var ( errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53") ) -// ValidName is a regular expression for names. +// ValidName is a regular expression for resource names. // // According to the Kubernetes help text, the regular expression it uses is: // -// (([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])? +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* // -// We modified that. First, we added start and end delimiters. Second, we changed -// the final ? to + to require that the pattern match at least once. This modification -// prevents an empty string from matching. -var ValidName = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$") +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) // Configuration injects the dependencies that all actions share. type Configuration struct { @@ -84,6 +94,132 @@ type Configuration struct { Log func(string, ...interface{}) } +// renderResources renders the templates in a chart +// +// TODO: This function is badly in need of a refactor. +func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { + hs := []*release.Hook{} + b := bytes.NewBuffer(nil) + + caps, err := c.getCapabilities() + if err != nil { + return hs, b, "", err + } + + if ch.Metadata.KubeVersion != "" { + if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { + return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) + } + } + + var files map[string]string + var err2 error + + // A `helm template` or `helm install --dry-run` should not talk to the remote cluster. + // It will break in interesting and exotic ways because other data (e.g. discovery) + // is mocked. It is not up to the template author to decide when the user wants to + // connect to the cluster. So when the user says to dry run, respect the user's + // wishes and do not connect to the cluster. + if !dryRun && c.RESTClientGetter != nil { + rest, err := c.RESTClientGetter.ToRESTConfig() + if err != nil { + return hs, b, "", err + } + files, err2 = engine.RenderWithClient(ch, values, rest) + } else { + files, err2 = engine.Render(ch, values) + } + + if err2 != nil { + return hs, b, "", err2 + } + + // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, + // pull it out of here into a separate file so that we can actually use the output of the rendered + // text file. We have to spin through this map because the file contains path information, so we + // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip + // it in the sortHooks. + var notesBuffer bytes.Buffer + for k, v := range files { + if strings.HasSuffix(k, notesFileSuffix) { + if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) { + // If buffer contains data, add newline before adding more + if notesBuffer.Len() > 0 { + notesBuffer.WriteString("\n") + } + notesBuffer.WriteString(v) + } + delete(files, k) + } + } + notes := notesBuffer.String() + + // Sort hooks, manifests, and partials. Only hooks and manifests are returned, + // as partials are not used after renderer.Render. Empty manifests are also + // removed here. + hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) + if err != nil { + // By catching parse errors here, we can prevent bogus releases from going + // to Kubernetes. + // + // We return the files as a big blob of data to help the user debug parser + // errors. + for name, content := range files { + if strings.TrimSpace(content) == "" { + continue + } + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + } + return hs, b, "", err + } + + // Aggregate all valid manifests into one big doc. + fileWritten := make(map[string]bool) + + if includeCrds { + for _, crd := range ch.CRDObjects() { + if outputDir == "" { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) + } else { + err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) + if err != nil { + return hs, b, "", err + } + fileWritten[crd.Name] = true + } + } + } + + for _, m := range manifests { + if outputDir == "" { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + } else { + newDir := outputDir + if useReleaseName { + newDir = filepath.Join(outputDir, releaseName) + } + // NOTE: We do not have to worry about the post-renderer because + // output dir is only used by `helm template`. In the next major + // release, we should move this logic to template only as it is not + // used by install or upgrade + err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) + if err != nil { + return hs, b, "", err + } + fileWritten[m.Name] = true + } + } + + if pr != nil { + b, err = pr.Run(b) + if err != nil { + return hs, b, notes, errors.Wrap(err, "error while running post render on files") + } + } + + return hs, b, notes, nil +} + // RESTClientGetter gets the rest client type RESTClientGetter interface { ToRESTConfig() (*rest.Config, error) @@ -221,23 +357,23 @@ func (c *Configuration) recordRelease(r *release.Release) { } // Init initializes the action configuration -func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace string, helmDriver string, log DebugLog) error { +func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { kc := kube.New(getter) kc.Log = log - clientset, err := kc.Factory.KubernetesClientSet() - if err != nil { - return err + lazyClient := &lazyClient{ + namespace: namespace, + clientFn: kc.Factory.KubernetesClientSet, } var store *storage.Storage switch helmDriver { case "secret", "secrets", "": - d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace)) + d := driver.NewSecrets(newSecretClient(lazyClient)) d.Log = log store = storage.Init(d) case "configmap", "configmaps": - d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace)) + d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) d.Log = log store = storage.Init(d) case "memory": @@ -255,6 +391,16 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac } d.SetNamespace(namespace) store = storage.Init(d) + case "sql": + d, err := driver.NewSQL( + os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), + log, + namespace, + ) + if err != nil { + panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err)) + } + store = storage.Init(d) default: // Not sure what to do here. panic("Unknown driver in HELM_DRIVER: " + helmDriver) diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 36ef261a3..0cbdb162b 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -316,3 +316,40 @@ func TestGetVersionSet(t *testing.T) { t.Error("Non-existent version is reported found.") } } + +// TestValidName is a regression test for ValidName +// +// Kubernetes has strict naming conventions for resource names. This test represents +// those conventions. +// +// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// NOTE: At the time of this writing, the docs above say that names cannot begin with +// digits. However, `kubectl`'s regular expression explicit allows this, and +// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits. +func TestValidName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + } + for input, expectPass := range names { + if ValidName.MatchString(input) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 5781cc913..4a4b8ebad 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -55,15 +55,22 @@ func (d *Dependency) List(chartpath string, out io.Writer) error { return nil } - d.printDependencies(chartpath, out, c.Metadata.Dependencies) + d.printDependencies(chartpath, out, c) fmt.Fprintln(out) d.printMissing(chartpath, out, c.Metadata.Dependencies) return nil } -func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency) string { +func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string { filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") + // If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs. + // Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here + // to preserved backward compatibility. In Helm 2/3, there is a "difference" between + // the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outouts + // "unpacked"). Early in Helm 2's history, this would have made a difference. But it no + // longer does. However, since this code shipped with Helm 3, the output must remain stable + // until Helm 4. switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); { case err != nil: return "bad pattern" @@ -91,58 +98,52 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency) s return "invalid version" } - if constraint.Check(v) { - return "ok" + if !constraint.Check(v) { + return "wrong version" } - return "wrong version" } return "ok" } } + // End unnecessary code. - folder := filepath.Join(chartpath, "charts", dep.Name) - if fi, err := os.Stat(folder); err != nil { - return "missing" - } else if !fi.IsDir() { - return "mispackaged" - } - - c, err := loader.Load(folder) - if err != nil { - return "corrupt" + var depChart *chart.Chart + for _, item := range parent.Dependencies() { + if item.Name() == dep.Name { + depChart = item + } } - if c.Name() != dep.Name { - return "misnamed" + if depChart == nil { + return "missing" } - if c.Metadata.Version != dep.Version { + if depChart.Metadata.Version != dep.Version { constraint, err := semver.NewConstraint(dep.Version) if err != nil { return "invalid version" } - v, err := semver.NewVersion(c.Metadata.Version) + v, err := semver.NewVersion(depChart.Metadata.Version) if err != nil { return "invalid version" } - if constraint.Check(v) { - return "unpacked" + if !constraint.Check(v) { + return "wrong version" } - return "wrong version" } return "unpacked" } // printDependencies prints all of the dependencies in the yaml file. -func (d *Dependency) printDependencies(chartpath string, out io.Writer, reqs []*chart.Dependency) { +func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { table := uitable.New() table.MaxColWidth = 80 table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") - for _, row := range reqs { - table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row)) + for _, row := range c.Metadata.Dependencies { + table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c)) } fmt.Fprintln(out, table) } diff --git a/pkg/action/dependency_test.go b/pkg/action/dependency_test.go new file mode 100644 index 000000000..158acbfb9 --- /dev/null +++ b/pkg/action/dependency_test.go @@ -0,0 +1,58 @@ +/* +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 ( + "bytes" + "testing" + + "helm.sh/helm/v3/internal/test" +) + +func TestList(t *testing.T) { + for _, tcase := range []struct { + chart string + golden string + }{ + { + chart: "testdata/charts/chart-with-compressed-dependencies", + golden: "output/compressed-deps.txt", + }, + { + chart: "testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz", + golden: "output/compressed-deps-tgz.txt", + }, + { + chart: "testdata/charts/chart-with-uncompressed-dependencies", + golden: "output/uncompressed-deps.txt", + }, + { + chart: "testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz", + golden: "output/uncompressed-deps-tgz.txt", + }, + { + chart: "testdata/charts/chart-missing-deps", + golden: "output/missing-deps.txt", + }, + } { + buf := bytes.Buffer{} + if err := NewDependency().List(tcase.chart, &buf); err != nil { + t.Fatal(err) + } + test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden) + } +} diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index a161f9377..40c1ffdb6 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -38,7 +38,8 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, } } - sort.Sort(hookByWeight(executingHooks)) + // hooke are pre-ordered by kind, so keep order stable + sort.Stable(hookByWeight(executingHooks)) for _, h := range executingHooks { // Set default delete policy to before-hook-creation diff --git a/pkg/action/install.go b/pkg/action/install.go index e7b481d47..351e0928c 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -39,8 +39,8 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" "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" @@ -144,20 +144,24 @@ func (i *Install) installCRDs(crds []chart.CRD) error { } totalItems = append(totalItems, res...) } - // Invalidate the local cache, since it will not have the new CRDs - // present. - discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient() - if err != nil { - return err - } - i.cfg.Log("Clearing discovery cache") - discoveryClient.Invalidate() - // Give time for the CRD to be recognized. - if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil { - return err + if len(totalItems) > 0 { + // Invalidate the local cache, since it will not have the new CRDs + // present. + discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient() + if err != nil { + return err + } + i.cfg.Log("Clearing discovery cache") + discoveryClient.Invalidate() + // Give time for the CRD to be recognized. + + if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil { + return err + } + + // Make sure to force a rebuild of the cache. + discoveryClient.ServerGroups() } - // Make sure to force a rebuild of the cache. - discoveryClient.ServerGroups() return nil } @@ -231,7 +235,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. rel := i.createRelease(chrt, vals) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() @@ -246,19 +250,27 @@ 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, // we'll end up in a state where we will delete those resources upon // deleting the release because the manifest will be pointing at that // resource - if !i.ClientOnly && !isUpgrade { - if err := existingResourceConflict(resources); err != nil { + if !i.ClientOnly && !isUpgrade && len(resources) > 0 { + 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") } } @@ -321,8 +333,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 && len(resources) > 0 { + if _, err := i.cfg.KubeClient.Create(resources); err != nil { + return i.failRelease(rel, err) + } + } else if len(resources) > 0 { + if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { + return i.failRelease(rel, err) + } } if i.Wait { @@ -460,125 +478,6 @@ func (i *Install) replaceRelease(rel *release.Release) error { return i.recordRelease(last) } -// renderResources renders the templates in a chart -func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer) ([]*release.Hook, *bytes.Buffer, string, error) { - hs := []*release.Hook{} - b := bytes.NewBuffer(nil) - - caps, err := c.getCapabilities() - if err != nil { - return hs, b, "", err - } - - if ch.Metadata.KubeVersion != "" { - if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { - return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) - } - } - - var files map[string]string - var err2 error - - if c.RESTClientGetter != nil { - rest, err := c.RESTClientGetter.ToRESTConfig() - if err != nil { - return hs, b, "", err - } - files, err2 = engine.RenderWithClient(ch, values, rest) - } else { - files, err2 = engine.Render(ch, values) - } - - if err2 != nil { - return hs, b, "", err2 - } - - // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, - // pull it out of here into a separate file so that we can actually use the output of the rendered - // text file. We have to spin through this map because the file contains path information, so we - // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip - // it in the sortHooks. - var notesBuffer bytes.Buffer - for k, v := range files { - if strings.HasSuffix(k, notesFileSuffix) { - if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) { - // If buffer contains data, add newline before adding more - if notesBuffer.Len() > 0 { - notesBuffer.WriteString("\n") - } - notesBuffer.WriteString(v) - } - delete(files, k) - } - } - notes := notesBuffer.String() - - // Sort hooks, manifests, and partials. Only hooks and manifests are returned, - // as partials are not used after renderer.Render. Empty manifests are also - // removed here. - hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) - if err != nil { - // By catching parse errors here, we can prevent bogus releases from going - // to Kubernetes. - // - // We return the files as a big blob of data to help the user debug parser - // errors. - for name, content := range files { - if strings.TrimSpace(content) == "" { - continue - } - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) - } - return hs, b, "", err - } - - // Aggregate all valid manifests into one big doc. - fileWritten := make(map[string]bool) - - if includeCrds { - for _, crd := range ch.CRDObjects() { - if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) - } else { - err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) - if err != nil { - return hs, b, "", err - } - fileWritten[crd.Name] = true - } - } - } - - for _, m := range manifests { - if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) - } else { - newDir := outputDir - if useReleaseName { - newDir = filepath.Join(outputDir, releaseName) - } - // NOTE: We do not have to worry about the post-renderer because - // output dir is only used by `helm template`. In the next major - // release, we should move this logic to template only as it is not - // used by install or upgrade - err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) - if err != nil { - return hs, b, "", err - } - fileWritten[m.Name] = true - } - } - - if pr != nil { - b, err = pr.Run(b) - if err != nil { - return hs, b, notes, errors.Wrap(err, "error while running post render on files") - } - } - - return hs, b, notes, nil -} - // write the to /. controls if the file is created or content will be appended func writeToFile(outputDir string, name string, data string, append bool) error { outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) @@ -744,6 +643,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 bf47895a1..6c4012cfd 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/assert" "helm.sh/helm/v3/internal/test" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" @@ -240,6 +241,27 @@ func TestInstallRelease_DryRun(t *testing.T) { is.Equal(res.Info.Description, "Dry run complete") } +// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run. +func TestInstallRelease_DryRun_Lookup(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRun = true + vals := map[string]interface{}{} + + mockChart := buildChart(withSampleTemplates()) + mockChart.Templates = append(mockChart.Templates, &chart.File{ + Name: "templates/lookup", + Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), + }) + + res, err := instAction.Run(mockChart, vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.Contains(res.Manifest, "goodbye: map[]") +} + func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/lazyclient.go b/pkg/action/lazyclient.go new file mode 100644 index 000000000..0bd57ff5b --- /dev/null +++ b/pkg/action/lazyclient.go @@ -0,0 +1,182 @@ +/* +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 ( + "context" + "sync" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +// lazyClient is a workaround to deal with Kubernetes having an unstable client API. +// In Kubernetes v1.18 the defaults where removed which broke creating a +// client without an explicit configuration. ಠ_ಠ +type lazyClient struct { + // client caches an initialized kubernetes client + initClient sync.Once + client kubernetes.Interface + clientErr error + + // clientFn loads a kubernetes client + clientFn func() (*kubernetes.Clientset, error) + + // namespace passed to each client request + namespace string +} + +func (s *lazyClient) init() error { + s.initClient.Do(func() { + s.client, s.clientErr = s.clientFn() + }) + return s.clientErr +} + +// secretClient implements a corev1.SecretsInterface +type secretClient struct{ *lazyClient } + +var _ corev1.SecretInterface = (*secretClient)(nil) + +func newSecretClient(lc *lazyClient) *secretClient { + return &secretClient{lazyClient: lc} +} + +func (s *secretClient) Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Create(ctx, secret, opts) +} + +func (s *secretClient) Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Update(ctx, secret, opts) +} + +func (s *secretClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + if err := s.init(); err != nil { + return err + } + return s.client.CoreV1().Secrets(s.namespace).Delete(ctx, name, opts) +} + +func (s *secretClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + if err := s.init(); err != nil { + return err + } + return s.client.CoreV1().Secrets(s.namespace).DeleteCollection(ctx, opts, listOpts) +} + +func (s *secretClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Get(ctx, name, opts) +} + +func (s *secretClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).List(ctx, opts) +} + +func (s *secretClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Watch(ctx, opts) +} + +func (s *secretClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Patch(ctx, name, pt, data, opts, subresources...) +} + +// configMapClient implements a corev1.ConfigMapInterface +type configMapClient struct{ *lazyClient } + +var _ corev1.ConfigMapInterface = (*configMapClient)(nil) + +func newConfigMapClient(lc *lazyClient) *configMapClient { + return &configMapClient{lazyClient: lc} +} + +func (c *configMapClient) Create(ctx context.Context, configMap *v1.ConfigMap, opts metav1.CreateOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, configMap, opts) +} + +func (c *configMapClient) Update(ctx context.Context, configMap *v1.ConfigMap, opts metav1.UpdateOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Update(ctx, configMap, opts) +} + +func (c *configMapClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + if err := c.init(); err != nil { + return err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Delete(ctx, name, opts) +} + +func (c *configMapClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + if err := c.init(); err != nil { + return err + } + return c.client.CoreV1().ConfigMaps(c.namespace).DeleteCollection(ctx, opts, listOpts) +} + +func (c *configMapClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, name, opts) +} + +func (c *configMapClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).List(ctx, opts) +} + +func (c *configMapClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Watch(ctx, opts) +} + +func (c *configMapClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Patch(ctx, name, pt, data, opts, subresources...) +} diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index b7a1da757..795c3c747 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "context" "fmt" "io" "time" @@ -81,7 +82,7 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { for _, e := range h.Events { if e == release.HookTest { req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{}) - logReader, err := req.Stream() + logReader, err := req.Stream(context.Background()) if err != nil { return errors.Wrapf(err, "unable to get pod logs for %s", h.Name) } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 942c9d8af..81812983f 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -211,7 +211,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas } deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "has no deployed releases") { return nil, err } // Supersede all previous deployments, see issue #2941. diff --git a/pkg/action/show.go b/pkg/action/show.go index 14b59a5ea..cc85477cd 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -51,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/testdata/charts/chart-missing-deps/.helmignore b/pkg/action/testdata/charts/chart-missing-deps/.helmignore new file mode 100755 index 000000000..e2cf7941f --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/.helmignore @@ -0,0 +1,5 @@ +.git +# OWNERS file for Kubernetes +OWNERS +# example production yaml +values-production.yaml \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml b/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml new file mode 100755 index 000000000..8304984fd --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml @@ -0,0 +1,20 @@ +appVersion: 4.9.8 +description: Web publishing platform for building blogs and websites. +engine: gotpl +home: http://www.wordpress.com/ +icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png +keywords: +- wordpress +- cms +- blog +- http +- web +- application +- php +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: chart-with-missing-deps +sources: +- https://github.com/bitnami/bitnami-docker-wordpress +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-missing-deps/README.md b/pkg/action/testdata/charts/chart-missing-deps/README.md new file mode 100755 index 000000000..5859a17fa --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/README.md @@ -0,0 +1,232 @@ +# WordPress + +[WordPress](https://wordpress.org/) is one of the most versatile open source content management systems on the market. A publishing platform for building blogs and websites. + +## TL;DR; + +```console +$ helm install stable/wordpress +``` + +## Introduction + +This chart bootstraps a [WordPress](https://github.com/bitnami/bitnami-docker-wordpress) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +It also packages the [Bitnami MariaDB chart](https://github.com/kubernetes/charts/tree/master/stable/mariadb) which is required for bootstrapping a MariaDB deployment for the database requirements of the WordPress application. + +## Prerequisites + +- Kubernetes 1.4+ with Beta APIs enabled +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +$ helm install --name my-release stable/wordpress +``` + +The command deploys WordPress on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```console +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following table lists the configurable parameters of the WordPress chart and their default values. + +| Parameter | Description | Default | +|----------------------------------|--------------------------------------------|---------------------------------------------------------| +| `image.registry` | WordPress image registry | `docker.io` | +| `image.repository` | WordPress image name | `bitnami/wordpress` | +| `image.tag` | WordPress image tag | `{VERSION}` | +| `image.pullPolicy` | Image pull policy | `Always` if `imageTag` is `latest`, else `IfNotPresent` | +| `image.pullSecrets` | Specify image pull secrets | `nil` | +| `wordpressUsername` | User of the application | `user` | +| `wordpressPassword` | Application password | _random 10 character long alphanumeric string_ | +| `wordpressEmail` | Admin email | `user@example.com` | +| `wordpressFirstName` | First name | `FirstName` | +| `wordpressLastName` | Last name | `LastName` | +| `wordpressBlogName` | Blog name | `User's Blog!` | +| `wordpressTablePrefix` | Table prefix | `wp_` | +| `allowEmptyPassword` | Allow DB blank passwords | `yes` | +| `smtpHost` | SMTP host | `nil` | +| `smtpPort` | SMTP port | `nil` | +| `smtpUser` | SMTP user | `nil` | +| `smtpPassword` | SMTP password | `nil` | +| `smtpUsername` | User name for SMTP emails | `nil` | +| `smtpProtocol` | SMTP protocol [`tls`, `ssl`] | `nil` | +| `replicaCount` | Number of WordPress Pods to run | `1` | +| `mariadb.enabled` | Deploy MariaDB container(s) | `true` | +| `mariadb.rootUser.password` | MariaDB admin password | `nil` | +| `mariadb.db.name` | Database name to create | `bitnami_wordpress` | +| `mariadb.db.user` | Database user to create | `bn_wordpress` | +| `mariadb.db.password` | Password for the database | _random 10 character long alphanumeric string_ | +| `externalDatabase.host` | Host of the external database | `localhost` | +| `externalDatabase.user` | Existing username in the external db | `bn_wordpress` | +| `externalDatabase.password` | Password for the above username | `nil` | +| `externalDatabase.database` | Name of the existing database | `bitnami_wordpress` | +| `externalDatabase.port` | Database port number | `3306` | +| `serviceType` | Kubernetes Service type | `LoadBalancer` | +| `serviceExternalTrafficPolicy` | Enable client source IP preservation | `Cluster` | +| `nodePorts.http` | Kubernetes http node port | `""` | +| `nodePorts.https` | Kubernetes https node port | `""` | +| `healthcheckHttps` | Use https for liveliness and readiness | `false` | +| `ingress.enabled` | Enable ingress controller resource | `false` | +| `ingress.hosts[0].name` | Hostname to your WordPress installation | `wordpress.local` | +| `ingress.hosts[0].path` | Path within the url structure | `/` | +| `ingress.hosts[0].tls` | Utilize TLS backend in ingress | `false` | +| `ingress.hosts[0].tlsSecret` | TLS Secret (certificates) | `wordpress.local-tls-secret` | +| `ingress.hosts[0].annotations` | Annotations for this host's ingress record | `[]` | +| `ingress.secrets[0].name` | TLS Secret Name | `nil` | +| `ingress.secrets[0].certificate` | TLS Secret Certificate | `nil` | +| `ingress.secrets[0].key` | TLS Secret Key | `nil` | +| `persistence.enabled` | Enable persistence using PVC | `true` | +| `persistence.existingClaim` | Enable persistence using an existing PVC | `nil` | +| `persistence.storageClass` | PVC Storage Class | `nil` (uses alpha storage class annotation) | +| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` | +| `persistence.size` | PVC Storage Request | `10Gi` | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | List of node taints to tolerate | `[]` | +| `affinity` | Map of node/pod affinities | `{}` | + +The above parameters map to the env variables defined in [bitnami/wordpress](http://github.com/bitnami/bitnami-docker-wordpress). For more information please refer to the [bitnami/wordpress](http://github.com/bitnami/bitnami-docker-wordpress) image documentation. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```console +$ helm install --name my-release \ + --set wordpressUsername=admin,wordpressPassword=password,mariadb.mariadbRootPassword=secretpassword \ + stable/wordpress +``` + +The above command sets the WordPress administrator account username and password to `admin` and `password` respectively. Additionally, it sets the MariaDB `root` user password to `secretpassword`. + +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, + +```console +$ helm install --name my-release -f values.yaml stable/wordpress +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Production and horizontal scaling + +The following repo contains the recommended production settings for wordpress capture in an alternative [values file](values-production.yaml). Please read carefully the comments in the values-production.yaml file to set up your environment appropriately. + +To horizontally scale this chart, first download the [values-production.yaml](values-production.yaml) file to your local folder, then: + +```console +$ helm install --name my-release -f ./values-production.yaml stable/wordpress +``` + +Note that [values-production.yaml](values-production.yaml) includes a replicaCount of 3, so there will be 3 WordPress pods. As a result, to use the /admin portal and to ensure you can scale wordpress you need to provide a ReadWriteMany PVC, if you don't have a provisioner for this type of storage, we recommend that you install the nfs provisioner and map it to a RWO volume. + +```console +$ helm install stable/nfs-server-provisioner --set persistence.enabled=true,persistence.size=10Gi +$ helm install --name my-release -f values-production.yaml --set persistence.storageClass=nfs stable/wordpress +``` + +## Persistence + +The [Bitnami WordPress](https://github.com/bitnami/bitnami-docker-wordpress) image stores the WordPress data and configurations at the `/bitnami` path of the container. + +Persistent Volume Claims are used to keep the data across deployments. This is known to work in GCE, AWS, and minikube. +See the [Configuration](#configuration) section to configure the PVC or to disable persistence. + +## Using an external database + +Sometimes you may want to have Wordpress connect to an external database rather than installing one inside your cluster, e.g. to use a managed database service, or use run a single database server for all your applications. To do this, the chart allows you to specify credentials for an external database under the [`externalDatabase` parameter](#configuration). You should also disable the MariaDB installation with the `mariadb.enabled` option. For example: + +```console +$ helm install stable/wordpress \ + --set mariadb.enabled=false,externalDatabase.host=myexternalhost,externalDatabase.user=myuser,externalDatabase.password=mypassword,externalDatabase.database=mydatabase,externalDatabase.port=3306 +``` + +Note also if you disable MariaDB per above you MUST supply values for the `externalDatabase` connection. + +## Ingress + +This chart provides support for ingress resources. If you have an +ingress controller installed on your cluster, such as [nginx-ingress](https://kubeapps.com/charts/stable/nginx-ingress) +or [traefik](https://kubeapps.com/charts/stable/traefik) you can utilize +the ingress controller to serve your WordPress application. + +To enable ingress integration, please set `ingress.enabled` to `true` + +### Hosts + +Most likely you will only want to have one hostname that maps to this +WordPress installation, however, it is possible to have more than one +host. To facilitate this, the `ingress.hosts` object is an array. + +For each item, please indicate a `name`, `tls`, `tlsSecret`, and any +`annotations` that you may want the ingress controller to know about. + +Indicating TLS will cause WordPress to generate HTTPS URLs, and +WordPress will be connected to at port 443. The actual secret that +`tlsSecret` references do not have to be generated by this chart. +However, please note that if TLS is enabled, the ingress record will not +work until this secret exists. + +For annotations, please see [this document](https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md). +Not all annotations are supported by all ingress controllers, but this +document does a good job of indicating which annotation is supported by +many popular ingress controllers. + +### TLS Secrets + +This chart will facilitate the creation of TLS secrets for use with the +ingress controller, however, this is not required. There are three +common use cases: + +* helm generates/manages certificate secrets +* user generates/manages certificates separately +* an additional tool (like [kube-lego](https://kubeapps.com/charts/stable/kube-lego)) +manages the secrets for the application + +In the first two cases, one will need a certificate and a key. We would +expect them to look like this: + +* certificate files should look like (and there can be more than one +certificate if there is a certificate chain) + +``` +-----BEGIN CERTIFICATE----- +MIID6TCCAtGgAwIBAgIJAIaCwivkeB5EMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +... +jScrvkiBO65F46KioCL9h5tDvomdU1aqpI/CBzhvZn1c0ZTf87tGQR8NK7v7 +-----END CERTIFICATE----- +``` +* keys should look like: +``` +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvLYcyu8f3skuRyUgeeNpeDvYBCDcgq+LsWap6zbX5f8oLqp4 +... +wrj2wDbCDCFmfqnSJ+dKI3vFLlEz44sAV8jX/kd4Y6ZTQhlLbYc= +-----END RSA PRIVATE KEY----- +```` + +If you are going to use Helm to manage the certificates, please copy +these values into the `certificate` and `key` values for a given +`ingress.secrets` entry. + +If you are going are going to manage TLS secrets outside of Helm, please +know that you can create a TLS secret by doing the following: + +``` +kubectl create secret tls wordpress.local-tls --key /path/to/key.key --cert /path/to/cert.crt +``` + +Please see [this example](https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx/examples/tls) +for more information. diff --git a/pkg/action/testdata/charts/chart-missing-deps/requirements.lock b/pkg/action/testdata/charts/chart-missing-deps/requirements.lock new file mode 100755 index 000000000..cb3439862 --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://kubernetes-charts.storage.googleapis.com/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml b/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml new file mode 100755 index 000000000..a894b8b3b --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://kubernetes-charts.storage.googleapis.com/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-missing-deps/templates/NOTES.txt b/pkg/action/testdata/charts/chart-missing-deps/templates/NOTES.txt new file mode 100755 index 000000000..55626e4d1 --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/templates/NOTES.txt @@ -0,0 +1,38 @@ +1. Get the WordPress URL: + +{{- if .Values.ingress.enabled }} + + You should be able to access your new WordPress installation through + + {{- range .Values.ingress.hosts }} + {{ if .tls }}https{{ else }}http{{ end }}://{{ .name }}/admin + {{- end }} + +{{- else if contains "LoadBalancer" .Values.serviceType }} + + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + Watch the status with: 'kubectl get svc --namespace {{ .Release.Namespace }} -w {{ template "fullname" . }}' + + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo "WordPress URL: http://$SERVICE_IP/" + echo "WordPress Admin URL: http://$SERVICE_IP/admin" + +{{- else if contains "ClusterIP" .Values.serviceType }} + + echo "WordPress URL: http://127.0.0.1:8080/" + echo "WordPress Admin URL: http://127.0.0.1:8080/admin" + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ template "fullname" . }} 8080:80 + +{{- else if contains "NodePort" .Values.serviceType }} + + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "WordPress URL: http://$NODE_IP:$NODE_PORT/" + echo "WordPress Admin URL: http://$NODE_IP:$NODE_PORT/admin" + +{{- end }} + +2. Login with the following credentials to see your blog + + echo Username: {{ .Values.wordpressUsername }} + echo Password: $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath="{.data.wordpress-password}" | base64 --decode) diff --git a/pkg/action/testdata/charts/chart-missing-deps/templates/_helpers.tpl b/pkg/action/testdata/charts/chart-missing-deps/templates/_helpers.tpl new file mode 100755 index 000000000..1e52d321c --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/templates/_helpers.tpl @@ -0,0 +1,24 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "mariadb.fullname" -}} +{{- printf "%s-%s" .Release.Name "mariadb" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/pkg/action/testdata/charts/chart-missing-deps/values.yaml b/pkg/action/testdata/charts/chart-missing-deps/values.yaml new file mode 100755 index 000000000..3cb66dafd --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/values.yaml @@ -0,0 +1,254 @@ +## Bitnami WordPress image version +## ref: https://hub.docker.com/r/bitnami/wordpress/tags/ +## +image: + registry: docker.io + repository: bitnami/wordpress + tag: 4.9.8-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +## User of the application +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressUsername: user + +## Application password +## Defaults to a random 10-character alphanumeric string if not set +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +# wordpressPassword: + +## Admin email +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressEmail: user@example.com + +## First name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressFirstName: FirstName + +## Last name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressLastName: LastName + +## Blog name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressBlogName: User's Blog! + +## Table prefix +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressTablePrefix: wp_ + +## Set to `yes` to allow the container to be started with blank passwords +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +allowEmptyPassword: yes + +## SMTP mail delivery configuration +## ref: https://github.com/bitnami/bitnami-docker-wordpress/#smtp-configuration +## +# smtpHost: +# smtpPort: +# smtpUser: +# smtpPassword: +# smtpUsername: +# smtpProtocol: + +replicaCount: 1 + +externalDatabase: +## All of these values are only used when mariadb.enabled is set to false + ## Database host + host: localhost + + ## non-root Username for Wordpress Database + user: bn_wordpress + + ## Database password + password: "" + + ## Database name + database: bitnami_wordpress + + ## Database port number + port: 3306 + +## +## MariaDB chart configuration +## +mariadb: + ## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters + enabled: true + ## Disable MariaDB replication + replication: + enabled: false + + ## Create a database and a database user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run + ## + db: + name: bitnami_wordpress + user: bn_wordpress + ## If the password is not specified, mariadb will generates a random password + ## + # password: + + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run + ## + # rootUser: + # password: + + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + master: + persistence: + enabled: true + ## mariadb data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 8Gi + +## Kubernetes configuration +## For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP +## +serviceType: LoadBalancer +## +## serviceType: NodePort +## nodePorts: +## http: +## https: +nodePorts: + http: "" + https: "" +## Enable client source IP preservation +## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip +## +serviceExternalTrafficPolicy: Cluster + +## Allow health checks to be pointed at the https port +healthcheckHttps: false + +## Configure extra options for liveness and readiness probes +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) +livenessProbe: + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 +readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +## Configure the ingress resource that allows you to access the +## Wordpress installation. Set up the URL +## ref: http://kubernetes.io/docs/user-guide/ingress/ +## +ingress: + ## Set to true to enable ingress record generation + enabled: false + + ## The list of hostnames to be covered with this ingress record. + ## Most likely this will be just one host, but in the event more hosts are needed, this is an array + hosts: + - name: wordpress.local + + ## Set this to true in order to enable TLS on the ingress record + ## A side effect of this will be that the backend wordpress service will be connected at port 443 + tls: false + + ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS + tlsSecret: wordpress.local-tls + + ## Ingress annotations done as key:value pairs + ## If you're using kube-lego, you will want to add: + ## kubernetes.io/tls-acme: true + ## + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: true + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: wordpress.local-tls + # key: + # certificate: + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## wordpress data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 10Gi + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + requests: + memory: 512Mi + cpu: 300m + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz b/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz new file mode 100644 index 000000000..7a22b1d82 Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/.helmignore b/pkg/action/testdata/charts/chart-with-compressed-dependencies/.helmignore new file mode 100755 index 000000000..e2cf7941f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/.helmignore @@ -0,0 +1,5 @@ +.git +# OWNERS file for Kubernetes +OWNERS +# example production yaml +values-production.yaml \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml b/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml new file mode 100755 index 000000000..602644caa --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml @@ -0,0 +1,20 @@ +appVersion: 4.9.8 +description: Web publishing platform for building blogs and websites. +engine: gotpl +home: http://www.wordpress.com/ +icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png +keywords: +- wordpress +- cms +- blog +- http +- web +- application +- php +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: chart-with-compressed-dependencies +sources: +- https://github.com/bitnami/bitnami-docker-wordpress +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/README.md b/pkg/action/testdata/charts/chart-with-compressed-dependencies/README.md new file mode 100755 index 000000000..3174417e0 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/README.md @@ -0,0 +1,3 @@ +# WordPress + +This is a testing fork of the Wordpress chart. It is not operational. diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz b/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz new file mode 100644 index 000000000..5b38fa1c3 Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock new file mode 100755 index 000000000..cb3439862 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://kubernetes-charts.storage.googleapis.com/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml new file mode 100755 index 000000000..a894b8b3b --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://kubernetes-charts.storage.googleapis.com/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/templates/NOTES.txt b/pkg/action/testdata/charts/chart-with-compressed-dependencies/templates/NOTES.txt new file mode 100755 index 000000000..3b94f9157 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/templates/NOTES.txt @@ -0,0 +1 @@ +Placeholder diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/values.yaml b/pkg/action/testdata/charts/chart-with-compressed-dependencies/values.yaml new file mode 100755 index 000000000..3cb66dafd --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/values.yaml @@ -0,0 +1,254 @@ +## Bitnami WordPress image version +## ref: https://hub.docker.com/r/bitnami/wordpress/tags/ +## +image: + registry: docker.io + repository: bitnami/wordpress + tag: 4.9.8-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +## User of the application +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressUsername: user + +## Application password +## Defaults to a random 10-character alphanumeric string if not set +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +# wordpressPassword: + +## Admin email +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressEmail: user@example.com + +## First name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressFirstName: FirstName + +## Last name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressLastName: LastName + +## Blog name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressBlogName: User's Blog! + +## Table prefix +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressTablePrefix: wp_ + +## Set to `yes` to allow the container to be started with blank passwords +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +allowEmptyPassword: yes + +## SMTP mail delivery configuration +## ref: https://github.com/bitnami/bitnami-docker-wordpress/#smtp-configuration +## +# smtpHost: +# smtpPort: +# smtpUser: +# smtpPassword: +# smtpUsername: +# smtpProtocol: + +replicaCount: 1 + +externalDatabase: +## All of these values are only used when mariadb.enabled is set to false + ## Database host + host: localhost + + ## non-root Username for Wordpress Database + user: bn_wordpress + + ## Database password + password: "" + + ## Database name + database: bitnami_wordpress + + ## Database port number + port: 3306 + +## +## MariaDB chart configuration +## +mariadb: + ## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters + enabled: true + ## Disable MariaDB replication + replication: + enabled: false + + ## Create a database and a database user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run + ## + db: + name: bitnami_wordpress + user: bn_wordpress + ## If the password is not specified, mariadb will generates a random password + ## + # password: + + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run + ## + # rootUser: + # password: + + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + master: + persistence: + enabled: true + ## mariadb data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 8Gi + +## Kubernetes configuration +## For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP +## +serviceType: LoadBalancer +## +## serviceType: NodePort +## nodePorts: +## http: +## https: +nodePorts: + http: "" + https: "" +## Enable client source IP preservation +## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip +## +serviceExternalTrafficPolicy: Cluster + +## Allow health checks to be pointed at the https port +healthcheckHttps: false + +## Configure extra options for liveness and readiness probes +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) +livenessProbe: + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 +readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +## Configure the ingress resource that allows you to access the +## Wordpress installation. Set up the URL +## ref: http://kubernetes.io/docs/user-guide/ingress/ +## +ingress: + ## Set to true to enable ingress record generation + enabled: false + + ## The list of hostnames to be covered with this ingress record. + ## Most likely this will be just one host, but in the event more hosts are needed, this is an array + hosts: + - name: wordpress.local + + ## Set this to true in order to enable TLS on the ingress record + ## A side effect of this will be that the backend wordpress service will be connected at port 443 + tls: false + + ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS + tlsSecret: wordpress.local-tls + + ## Ingress annotations done as key:value pairs + ## If you're using kube-lego, you will want to add: + ## kubernetes.io/tls-acme: true + ## + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: true + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: wordpress.local-tls + # key: + # certificate: + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## wordpress data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 10Gi + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + requests: + memory: 512Mi + cpu: 300m + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz new file mode 100644 index 000000000..ad9e68179 Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore new file mode 100755 index 000000000..e2cf7941f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore @@ -0,0 +1,5 @@ +.git +# OWNERS file for Kubernetes +OWNERS +# example production yaml +values-production.yaml \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml new file mode 100755 index 000000000..4d8569c89 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml @@ -0,0 +1,20 @@ +appVersion: 4.9.8 +description: Web publishing platform for building blogs and websites. +engine: gotpl +home: http://www.wordpress.com/ +icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png +keywords: +- wordpress +- cms +- blog +- http +- web +- application +- php +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: chart-with-uncompressed-dependencies +sources: +- https://github.com/bitnami/bitnami-docker-wordpress +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md new file mode 100755 index 000000000..341a1ad93 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md @@ -0,0 +1,3 @@ +# WordPress + +This is a testing mock, and is not operational. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore new file mode 100755 index 000000000..6b8710a71 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore @@ -0,0 +1 @@ +.git diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml new file mode 100755 index 000000000..cefc15836 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml @@ -0,0 +1,21 @@ +appVersion: 10.1.34 +description: Fast, reliable, scalable, and easy to use open-source relational database + system. MariaDB Server is intended for mission-critical, heavy-load production systems + as well as for embedding into mass-deployed software. Highly available MariaDB cluster. +engine: gotpl +home: https://mariadb.org +icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png +keywords: +- mariadb +- mysql +- database +- sql +- prometheus +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: mariadb +sources: +- https://github.com/bitnami/bitnami-docker-mariadb +- https://github.com/prometheus/mysqld_exporter +version: 4.3.1 diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md new file mode 100755 index 000000000..3463b8b6d --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md @@ -0,0 +1,143 @@ +# MariaDB + +[MariaDB](https://mariadb.org) is one of the most popular database servers in the world. It’s made by the original developers of MySQL and guaranteed to stay open source. Notable users include Wikipedia, Facebook and Google. + +MariaDB is developed as open source software and as a relational database it provides an SQL interface for accessing data. The latest versions of MariaDB also include GIS and JSON features. + +## TL;DR + +```bash +$ helm install stable/mariadb +``` + +## Introduction + +This chart bootstraps a [MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) replication cluster deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +## Prerequisites + +- Kubernetes 1.4+ with Beta APIs enabled +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install --name my-release stable/mariadb +``` + +The command deploys MariaDB on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following table lists the configurable parameters of the MariaDB chart and their default values. + +| Parameter | Description | Default | +|-------------------------------------------|-----------------------------------------------------|-------------------------------------------------------------------| +| `image.registry` | MariaDB image registry | `docker.io` | +| `image.repository` | MariaDB Image name | `bitnami/mariadb` | +| `image.tag` | MariaDB Image tag | `{VERSION}` | +| `image.pullPolicy` | MariaDB image pull policy | `Always` if `imageTag` is `latest`, else `IfNotPresent` | +| `image.pullSecrets` | Specify image pull secrets | `nil` (does not add image pull secrets to deployed pods) | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | MySQL service port | `3306` | +| `rootUser.password` | Password for the `root` user | _random 10 character alphanumeric string_ | +| `rootUser.forcePassword` | Force users to specify a password | `false` | +| `db.user` | Username of new user to create | `nil` | +| `db.password` | Password for the new user | _random 10 character alphanumeric string if `db.user` is defined_ | +| `db.name` | Name for new database to create | `my_database` | +| `replication.enabled` | MariaDB replication enabled | `true` | +| `replication.user` | MariaDB replication user | `replicator` | +| `replication.password` | MariaDB replication user password | _random 10 character alphanumeric string_ | +| `master.antiAffinity` | Master pod anti-affinity policy | `soft` | +| `master.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` | +| `master.persistence.annotations` | Persistent Volume Claim annotations | `{}` | +| `master.persistence.storageClass` | Persistent Volume Storage Class | `` | +| `master.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` | +| `master.persistence.size` | Persistent Volume Size | `8Gi` | +| `master.config` | Config file for the MariaDB Master server | `_default values in the values.yaml file_` | +| `master.resources` | CPU/Memory resource requests/limits for master node | `{}` | +| `master.livenessProbe.enabled` | Turn on and off liveness probe (master) | `true` | +| `master.livenessProbe.initialDelaySeconds`| Delay before liveness probe is initiated (master) | `120` | +| `master.livenessProbe.periodSeconds` | How often to perform the probe (master) | `10` | +| `master.livenessProbe.timeoutSeconds` | When the probe times out (master) | `1` | +| `master.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` | +| `master.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` | +| `master.readinessProbe.enabled` | Turn on and off readiness probe (master) | `true` | +| `master.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (master) | `15` | +| `master.readinessProbe.periodSeconds` | How often to perform the probe (master) | `10` | +| `master.readinessProbe.timeoutSeconds` | When the probe times out (master) | `1` | +| `master.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` | +| `master.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` | +| `slave.replicas` | Desired number of slave replicas | `1` | +| `slave.antiAffinity` | Slave pod anti-affinity policy | `soft` | +| `slave.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` | +| `slave.persistence.annotations` | Persistent Volume Claim annotations | `{}` | +| `slave.persistence.storageClass` | Persistent Volume Storage Class | `` | +| `slave.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` | +| `slave.persistence.size` | Persistent Volume Size | `8Gi` | +| `slave.config` | Config file for the MariaDB Slave replicas | `_default values in the values.yaml file_` | +| `slave.resources` | CPU/Memory resource requests/limits for slave node | `{}` | +| `slave.livenessProbe.enabled` | Turn on and off liveness probe (slave) | `true` | +| `slave.livenessProbe.initialDelaySeconds` | Delay before liveness probe is initiated (slave) | `120` | +| `slave.livenessProbe.periodSeconds` | How often to perform the probe (slave) | `10` | +| `slave.livenessProbe.timeoutSeconds` | When the probe times out (slave) | `1` | +| `slave.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` | +| `slave.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` | +| `slave.readinessProbe.enabled` | Turn on and off readiness probe (slave) | `true` | +| `slave.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (slave) | `15` | +| `slave.readinessProbe.periodSeconds` | How often to perform the probe (slave) | `10` | +| `slave.readinessProbe.timeoutSeconds` | When the probe times out (slave) | `1` | +| `slave.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` | +| `slave.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` | +| `metrics.enabled` | Start a side-car prometheus exporter | `false` | +| `metrics.image.registry` | Exporter image registry | `docker.io` | +`metrics.image.repository` | Exporter image name | `prom/mysqld-exporter` | +| `metrics.image.tag` | Exporter image tag | `v0.10.0` | +| `metrics.image.pullPolicy` | Exporter image pull policy | `IfNotPresent` | +| `metrics.resources` | Exporter resource requests/limit | `nil` | + +The above parameters map to the env variables defined in [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb). For more information please refer to the [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb) image documentation. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +$ helm install --name my-release \ + --set root.password=secretpassword,user.database=app_database \ + stable/mariadb +``` + +The above command sets the MariaDB `root` account password to `secretpassword`. Additionally it creates a database named `my_database`. + +Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, + +```bash +$ helm install --name my-release -f values.yaml stable/mariadb +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Initialize a fresh instance + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image allows you to use your custom scripts to initialize a fresh instance. In order to execute the scripts, they must be located inside the chart folder `files/docker-entrypoint-initdb.d` so they can be consumed as a ConfigMap. + +The allowed extensions are `.sh`, `.sql` and `.sql.gz`. + +## Persistence + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image stores the MariaDB data and configurations at the `/bitnami/mariadb` path of the container. + +The chart mounts a [Persistent Volume](kubernetes.io/docs/user-guide/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning, by default. An existing PersistentVolumeClaim can be defined. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md new file mode 100755 index 000000000..aaddde303 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md @@ -0,0 +1,3 @@ +You can copy here your custom .sh, .sql or .sql.gz file so they are executed during the first boot of the image. + +More info in the [bitnami-docker-mariadb](https://github.com/bitnami/bitnami-docker-mariadb#initializing-a-new-instance) repository. \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt new file mode 100755 index 000000000..4ba3b668a --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt @@ -0,0 +1,35 @@ + +Please be patient while the chart is being deployed + +Tip: + + Watch the deployment status using the command: kubectl get pods -w --namespace {{ .Release.Namespace }} -l release={{ .Release.Name }} + +Services: + + echo Master: {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- if .Values.replication.enabled }} + echo Slave: {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- end }} + +Administrator credentials: + + Username: root + Password : $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "mariadb.fullname" . }} -o jsonpath="{.data.mariadb-root-password}" | base64 --decode) + +To connect to your database + + 1. Run a pod that you can use as a client: + + kubectl run {{ template "mariadb.fullname" . }}-client --rm --tty -i --image {{ template "mariadb.image" . }} --namespace {{ .Release.Namespace }} --command -- bash + + 2. To connect to master service (read/write): + + mysql -h {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }} + +{{- if .Values.replication.enabled }} + + 3. To connect to slave service (read-only): + + mysql -h {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl new file mode 100755 index 000000000..5afe380ff --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl @@ -0,0 +1,53 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "mariadb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "mariadb.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "master.fullname" -}} +{{- if .Values.replication.enabled -}} +{{- printf "%s-%s" .Release.Name "mariadb-master" | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name "mariadb" | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + + +{{- define "slave.fullname" -}} +{{- printf "%s-%s" .Release.Name "mariadb-slave" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "mariadb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Return the proper image name +*/}} +{{- define "mariadb.image" -}} +{{- $registryName := .Values.image.registry -}} +{{- $repositoryName := .Values.image.repository -}} +{{- $tag := .Values.image.tag | toString -}} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- end -}} + +{{/* +Return the proper image name +*/}} +{{- define "metrics.image" -}} +{{- $registryName := .Values.metrics.image.registry -}} +{{- $repositoryName := .Values.metrics.image.repository -}} +{{- $tag := .Values.metrics.image.tag | toString -}} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- end -}} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml new file mode 100755 index 000000000..7bb969627 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "master.fullname" . }}-init-scripts + labels: + app: {{ template "mariadb.name" . }} + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: +{{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml new file mode 100755 index 000000000..880a10198 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml @@ -0,0 +1,15 @@ +{{- if .Values.master.config }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "master.fullname" . }} + labels: + app: {{ template "mariadb.name" . }} + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: + my.cnf: |- +{{ .Values.master.config | indent 4 }} +{{- end -}} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml new file mode 100755 index 000000000..0d74f01ff --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml @@ -0,0 +1,187 @@ +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: {{ template "master.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "master" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + serviceName: "{{ template "master.fullname" . }}" + replicas: 1 + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: "{{ template "mariadb.name" . }}" + component: "master" + release: "{{ .Release.Name }}" + chart: {{ template "mariadb.chart" . }} + spec: + securityContext: + runAsUser: 1001 + fsGroup: 1001 + {{- if eq .Values.master.antiAffinity "hard" }} + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- else if eq .Values.master.antiAffinity "soft" }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range .Values.image.pullSecrets }} + - name: {{ . }} + {{- end}} + {{- end }} + containers: + - name: "mariadb" + image: {{ template "mariadb.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + {{- if .Values.db.user }} + - name: MARIADB_USER + value: "{{ .Values.db.user }}" + - name: MARIADB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-password + {{- end }} + - name: MARIADB_DATABASE + value: "{{ .Values.db.name }}" + {{- if .Values.replication.enabled }} + - name: MARIADB_REPLICATION_MODE + value: "master" + - name: MARIADB_REPLICATION_USER + value: "{{ .Values.replication.user }}" + - name: MARIADB_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-replication-password + {{- end }} + ports: + - name: mysql + containerPort: 3306 + {{- if .Values.master.livenessProbe.enabled }} + livenessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.master.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.master.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.master.livenessProbe.timeoutSeconds }} + successThreshold: {{ .Values.master.livenessProbe.successThreshold }} + failureThreshold: {{ .Values.master.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.master.readinessProbe.enabled }} + readinessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.master.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.master.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.master.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.master.readinessProbe.successThreshold }} + failureThreshold: {{ .Values.master.readinessProbe.failureThreshold }} + {{- end }} + resources: +{{ toYaml .Values.master.resources | indent 10 }} + volumeMounts: + - name: data + mountPath: /bitnami/mariadb + - name: custom-init-scripts + mountPath: /docker-entrypoint-initdb.d +{{- if .Values.master.config }} + - name: config + mountPath: /opt/bitnami/mariadb/conf/my.cnf + subPath: my.cnf +{{- end }} +{{- if .Values.metrics.enabled }} + - name: metrics + image: {{ template "metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ] + ports: + - name: metrics + containerPort: 9104 + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 15 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: +{{ toYaml .Values.metrics.resources | indent 10 }} +{{- end }} + volumes: + {{- if .Values.master.config }} + - name: config + configMap: + name: {{ template "master.fullname" . }} + {{- end }} + - name: custom-init-scripts + configMap: + name: {{ template "master.fullname" . }}-init-scripts +{{- if .Values.master.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "master" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + accessModes: + {{- range .Values.master.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.master.persistence.size | quote }} + {{- if .Values.master.persistence.storageClass }} + {{- if (eq "-" .Values.master.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.master.persistence.storageClass | quote }} + {{- end }} + {{- end }} +{{- else }} + - name: "data" + emptyDir: {} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml new file mode 100755 index 000000000..460ec328e --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "mariadb.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +{{- if .Values.metrics.enabled }} + annotations: +{{ toYaml .Values.metrics.annotations | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: mysql + port: {{ .Values.service.port }} + targetPort: mysql +{{- if .Values.metrics.enabled }} + - name: metrics + port: 9104 + targetPort: metrics +{{- end }} + selector: + app: "{{ template "mariadb.name" . }}" + component: "master" + release: "{{ .Release.Name }}" diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml new file mode 100755 index 000000000..17999d609 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml @@ -0,0 +1,38 @@ +{{- if (not .Values.rootUser.existingSecret) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "mariadb.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +type: Opaque +data: + {{- if .Values.rootUser.password }} + mariadb-root-password: "{{ .Values.rootUser.password | b64enc }}" + {{- else if (not .Values.rootUser.forcePassword) }} + mariadb-root-password: "{{ randAlphaNum 10 | b64enc }}" + {{ else }} + mariadb-root-password: {{ required "A MariaDB Root Password is required!" .Values.rootUser.password }} + {{- end }} + {{- if .Values.db.user }} + {{- if .Values.db.password }} + mariadb-password: "{{ .Values.db.password | b64enc }}" + {{- else if (not .Values.db.forcePassword) }} + mariadb-password: "{{ randAlphaNum 10 | b64enc }}" + {{- else }} + mariadb-password: {{ required "A MariaDB Database Password is required!" .Values.db.password }} + {{- end }} + {{- end }} + {{- if .Values.replication.enabled }} + {{- if .Values.replication.password }} + mariadb-replication-password: "{{ .Values.replication.password | b64enc }}" + {{- else if (not .Values.replication.forcePassword) }} + mariadb-replication-password: "{{ randAlphaNum 10 | b64enc }}" + {{- else }} + mariadb-replication-password: {{ required "A MariaDB Replication Password is required!" .Values.replication.password }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml new file mode 100755 index 000000000..056cf5c07 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.replication.enabled .Values.slave.config }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: {{ template "mariadb.name" . }} + component: "slave" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: + my.cnf: |- +{{ .Values.slave.config | indent 4 }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml new file mode 100755 index 000000000..aa67d4a70 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml @@ -0,0 +1,193 @@ +{{- if .Values.replication.enabled }} +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + serviceName: "{{ template "slave.fullname" . }}" + replicas: {{ .Values.slave.replicas }} + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: "{{ template "mariadb.name" . }}" + component: "slave" + release: "{{ .Release.Name }}" + chart: {{ template "mariadb.chart" . }} + spec: + securityContext: + runAsUser: 1001 + fsGroup: 1001 + {{- if eq .Values.slave.antiAffinity "hard" }} + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- else if eq .Values.slave.antiAffinity "soft" }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range .Values.image.pullSecrets }} + - name: {{ . }} + {{- end}} + {{- end }} + containers: + - name: "mariadb" + image: {{ template "mariadb.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + {{- if .Values.db.user }} + - name: MARIADB_USER + value: "{{ .Values.db.user }}" + - name: MARIADB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-password + {{- end }} + - name: MARIADB_DATABASE + value: "{{ .Values.db.name }}" + - name: MARIADB_REPLICATION_MODE + value: "slave" + - name: MARIADB_MASTER_HOST + value: {{ template "mariadb.fullname" . }} + - name: MARIADB_MASTER_PORT + value: "3306" + - name: MARIADB_MASTER_USER + value: "root" + - name: MARIADB_MASTER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + - name: MARIADB_REPLICATION_USER + value: "{{ .Values.replication.user }}" + - name: MARIADB_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-replication-password + ports: + - name: mysql + containerPort: 3306 + {{- if .Values.slave.livenessProbe.enabled }} + livenessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.slave.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.slave.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.slave.livenessProbe.timeoutSeconds }} + successThreshold: {{ .Values.slave.livenessProbe.successThreshold }} + failureThreshold: {{ .Values.slave.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.slave.readinessProbe.enabled }} + readinessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.slave.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.slave.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.slave.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.slave.readinessProbe.successThreshold }} + failureThreshold: {{ .Values.slave.readinessProbe.failureThreshold }} + {{- end }} + resources: +{{ toYaml .Values.slave.resources | indent 10 }} + volumeMounts: + - name: data + mountPath: /bitnami/mariadb +{{- if .Values.slave.config }} + - name: config + mountPath: /opt/bitnami/mariadb/conf/my.cnf + subPath: my.cnf +{{- end }} +{{- if .Values.metrics.enabled }} + - name: metrics + image: {{ template "metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ] + ports: + - name: metrics + containerPort: 9104 + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 15 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: +{{ toYaml .Values.metrics.resources | indent 10 }} +{{- end }} + volumes: + {{- if .Values.slave.config }} + - name: config + configMap: + name: {{ template "slave.fullname" . }} + {{- end }} +{{- if .Values.slave.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + accessModes: + {{- range .Values.slave.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.slave.persistence.size | quote }} + {{- if .Values.slave.persistence.storageClass }} + {{- if (eq "-" .Values.slave.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.slave.persistence.storageClass | quote }} + {{- end }} + {{- end }} +{{- else }} + - name: "data" + emptyDir: {} +{{- end }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml new file mode 100755 index 000000000..fa551371f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml @@ -0,0 +1,31 @@ +{{- if .Values.replication.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +{{- if .Values.metrics.enabled }} + annotations: +{{ toYaml .Values.metrics.annotations | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: mysql + port: {{ .Values.service.port }} + targetPort: mysql +{{- if .Values.metrics.enabled }} + - name: metrics + port: 9104 + targetPort: metrics +{{- end }} + selector: + app: "{{ template "mariadb.name" . }}" + component: "slave" + release: "{{ .Release.Name }}" +{{- end }} \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml new file mode 100755 index 000000000..99a85d4aa --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ template "mariadb.fullname" . }}-test-{{ randAlphaNum 5 | lower }}" + annotations: + "helm.sh/hook": test-success +spec: + initContainers: + - name: "test-framework" + image: "dduportal/bats:0.4.0" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: mariadb-test + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: {{ template "mariadb.fullname" . }}-tests + - name: tools + emptyDir: {} + restartPolicy: Never diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml new file mode 100755 index 000000000..957f3fd1e --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "mariadb.fullname" . }}-tests +data: + run.sh: |- + @test "Testing MariaDB is accessible" { + mysql -h {{ template "mariadb.fullname" . }} -uroot -p$MARIADB_ROOT_PASSWORD -e 'show databases;' + } diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml new file mode 100755 index 000000000..ce2414e9f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml @@ -0,0 +1,233 @@ +## Bitnami MariaDB image +## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ +## +image: + registry: docker.io + repository: bitnami/mariadb + tag: 10.1.34-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +service: + ## Kubernetes service type + type: ClusterIP + port: 3306 + +rootUser: + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-the-root-password-on-first-run + ## + password: + ## Use existing secret (ignores root, db and replication passwords) + # existingSecret: + ## + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +db: + ## MariaDB username and password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-user-on-first-run + ## + user: + password: + ## Password is ignored if existingSecret is specified. + ## Database to create + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-on-first-run + ## + name: my_database + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +replication: + ## Enable replication. This enables the creation of replicas of MariaDB. If false, only a + ## master deployment would be created + enabled: true + ## + ## MariaDB replication user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster + ## + user: replicator + ## MariaDB replication user password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster + ## + password: + ## Password is ignored if existingSecret is specified. + ## + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +master: + antiAffinity: soft + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + persistence: + ## If true, use a Persistent Volume Claim, If false, use emptyDir + ## + enabled: true + ## Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## Persistent Volume Claim annotations + ## + annotations: + ## Persistent Volume Access Mode + ## + accessModes: + - ReadWriteOnce + ## Persistent Volume size + ## + size: 8Gi + ## + + ## Configure MySQL with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + config: |- + [mysqld] + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mariadb + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + tmpdir=/opt/bitnami/mariadb/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + log-error=/opt/bitnami/mariadb/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + default-character-set=UTF8 + + [manager] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + + ## Configure master resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: {} + livenessProbe: + enabled: true + ## + ## Initializing the database could take some time + initialDelaySeconds: 120 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 15 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + +slave: + replicas: 1 + antiAffinity: soft + persistence: + ## If true, use a Persistent Volume Claim, If false, use emptyDir + ## + enabled: true + # storageClass: "-" + annotations: + accessModes: + - ReadWriteOnce + ## Persistent Volume size + ## + size: 8Gi + ## + + ## Configure MySQL slave with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + config: |- + [mysqld] + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mariadb + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + tmpdir=/opt/bitnami/mariadb/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + log-error=/opt/bitnami/mariadb/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + default-character-set=UTF8 + + [manager] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + + ## + ## Configure slave resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: {} + livenessProbe: + enabled: true + ## + ## Initializing the database could take some time + initialDelaySeconds: 120 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 15 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + +metrics: + enabled: false + image: + registry: docker.io + repository: prom/mysqld-exporter + tag: v0.10.0 + pullPolicy: IfNotPresent + resources: {} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9104" diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock new file mode 100755 index 000000000..cb3439862 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://kubernetes-charts.storage.googleapis.com/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml new file mode 100755 index 000000000..a894b8b3b --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://kubernetes-charts.storage.googleapis.com/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt new file mode 100755 index 000000000..75ed9b64f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt @@ -0,0 +1 @@ +Placeholder. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml new file mode 100755 index 000000000..3cb66dafd --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml @@ -0,0 +1,254 @@ +## Bitnami WordPress image version +## ref: https://hub.docker.com/r/bitnami/wordpress/tags/ +## +image: + registry: docker.io + repository: bitnami/wordpress + tag: 4.9.8-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +## User of the application +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressUsername: user + +## Application password +## Defaults to a random 10-character alphanumeric string if not set +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +# wordpressPassword: + +## Admin email +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressEmail: user@example.com + +## First name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressFirstName: FirstName + +## Last name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressLastName: LastName + +## Blog name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressBlogName: User's Blog! + +## Table prefix +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressTablePrefix: wp_ + +## Set to `yes` to allow the container to be started with blank passwords +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +allowEmptyPassword: yes + +## SMTP mail delivery configuration +## ref: https://github.com/bitnami/bitnami-docker-wordpress/#smtp-configuration +## +# smtpHost: +# smtpPort: +# smtpUser: +# smtpPassword: +# smtpUsername: +# smtpProtocol: + +replicaCount: 1 + +externalDatabase: +## All of these values are only used when mariadb.enabled is set to false + ## Database host + host: localhost + + ## non-root Username for Wordpress Database + user: bn_wordpress + + ## Database password + password: "" + + ## Database name + database: bitnami_wordpress + + ## Database port number + port: 3306 + +## +## MariaDB chart configuration +## +mariadb: + ## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters + enabled: true + ## Disable MariaDB replication + replication: + enabled: false + + ## Create a database and a database user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run + ## + db: + name: bitnami_wordpress + user: bn_wordpress + ## If the password is not specified, mariadb will generates a random password + ## + # password: + + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run + ## + # rootUser: + # password: + + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + master: + persistence: + enabled: true + ## mariadb data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 8Gi + +## Kubernetes configuration +## For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP +## +serviceType: LoadBalancer +## +## serviceType: NodePort +## nodePorts: +## http: +## https: +nodePorts: + http: "" + https: "" +## Enable client source IP preservation +## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip +## +serviceExternalTrafficPolicy: Cluster + +## Allow health checks to be pointed at the https port +healthcheckHttps: false + +## Configure extra options for liveness and readiness probes +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) +livenessProbe: + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 +readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +## Configure the ingress resource that allows you to access the +## Wordpress installation. Set up the URL +## ref: http://kubernetes.io/docs/user-guide/ingress/ +## +ingress: + ## Set to true to enable ingress record generation + enabled: false + + ## The list of hostnames to be covered with this ingress record. + ## Most likely this will be just one host, but in the event more hosts are needed, this is an array + hosts: + - name: wordpress.local + + ## Set this to true in order to enable TLS on the ingress record + ## A side effect of this will be that the backend wordpress service will be connected at port 443 + tls: false + + ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS + tlsSecret: wordpress.local-tls + + ## Ingress annotations done as key:value pairs + ## If you're using kube-lego, you will want to add: + ## kubernetes.io/tls-acme: true + ## + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: true + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: wordpress.local-tls + # key: + # certificate: + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## wordpress data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 10Gi + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + requests: + memory: 512Mi + cpu: 300m + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} diff --git a/pkg/action/testdata/output/compressed-deps-tgz.txt b/pkg/action/testdata/output/compressed-deps-tgz.txt new file mode 100644 index 000000000..6cc526b70 --- /dev/null +++ b/pkg/action/testdata/output/compressed-deps-tgz.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked + diff --git a/pkg/action/testdata/output/compressed-deps.txt b/pkg/action/testdata/output/compressed-deps.txt new file mode 100644 index 000000000..ff2b0ab75 --- /dev/null +++ b/pkg/action/testdata/output/compressed-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ ok + diff --git a/pkg/action/testdata/output/missing-deps.txt b/pkg/action/testdata/output/missing-deps.txt new file mode 100644 index 000000000..8d742883a --- /dev/null +++ b/pkg/action/testdata/output/missing-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ missing + diff --git a/pkg/action/testdata/output/uncompressed-deps-tgz.txt b/pkg/action/testdata/output/uncompressed-deps-tgz.txt new file mode 100644 index 000000000..6cc526b70 --- /dev/null +++ b/pkg/action/testdata/output/uncompressed-deps-tgz.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked + diff --git a/pkg/action/testdata/output/uncompressed-deps.txt b/pkg/action/testdata/output/uncompressed-deps.txt new file mode 100644 index 000000000..6cc526b70 --- /dev/null +++ b/pkg/action/testdata/output/uncompressed-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked + diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index dfaa98472..a51a283d6 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -169,6 +169,7 @@ func joinErrors(errs []error) string { // deleteRelease deletes the release and returns manifests that were kept in the deletion process func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { + var errs []error caps, err := u.cfg.getCapabilities() if err != nil { return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} @@ -194,11 +195,13 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { for _, file := range filesToDelete { builder.WriteString("\n---\n" + file.Content) } + resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) if err != nil { return "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} } - - _, errs := u.cfg.KubeClient.Delete(resources) + if len(resources) > 0 { + _, errs = u.cfg.KubeClient.Delete(resources) + } return kept, errs } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 08b638171..fc289dbab 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -18,6 +18,7 @@ package action import ( "bytes" + "context" "fmt" "strings" "time" @@ -32,6 +33,7 @@ import ( "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/storage/driver" ) // Upgrade is the action for upgrading releases. @@ -42,25 +44,57 @@ type Upgrade struct { ChartPathOptions - Install bool - Devel bool - Namespace string - Timeout time.Duration - Wait bool + // Install is a purely informative flag that indicates whether this upgrade was done in "install" mode. + // + // Applications may use this to determine whether this Upgrade operation was done as part of a + // pure upgrade (Upgrade.Install == false) or as part of an install-or-upgrade operation + // (Upgrade.Install == true). + // + // Setting this to `true` will NOT cause `Upgrade` to perform an install if the release does not exist. + // That process must be handled by creating an Install action directly. See cmd/upgrade.go for an + // example of how this flag is used. + Install bool + // Devel indicates that the operation is done in devel mode. + Devel bool + // Namespace is the namespace in which this operation should be performed. + Namespace string + // SkipCRDs skips installing CRDs when install flag is enabled during upgrade + SkipCRDs bool + // Timeout is the timeout for this operation + Timeout time.Duration + // Wait determines whether the wait operation should be performed after the upgrade is requested. + Wait bool + // DisableHooks disables hook processing if set to true. DisableHooks bool - DryRun bool - Force bool - ResetValues bool - ReuseValues bool + // DryRun controls whether the operation is prepared, but not executed. + // If `true`, the upgrade is prepared but not performed. + DryRun bool + // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + // + // This should be used with caution. + Force bool + // ResetValues will reset the values to the chart's built-ins rather than merging with existing. + ResetValues bool + // ReuseValues will re-use the user's last supplied values. + ReuseValues bool // 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, if true, will roll back on failure. + Atomic bool + // CleanupOnFail will, if true, cause the upgrade to delete newly-created resources on a failed update. + CleanupOnFail bool + // SubNotes determines whether sub-notes are rendered in the chart. + SubNotes bool + // Description is the description of this operation + Description string + // PostRender is an optional post-renderer + // + // If this is non-nil, then after templates are rendered, they will be sent to the + // post renderer before sending to the Kuberntes API server. + PostRenderer postrender.PostRenderer + // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. DisableOpenAPIValidation bool } @@ -73,6 +107,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 @@ -122,12 +160,33 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, errMissingChart } - // finds the deployed release with the given name - currentRelease, err := u.cfg.Releases.Deployed(name) + // finds the last non-deleted release with the given name + lastRelease, err := u.cfg.Releases.Last(name) if err != nil { + // to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist + if errors.Is(err, driver.ErrReleaseNotFound) { + return nil, nil, driver.NewErrNoDeployedReleases(name) + } return nil, nil, err } + var currentRelease *release.Release + if lastRelease.Info.Status == release.StatusDeployed { + // no need to retrieve the last deployed release from storage as the last release is deployed + currentRelease = lastRelease + } else { + // finds the deployed release with the given name + currentRelease, err = u.cfg.Releases.Deployed(name) + if err != nil { + if errors.Is(err, driver.ErrNoDeployedReleases) && + (lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) { + currentRelease = lastRelease + } else { + return nil, nil, err + } + } + } + // determine if values will be reused vals, err = u.reuseValues(chart, currentRelease, vals) if err != nil { @@ -138,12 +197,6 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, err } - // finds the non-deleted release with the given name - lastRelease, err := u.cfg.Releases.Last(name) - if err != nil { - return nil, nil, err - } - // Increment revision count. This is passed to templates, and also stored on // the release object. revision := lastRelease.Version + 1 @@ -164,7 +217,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun) if err != nil { return nil, nil, err } @@ -196,6 +249,13 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) { current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false) if err != nil { + // Checking for removed Kubernetes API error so can provide a more informative error message to the user + // Ref: https://github.com/helm/helm/issues/7219 + if strings.Contains(err.Error(), "unable to recognize \"\": no matches for kind") { + return upgradedRelease, errors.Wrap(err, "current release manifest contains removed kubernetes api(s) for this "+ + "kubernetes version and it is therefore unable to build the kubernetes "+ + "objects for performing the diff. error from kubernetes") + } return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest") } target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation) @@ -203,6 +263,12 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea 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 { @@ -216,10 +282,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 { @@ -407,7 +482,7 @@ func recreate(cfg *Configuration, resources kube.ResourceList) error { return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name) } - pods, err := client.CoreV1().Pods(res.Namespace).List(metav1.ListOptions{ + pods, err := client.CoreV1().Pods(res.Namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: selector.String(), }) if err != nil { @@ -417,7 +492,7 @@ func recreate(cfg *Configuration, resources kube.ResourceList) error { // Restart pods for _, pod := range pods.Items { // Delete each pod for get them restarted with changed spec. - if err := client.CoreV1().Pods(pod.Namespace).Delete(pod.Name, metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { + if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, *metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name) } } 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/chart/chart_test.go b/pkg/chart/chart_test.go index ef623fff6..ef8cec3ad 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -78,3 +78,134 @@ func TestSaveChartNoRawData(t *testing.T) { is.Equal([]*File(nil), res.Raw) } + +func TestMetadata(t *testing.T) { + chrt := Chart{ + Metadata: &Metadata{ + Name: "foo.yaml", + AppVersion: "1.0.0", + APIVersion: "v2", + Version: "1.0.0", + Type: "application", + }, + } + + is := assert.New(t) + + is.Equal("foo.yaml", chrt.Name()) + is.Equal("1.0.0", chrt.AppVersion()) + is.Equal(nil, chrt.Validate()) +} + +func TestIsRoot(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal(false, chrt1.IsRoot()) + is.Equal(true, chrt2.IsRoot()) +} + +func TestChartPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo.", chrt1.ChartPath()) + is.Equal("foo", chrt2.ChartPath()) +} + +func TestChartFullPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo/charts/", chrt1.ChartFullPath()) + is.Equal("foo", chrt2.ChartFullPath()) +} + +func TestCRDObjects(t *testing.T) { + chrt := Chart{ + Files: []*File{ + { + Name: "crds/foo.yaml", + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + Data: []byte("# hello"), + }, + }, + } + + expected := []CRD{ + { + Name: "crds/foo.yaml", + Filename: "crds/foo.yaml", + File: &File{ + Name: "crds/foo.yaml", + Data: []byte("hello"), + }, + }, + { + Name: "crds/foo/bar/baz.yaml", + Filename: "crds/foo/bar/baz.yaml", + File: &File{ + Name: "crds/foo/bar/baz.yaml", + Data: []byte("hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDObjects() + is.Equal(expected, crds) +} diff --git a/pkg/chart/loader/archive.go b/pkg/chart/loader/archive.go index 7e187a170..8b38cb89f 100644 --- a/pkg/chart/loader/archive.go +++ b/pkg/chart/loader/archive.go @@ -173,7 +173,9 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { return nil, err } - files = append(files, &BufferedFile{Name: n, Data: b.Bytes()}) + data := bytes.TrimPrefix(b.Bytes(), utf8bom) + + files = append(files, &BufferedFile{Name: n, Data: data}) b.Reset() } diff --git a/pkg/chart/loader/directory.go b/pkg/chart/loader/directory.go index a12c5158e..bbe543870 100644 --- a/pkg/chart/loader/directory.go +++ b/pkg/chart/loader/directory.go @@ -17,6 +17,7 @@ limitations under the License. package loader import ( + "bytes" "fmt" "io/ioutil" "os" @@ -30,6 +31,8 @@ import ( "helm.sh/helm/v3/pkg/chart" ) +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + // DirLoader loads a chart from a directory type DirLoader string @@ -104,6 +107,8 @@ func LoadDir(dir string) (*chart.Chart, error) { return errors.Wrapf(err, "error reading %s", n) } + data = bytes.TrimPrefix(data, utf8bom) + files = append(files, &BufferedFile{Name: n, Data: data}) return nil } diff --git a/pkg/chart/loader/load_test.go b/pkg/chart/loader/load_test.go index 26513d359..40b86dec2 100644 --- a/pkg/chart/loader/load_test.go +++ b/pkg/chart/loader/load_test.go @@ -20,6 +20,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "io" "io/ioutil" "os" "path/filepath" @@ -85,6 +86,86 @@ func TestLoadDirWithSymlink(t *testing.T) { verifyDependenciesLock(t, c) } +func TestBomTestData(t *testing.T) { + testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} + for _, file := range testFiles { + data, err := ioutil.ReadFile("testdata/" + file) + if err != nil || !bytes.HasPrefix(data, utf8bom) { + t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) + } + } + + archive, err := ioutil.ReadFile("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + unzipped, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + defer unzipped.Close() + for _, testFile := range testFiles { + data := make([]byte, 3) + err := unzipped.Reset(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + tr := tar.NewReader(unzipped) + for { + file, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + if file != nil && strings.EqualFold(file.Name, testFile) { + _, err := tr.Read(data) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } else { + break + } + } + } + if !bytes.Equal(data, utf8bom) { + t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) + } + } +} + +func TestLoadDirWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadArchiveWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + func TestLoadV1(t *testing.T) { l, err := Loader("testdata/frobnitz.v1") if err != nil { @@ -465,3 +546,11 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { } } } + +func verifyBomStripped(t *testing.T, files []*chart.File) { + for _, file := range files { + if bytes.HasPrefix(file.Data, utf8bom) { + t.Errorf("Byte Order Mark still present in processed file %s", file.Name) + } + } +} diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom.tgz new file mode 100644 index 000000000..be0cd027d Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore b/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore new file mode 100644 index 000000000..7a4b92da2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock new file mode 100644 index 000000000..ed43b227f --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml new file mode 100644 index 000000000..21b21f0b5 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt new file mode 100644 index 000000000..77c4e724a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE b/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE new file mode 100644 index 000000000..c27b00bf2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/README.md new file mode 100644 index 000000000..e9c40031b --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me new file mode 100644 index 000000000..a7e3a38b7 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml new file mode 100644 index 000000000..adb9853c6 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md new file mode 100644 index 000000000..ea7526bee --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md @@ -0,0 +1,9 @@ +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.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..1ad84b346 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..f690d53c4 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 000000000..61cb62051 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..f3e662a28 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml new file mode 100644 index 000000000..6b7cb2596 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz new file mode 100644 index 000000000..3190136b0 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md new file mode 100644 index 000000000..816c3e431 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg b/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl new file mode 100644 index 000000000..bb29c5491 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml new file mode 100644 index 000000000..c24ceadf9 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/metadata_test.go b/pkg/chart/metadata_test.go new file mode 100644 index 000000000..8b436000b --- /dev/null +++ b/pkg/chart/metadata_test.go @@ -0,0 +1,59 @@ +/* +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 chart + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + tests := []struct { + md *Metadata + err error + }{ + { + nil, + ValidationError("chart.metadata is required"), + }, + { + &Metadata{Name: "test", Version: "1.0"}, + ValidationError("chart.metadata.apiVersion is required"), + }, + { + &Metadata{APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name is required"), + }, + { + &Metadata{Name: "test", APIVersion: "v2"}, + ValidationError("chart.metadata.version is required"), + }, + { + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "test"}, + ValidationError("chart.metadata.type must be application or library"), + }, + { + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"}, + nil, + }, + } + + for _, tt := range tests { + result := tt.md.Validate() + if result != tt.err { + t.Errorf("expected %s, got %s", tt.err, result) + } + } +} diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go index bbdd5f21c..1d3d45e99 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -35,8 +35,6 @@ import ( // - A chart has access to all of the variables for it, as well as all of // the values destined for its dependencies. func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - // create a copy of vals and then pass it to coalesce - // and coalesceDeps, as both will mutate the passed values v, err := copystructure.Copy(vals) if err != nil { return vals, err @@ -47,10 +45,7 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err if valsCopy == nil { valsCopy = make(map[string]interface{}) } - if _, err := coalesce(chrt, valsCopy); err != nil { - return valsCopy, err - } - return coalesceDeps(chrt, valsCopy) + return coalesce(chrt, valsCopy) } // coalesce coalesces the dest values and the chart values, giving priority to the dest values. @@ -180,25 +175,28 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) { // // dest is considered authoritative. func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { - if dst == nil || src == nil { + // When --reuse-values is set but there are no modifications yet, return new values + if src == nil { + return dst + } + if dst == nil { return src } // Because dest has higher precedence than src, dest values override src // values. for key, val := range src { - if istable(val) { - switch innerdst, ok := dst[key]; { - case !ok: - dst[key] = val - case istable(innerdst): - CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) - default: + if dv, ok := dst[key]; ok && dv == nil { + delete(dst, key) + } else if !ok { + dst[key] = val + } else if istable(val) { + if istable(dv) { + CoalesceTables(dv.(map[string]interface{}), val.(map[string]interface{})) + } else { log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) } - } else if dv, ok := dst[key]; ok && istable(dv) { + } else if istable(dv) { log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) - } else if !ok { // <- ok is still in scope from preceding conditional. - dst[key] = val } } return dst diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index 6e82de590..2a3d848fa 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -31,6 +31,8 @@ right: Null left: NULL front: ~ back: "" +nested: + boat: null global: name: Ishmael @@ -47,6 +49,10 @@ pequod: sail: true ahab: scope: whale + boat: null + nested: + foo: true + bar: null `) func TestCoalesceValues(t *testing.T) { @@ -86,6 +92,7 @@ func TestCoalesceValues(t *testing.T) { {"{{.pequod.name}}", "pequod"}, {"{{.pequod.ahab.name}}", "ahab"}, {"{{.pequod.ahab.scope}}", "whale"}, + {"{{.pequod.ahab.nested.foo}}", "true"}, {"{{.pequod.ahab.global.name}}", "Ishmael"}, {"{{.pequod.ahab.global.subject}}", "Queequeg"}, {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, @@ -114,6 +121,19 @@ func TestCoalesceValues(t *testing.T) { } } + if _, ok := v["nested"].(map[string]interface{})["boat"]; ok { + t.Error("Expected nested boat key to be removed, still present") + } + + subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) + if _, ok := subchart["boat"]; ok { + t.Error("Expected subchart boat key to be removed, still present") + } + + if _, ok := subchart["nested"].(map[string]interface{})["bar"]; ok { + t.Error("Expected subchart nested bar key to be removed, still present") + } + // CoalesceValues should not mutate the passed arguments is.Equal(valsCopy, vals) } @@ -122,24 +142,28 @@ func TestCoalesceTables(t *testing.T) { dst := map[string]interface{}{ "name": "Ishmael", "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": nil, }, "details": map[string]interface{}{ "friends": []string{"Tashtego"}, }, "boat": "pequod", + "hole": nil, } src := map[string]interface{}{ "occupation": "whaler", "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", + "state": "MA", + "street": "234 Spouter Inn Ct.", + "country": "US", }, "details": "empty", "boat": map[string]interface{}{ "mast": true, }, + "hole": "black", } // What we expect is that anything in dst overrides anything in src, but that @@ -170,6 +194,10 @@ func TestCoalesceTables(t *testing.T) { t.Errorf("Unexpected state: %v", addr["state"]) } + if _, ok = addr["country"]; ok { + t.Error("The country is not left out.") + } + if det, ok := dst["details"].(map[string]interface{}); !ok { t.Fatalf("Details is the wrong type: %v", dst["details"]) } else if _, ok := det["friends"]; !ok { @@ -179,4 +207,61 @@ func TestCoalesceTables(t *testing.T) { if dst["boat"].(string) != "pequod" { t.Errorf("Expected boat string, got %v", dst["boat"]) } + + if _, ok = dst["hole"]; ok { + t.Error("The hole still exists.") + } + + dst2 := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": "US", + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": "black", + } + + // What we expect is that anything in dst should have all values set, + // this happens when the --reuse-values flag is set but the chart has no modifications yet + CoalesceTables(dst2, nil) + + if dst2["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst2["name"]) + } + + addr2, ok := dst2["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr2["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr2["street"]) + } + + if addr2["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr2["city"]) + } + + if addr2["country"].(string) != "US" { + t.Errorf("Unexpected Country: %v", addr2["country"]) + } + + if det2, ok := dst2["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst2["details"]) + } else if _, ok := det2["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst2["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst2["boat"]) + } + + if dst2["hole"].(string) != "black" { + t.Errorf("Expected hole string, got %v", dst2["boat"]) + } } diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index 24eb1e277..0e87c7b47 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -53,6 +53,8 @@ const ( ServiceName = TemplatesDir + sep + "service.yaml" // ServiceAccountName is the name of the example serviceaccount file. ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" + // HorizontalPodAutoscalerName is the name of the example hpa file. + HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" // NotesName is the name of the example NOTES.txt file. NotesName = TemplatesDir + sep + "NOTES.txt" // HelpersName is the name of the example helpers file. @@ -79,10 +81,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.1.0 # This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. appVersion: 1.16.0 ` @@ -95,6 +99,8 @@ replicaCount: 1 image: repository: nginx pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" imagePullSecrets: [] nameOverride: "" @@ -107,7 +113,9 @@ serviceAccount: annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template - name: + name: "" + +podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 @@ -149,6 +157,13 @@ resources: {} # cpu: 100m # memory: 128Mi +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + nodeSelector: {} tolerations: [] @@ -199,29 +214,29 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: -{{- if .Values.ingress.tls }} + {{- if .Values.ingress.tls }} tls: - {{- range .Values.ingress.tls }} + {{- range .Values.ingress.tls }} - hosts: - {{- range .hosts }} + {{- range .hosts }} - {{ . | quote }} - {{- end }} + {{- end }} secretName: {{ .secretName }} + {{- end }} {{- end }} -{{- end }} rules: - {{- range .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: - {{- range .paths }} + {{- range .paths }} - path: {{ . }} backend: serviceName: {{ $fullName }} servicePort: {{ $svcPort }} - {{- end }} + {{- end }} + {{- end }} {{- end }} -{{- end }} ` const defaultDeployment = `apiVersion: apps/v1 @@ -231,19 +246,25 @@ metadata: labels: {{- include ".labels" . | nindent 4 }} spec: +{{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} +{{- end }} selector: matchLabels: {{- include ".selectorLabels" . | nindent 6 }} template: metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include ".selectorLabels" . | nindent 8 }} spec: - {{- with .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} serviceAccountName: {{ include ".serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} @@ -251,7 +272,7 @@ spec: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http @@ -271,14 +292,14 @@ spec: nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.affinity }} + {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} + {{- end }} + {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} ` const defaultService = `apiVersion: v1 @@ -309,7 +330,37 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} -{{- end -}} +{{- end }} +` + +const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include ".fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} ` const defaultNotes = `1. Get the application URL by running these commands: @@ -340,8 +391,8 @@ const defaultHelpers = `{{/* vim: set filetype=mustache: */}} Expand the name of the chart. */}} {{- define ".name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} {{/* Create a default fully qualified app name. @@ -349,24 +400,24 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this If release name contains chart name it will be used as a full name. */}} {{- define ".fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define ".chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} {{/* Common labels @@ -378,7 +429,7 @@ helm.sh/chart: {{ include ".chart" . }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end -}} +{{- end }} {{/* Selector labels @@ -386,18 +437,18 @@ Selector labels {{- define ".selectorLabels" -}} app.kubernetes.io/name: {{ include ".name" . }} app.kubernetes.io/instance: {{ .Release.Name }} -{{- end -}} +{{- end }} {{/* Create the name of the service account to use */}} {{- define ".serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} - {{ default (include ".fullname" .) .Values.serviceAccount.name }} -{{- else -}} - {{ default "default" .Values.serviceAccount.name }} -{{- end -}} -{{- end -}} +{{- if .Values.serviceAccount.create }} +{{- default (include ".fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} ` const defaultTestConnection = `apiVersion: v1 @@ -526,6 +577,11 @@ func Create(name, dir string) (string, error) { path: filepath.Join(cdir, ServiceAccountName), content: transform(defaultServiceAccount, name), }, + { + // hpa.yaml + path: filepath.Join(cdir, HorizontalPodAutoscalerName), + content: transform(defaultHorizontalPodAutoscaler, name), + }, { // NOTES.txt path: filepath.Join(cdir, NotesName), diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index a2c6a9225..1011436b5 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -34,6 +34,9 @@ import ( var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") // SaveDir saves a chart as files in a directory. +// +// This takes the chart name, and creates a new subdirectory inside of the given dest +// directory, writing the chart's contents to that subdirectory. func SaveDir(c *chart.Chart, dest string) error { // Create the chart directory outdir := filepath.Join(dest, c.Name()) @@ -223,7 +226,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/testdata/moby/charts/pequod/charts/ahab/values.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml index 86c3f63aa..eee6980fa 100644 --- a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml +++ b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml @@ -1,2 +1,6 @@ scope: ahab name: ahab +boat: true +nested: + foo: false + bar: true diff --git a/pkg/chartutil/testdata/moby/values.yaml b/pkg/chartutil/testdata/moby/values.yaml index 54e1ce463..2169d7566 100644 --- a/pkg/chartutil/testdata/moby/values.yaml +++ b/pkg/chartutil/testdata/moby/values.yaml @@ -7,3 +7,5 @@ right: exists left: exists front: exists back: exists +nested: + boat: true diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml new file mode 100644 index 000000000..91b954e5f --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml new file mode 100644 index 000000000..5d193f1a6 --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Chart.Name }}-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Chart.Name }}-role +subjects: +- kind: ServiceAccount + name: {{ .Chart.Name }}-sa + namespace: default diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml new file mode 100644 index 000000000..7126c7d89 --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-sa diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index e279331b0..d62f57a55 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -26,21 +26,17 @@ import ( "fmt" "os" "strconv" - "sync" "github.com/spf13/pflag" - "k8s.io/cli-runtime/pkg/genericclioptions" "helm.sh/helm/v3/pkg/helmpath" - "helm.sh/helm/v3/pkg/kube" ) // EnvSettings describes all of the environment settings. type EnvSettings struct { - namespace string - config genericclioptions.RESTClientGetter - configOnce sync.Once + namespace string + config *genericclioptions.ConfigFlags // KubeConfig is the path to the kubeconfig file KubeConfig string @@ -63,8 +59,7 @@ type EnvSettings struct { } func New() *EnvSettings { - - env := EnvSettings{ + env := &EnvSettings{ namespace: os.Getenv("HELM_NAMESPACE"), KubeContext: os.Getenv("HELM_KUBECONTEXT"), KubeToken: os.Getenv("HELM_KUBETOKEN"), @@ -75,7 +70,16 @@ func New() *EnvSettings { RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) - return &env + + // bind to kubernetes config flags + env.config = &genericclioptions.ConfigFlags{ + Namespace: &env.namespace, + Context: &env.KubeContext, + BearerToken: &env.KubeToken, + APIServer: &env.KubeAPIServer, + KubeConfig: &env.KubeConfig, + } + return env } // AddFlags binds flags to the given flagset. @@ -101,48 +105,36 @@ func envOr(name, def string) string { func (s *EnvSettings) EnvVars() map[string]string { envvars := map[string]string{ "HELM_BIN": os.Args[0], + "HELM_CACHE_HOME": helmpath.CachePath(""), + "HELM_CONFIG_HOME": helmpath.ConfigPath(""), + "HELM_DATA_HOME": helmpath.DataPath(""), "HELM_DEBUG": fmt.Sprint(s.Debug), "HELM_PLUGINS": s.PluginsDirectory, "HELM_REGISTRY_CONFIG": s.RegistryConfig, "HELM_REPOSITORY_CACHE": s.RepositoryCache, "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, "HELM_NAMESPACE": s.Namespace(), - "HELM_KUBECONTEXT": s.KubeContext, - "HELM_KUBETOKEN": s.KubeToken, - "HELM_KUBEAPISERVER": s.KubeAPIServer, - } + // broken, these are populated from helm flags and not kubeconfig. + "HELM_KUBECONTEXT": s.KubeContext, + "HELM_KUBETOKEN": s.KubeToken, + "HELM_KUBEAPISERVER": s.KubeAPIServer, + } if s.KubeConfig != "" { envvars["KUBECONFIG"] = s.KubeConfig } - return envvars } -//Namespace gets the namespace from the configuration +// Namespace gets the namespace from the configuration func (s *EnvSettings) Namespace() string { - if s.namespace != "" { - return s.namespace - } - - if ns, _, err := s.RESTClientGetter().ToRawKubeConfigLoader().Namespace(); err == nil { + if ns, _, err := s.config.ToRawKubeConfigLoader().Namespace(); err == nil { return ns } return "default" } -//RESTClientGetter gets the kubeconfig from EnvSettings +// RESTClientGetter gets the kubeconfig from EnvSettings func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { - s.configOnce.Do(func() { - 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..ef26f3348 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -18,7 +18,6 @@ package downloader import ( "fmt" "io" - "io/ioutil" "net/url" "os" "path/filepath" @@ -26,6 +25,7 @@ import ( "github.com/pkg/errors" + "helm.sh/helm/v3/internal/fileutil" "helm.sh/helm/v3/internal/urlutil" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/helmpath" @@ -101,7 +101,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven name := filepath.Base(u.Path) destfile := filepath.Join(dest, name) - if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil { + if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil { return destfile, nil, err } @@ -117,7 +117,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return destfile, ver, nil } provfile := destfile + ".prov" - if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil { + if err := fileutil.AtomicWriteFile(provfile, body, 0644); err != nil { return destfile, nil, err } @@ -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..00198de0c 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 { @@ -226,6 +239,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) var saveError error + churls := make(map[string]struct{}) for _, dep := range deps { // No repository means the chart is in charts directory if dep.Repository == "" { @@ -265,8 +279,6 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { continue } - fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) - // Any failure to resolve/download a chart should fail: // https://github.com/helm/helm/issues/1439 churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) @@ -275,6 +287,13 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { break } + if _, ok := churls[churl]; ok { + fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository) + continue + } + + fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) + dl := ChartDownloader{ Out: m.Out, Verify: m.Verify, @@ -291,6 +310,8 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { saveError = errors.Wrapf(err, "could not download %s", churl) break } + + churls[churl] = struct{}{} } if saveError == nil { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 1cc94d685..c5d064ad5 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -172,7 +172,10 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render } return val, nil } - if e.config != nil { + + // If we are not linting and have a cluster connection, provide a Kubernetes-backed + // implementation. + if !e.LintMode && e.config != nil { funcMap["lookup"] = NewLookupFunction(e.config) } @@ -213,6 +216,7 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) // We want to parse the templates in a predictable order. The order favors // higher-level (in file system) templates over deeply nested templates. keys := sortTemplates(tpls) + referenceKeys := sortTemplates(referenceTpls) for _, filename := range keys { r := tpls[filename] @@ -223,8 +227,9 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) // Adding the reference templates to the template context // so they can be referenced in the tpl function - for filename, r := range referenceTpls { + for _, filename := range referenceKeys { if t.Lookup(filename) == nil { + r := referenceTpls[filename] if _, err := t.New(filename).Parse(r.tpl); err != nil { return map[string]string{}, cleanupParseError(filename, err) } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index d5f36aac8..87e84c48b 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -70,7 +70,7 @@ func TestFuncMap(t *testing.T) { } // Test for Engine-specific template functions. - expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson"} + expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson", "lookup"} for _, f := range expect { if _, ok := fns[f]; !ok { t.Errorf("Expected add-on function %q", f) @@ -126,6 +126,46 @@ func TestRender(t *testing.T) { } } +func TestRenderRefsOrdering(t *testing.T) { + parentChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "parent", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + }, + } + childChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "child", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + }, + } + parentChart.AddDependency(childChart) + + expect := map[string]string{ + "parent/templates/test.yaml": "parent value", + } + + for i := 0; i < 100; i++ { + out, err := Render(parentChart, chartutil.Values{}) + if err != nil { + t.Fatalf("Failed to render templates: %s", err) + } + + for name, data := range expect { + if out[name] != data { + t.Fatalf("Expected %q, got %q (iteraction %d)", data, out[name], i+1) + } + } + } +} + func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index dac105e74..92b4c3383 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -48,11 +48,13 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "toYaml": toYAML, - "fromYaml": fromYAML, - "toJson": toJSON, - "fromJson": fromJSON, + "toToml": toTOML, + "toYaml": toYAML, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the @@ -60,6 +62,11 @@ func funcMap() template.FuncMap { "include": func(string, interface{}) string { return "not implemented" }, "tpl": func(string, interface{}) interface{} { return "not implemented" }, "required": func(string, interface{}) (interface{}, error) { return "not implemented", nil }, + // Provide a placeholder for the "lookup" function, which requires a kubernetes + // connection. + "lookup": func(string, string, string, string) (map[string]interface{}, error) { + return map[string]interface{}{}, nil + }, } for k, v := range extra { @@ -97,6 +104,21 @@ func fromYAML(str string) map[string]interface{} { return m } +// fromYAMLArray converts a YAML array into a []interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) []interface{} { + a := []interface{}{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} + // toTOML takes an interface, marshals it to toml, and returns a string. It will // always return a string, even on marshal error (empty string). // @@ -138,3 +160,18 @@ func fromJSON(str string) map[string]interface{} { } return m } + +// fromJSONArray converts a JSON array into a []interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromJSONArray(str string) []interface{} { + a := []interface{}{} + + if err := json.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index a94ff257e..62c63ec2b 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -45,6 +45,14 @@ func TestFuncs(t *testing.T) { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", vars: `hello: world`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: "- one\n- 2\n- name: helm\n", + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: `["one", 2, { "name": "helm" }]`, }, { // Regression for https://github.com/helm/helm/issues/2271 tpl: `{{ toToml . }}`, @@ -62,6 +70,14 @@ func TestFuncs(t *testing.T) { tpl: `{{ fromJson . }}`, expect: `map[Error:json: cannot unmarshal array into Go value of type map[string]interface {}]`, vars: `["one", "two"]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[one 2 map[name:helm]]`, + vars: `["one", 2, { "name": "helm" }]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `{"hello": "world"}`, }, { tpl: `{{ merge .dict (fromYaml .yaml) }}`, expect: `map[a:map[b:c]]`, @@ -74,6 +90,15 @@ func TestFuncs(t *testing.T) { tpl: `{{ fromYaml . }}`, expect: `map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]`, vars: `["one", "two"]`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: `[error unmarshaling JSON: while decoding JSON: json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `hello: world`, + }, { + // This should never result in a network lookup. Regression for #7955 + tpl: `{{ lookup "v1" "Namespace" "" "unlikelynamespace99999999" }}`, + expect: `map[]`, + vars: `["one", "two"]`, }} for _, tt := range tests { @@ -83,3 +108,71 @@ func TestFuncs(t *testing.T) { assert.Equal(t, tt.expect, b.String(), tt.tpl) } } + +// This test to check a function provided by sprig is due to a change in a +// dependency of sprig. mergo in v0.3.9 changed the way it merges and only does +// public fields (i.e. those starting with a capital letter). This test, from +// sprig, fails in the new version. This is a behavior change for mergo that +// impacts sprig and Helm users. This test will help us to not update to a +// version of mergo (even accidentally) that causes a breaking change. See +// sprig changelog and notes for more details. +// Note, Go modules assume semver is never broken. So, there is no way to tell +// the tooling to not update to a minor or patch version. `go get -u` could be +// used to accidentally update mergo. This test and message should catch the +// problem and explain why it's happening. +func TestMerge(t *testing.T) { + dict := map[string]interface{}{ + "src2": map[string]interface{}{ + "h": 10, + "i": "i", + "j": "j", + }, + "src1": map[string]interface{}{ + "a": 1, + "b": 2, + "d": map[string]interface{}{ + "e": "four", + }, + "g": []int{6, 7}, + "i": "aye", + "j": "jay", + "k": map[string]interface{}{ + "l": false, + }, + }, + "dst": map[string]interface{}{ + "a": "one", + "c": 3, + "d": map[string]interface{}{ + "f": 5, + }, + "g": []int{8, 9}, + "i": "eye", + "k": map[string]interface{}{ + "l": true, + }, + }, + } + tpl := `{{merge .dst .src1 .src2}}` + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tpl)).Execute(&b, dict) + assert.NoError(t, err) + + expected := map[string]interface{}{ + "a": "one", // key overridden + "b": 2, // merged from src1 + "c": 3, // merged from dst + "d": map[string]interface{}{ // deep merge + "e": "four", + "f": 5, + }, + "g": []int{8, 9}, // overridden - arrays are not merged + "h": 10, // merged from src2 + "i": "eye", // overridden twice + "j": "jay", // overridden and merged + "k": map[string]interface{}{ + "l": true, // overridden + }, + } + assert.Equal(t, expected, dict["dst"]) +} diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 5dde29443..20be9189e 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -17,6 +17,7 @@ limitations under the License. package engine import ( + "context" "log" "strings" @@ -31,8 +32,12 @@ import ( type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) -// NewLookupFunction returns a function for looking up objects in the cluster. If the resource does not exist, no error -// is raised. +// NewLookupFunction returns a function for looking up objects in the cluster. +// +// If the resource does not exist, no error is raised. +// +// This function is considered deprecated, and will be renamed in Helm 4. It will no +// longer be a public function. func NewLookupFunction(config *rest.Config) lookupFunc { return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { var client dynamic.ResourceInterface @@ -47,7 +52,7 @@ func NewLookupFunction(config *rest.Config) lookupFunc { } if name != "" { // this will return a single object - obj, err := client.Get(name, metav1.GetOptions{}) + obj, err := client.Get(context.Background(), name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. @@ -59,7 +64,7 @@ func NewLookupFunction(config *rest.Config) lookupFunc { return obj.UnstructuredContent(), nil } //this will return a list - obj, err := client.List(metav1.ListOptions{}) + obj, err := client.List(context.Background(), metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. diff --git a/pkg/getter/getter_test.go b/pkg/getter/getter_test.go index 60eb4738e..79a3338e9 100644 --- a/pkg/getter/getter_test.go +++ b/pkg/getter/getter_test.go @@ -53,9 +53,10 @@ func TestProviders(t *testing.T) { } func TestAll(t *testing.T) { - all := All(&cli.EnvSettings{ - PluginsDirectory: pluginDir, - }) + env := cli.New() + env.PluginsDirectory = pluginDir + + all := All(env) if len(all) != 3 { t.Errorf("expected 3 providers (default plus two plugins), got %d", len(all)) } @@ -66,9 +67,10 @@ func TestAll(t *testing.T) { } func TestByScheme(t *testing.T) { - g := All(&cli.EnvSettings{ - PluginsDirectory: pluginDir, - }) + env := cli.New() + env.PluginsDirectory = pluginDir + + g := All(env) if _, err := g.ByScheme("test"); err != nil { t.Error(err) } diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 695a87743..51ad1ceb9 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -90,6 +90,10 @@ func NewHTTPGetter(options ...Option) (Getter, error) { } func (g *HTTPGetter) httpClient() (*http.Client, error) { + transport := &http.Transport{ + DisableCompression: true, + Proxy: http.ProxyFromEnvironment, + } if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" { tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile) if err != nil { @@ -103,28 +107,19 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { } tlsConf.ServerName = sni - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - Proxy: http.ProxyFromEnvironment, - }, - } - - return client, nil + transport.TLSClientConfig = tlsConf } if g.opts.insecureSkipVerifyTLS { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - Proxy: http.ProxyFromEnvironment, - }, + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, } - return client, nil } - return http.DefaultClient, nil + client := &http.Client{ + Transport: transport, + } + + return client, nil } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index a1288bf47..91a1b4e62 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -17,10 +17,13 @@ package getter import ( "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" "path/filepath" + "strconv" "strings" "testing" @@ -122,7 +125,7 @@ func TestDownload(t *testing.T) { })) defer srv.Close() - g, err := All(new(cli.EnvSettings)).ByScheme("http") + g, err := All(cli.New()).ByScheme("http") if err != nil { t.Fatal(err) } @@ -248,3 +251,39 @@ func TestDownloadInsecureSkipTLSVerify(t *testing.T) { } } + +func TestHTTPGetterTarDownload(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f, _ := os.Open("testdata/empty-0.0.1.tgz") + defer f.Close() + + b := make([]byte, 512) + f.Read(b) + //Get the file size + FileStat, _ := f.Stat() + FileSize := strconv.FormatInt(FileStat.Size(), 10) + + //Simulating improper header values from bitbucket + w.Header().Set("Content-Type", "application/x-tar") + w.Header().Set("Content-Encoding", "gzip") + w.Header().Set("Content-Length", FileSize) + + f.Seek(0, 0) + io.Copy(w, f) + })) + + defer srv.Close() + + g, err := NewHTTPGetter(WithURL(srv.URL)) + if err != nil { + t.Fatal(err) + } + + data, _ := g.Get(srv.URL) + mimeType := http.DetectContentType(data.Bytes()) + + expectedMimeType := "application/x-gzip" + if mimeType != expectedMimeType { + t.Fatalf("Expected response with MIME type %s, but got %s", expectedMimeType, mimeType) + } +} diff --git a/pkg/getter/plugingetter_test.go b/pkg/getter/plugingetter_test.go index 71563e169..a18fa302b 100644 --- a/pkg/getter/plugingetter_test.go +++ b/pkg/getter/plugingetter_test.go @@ -24,9 +24,9 @@ import ( ) func TestCollectPlugins(t *testing.T) { - env := &cli.EnvSettings{ - PluginsDirectory: pluginDir, - } + env := cli.New() + env.PluginsDirectory = pluginDir + p, err := collectPlugins(env) if err != nil { t.Fatal(err) @@ -54,9 +54,8 @@ func TestPluginGetter(t *testing.T) { t.Skip("TODO: refactor this test to work on windows") } - env := &cli.EnvSettings{ - PluginsDirectory: pluginDir, - } + env := cli.New() + env.PluginsDirectory = pluginDir pg := NewPluginGetter("echo", env, "test", ".") g, err := pg() if err != nil { @@ -80,9 +79,9 @@ func TestPluginSubCommands(t *testing.T) { t.Skip("TODO: refactor this test to work on windows") } - env := &cli.EnvSettings{ - PluginsDirectory: pluginDir, - } + env := cli.New() + env.PluginsDirectory = pluginDir + pg := NewPluginGetter("echo -n", env, "test", ".") g, err := pg() if err != nil { diff --git a/pkg/getter/testdata/empty-0.0.1.tgz b/pkg/getter/testdata/empty-0.0.1.tgz new file mode 100644 index 000000000..6c4c3d205 Binary files /dev/null and b/pkg/getter/testdata/empty-0.0.1.tgz differ diff --git a/pkg/helmpath/lazypath.go b/pkg/helmpath/lazypath.go index 0b9068671..22d7bf0a1 100644 --- a/pkg/helmpath/lazypath.go +++ b/pkg/helmpath/lazypath.go @@ -20,11 +20,34 @@ import ( "helm.sh/helm/v3/pkg/helmpath/xdg" ) +const ( + // CacheHomeEnvVar is the environment variable used by Helm + // for the cache directory. When no value is set a default is used. + CacheHomeEnvVar = "HELM_CACHE_HOME" + + // ConfigHomeEnvVar is the environment variable used by Helm + // for the config directory. When no value is set a default is used. + ConfigHomeEnvVar = "HELM_CONFIG_HOME" + + // DataHomeEnvVar is the environment variable used by Helm + // for the data directory. When no value is set a default is used. + DataHomeEnvVar = "HELM_DATA_HOME" +) + // lazypath is an lazy-loaded path buffer for the XDG base directory specification. type lazypath string -func (l lazypath) path(envVar string, defaultFn func() string, elem ...string) string { - base := os.Getenv(envVar) +func (l lazypath) path(helmEnvVar, xdgEnvVar string, defaultFn func() string, elem ...string) string { + + // There is an order to checking for a path. + // 1. See if a Helm specific environment variable has been set. + // 2. Check if an XDG environment variable is set + // 3. Fall back to a default + base := os.Getenv(helmEnvVar) + if base != "" { + return filepath.Join(base, filepath.Join(elem...)) + } + base = os.Getenv(xdgEnvVar) if base == "" { base = defaultFn() } @@ -34,16 +57,16 @@ func (l lazypath) path(envVar string, defaultFn func() string, elem ...string) s // cachePath defines the base directory relative to which user specific non-essential data files // should be stored. func (l lazypath) cachePath(elem ...string) string { - return l.path(xdg.CacheHomeEnvVar, cacheHome, filepath.Join(elem...)) + return l.path(CacheHomeEnvVar, xdg.CacheHomeEnvVar, cacheHome, filepath.Join(elem...)) } // configPath defines the base directory relative to which user specific configuration files should // be stored. func (l lazypath) configPath(elem ...string) string { - return l.path(xdg.ConfigHomeEnvVar, configHome, filepath.Join(elem...)) + return l.path(ConfigHomeEnvVar, xdg.ConfigHomeEnvVar, configHome, filepath.Join(elem...)) } // dataPath defines the base directory relative to which user specific data files should be stored. func (l lazypath) dataPath(elem ...string) string { - return l.path(xdg.DataHomeEnvVar, dataHome, filepath.Join(elem...)) + return l.path(DataHomeEnvVar, xdg.DataHomeEnvVar, dataHome, filepath.Join(elem...)) } diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 77e0526b9..0f3740e81 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -95,10 +95,17 @@ var nopLogger = func(_ string, _ ...interface{}) {} // IsReachable tests connectivity to the cluster func (c *Client) IsReachable() error { - client, _ := c.Factory.KubernetesClientSet() - _, err := client.ServerVersion() + client, err := c.Factory.KubernetesClientSet() + if err == genericclioptions.ErrEmptyConfig { + // re-replace kubernetes ErrEmptyConfig error with a friendy error + // moar workarounds for Kubernetes API breaking. + return errors.New("Kubernetes cluster unreachable") + } if err != nil { - return fmt.Errorf("Kubernetes cluster unreachable: %s", err.Error()) + return errors.Wrap(err, "Kubernetes cluster unreachable") + } + if _, err := client.ServerVersion(); err != nil { + return errors.Wrap(err, "Kubernetes cluster unreachable") } return nil } @@ -379,6 +386,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err if err := info.Get(); err != nil { c.Log("Unable to get obj %q, err: %s", info.Name, err) + continue } annotations, err := metadataAccessor.Annotations(info.Object) if err != nil { @@ -388,16 +396,11 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) continue } - - res.Deleted = append(res.Deleted, info) if err := deleteResource(info); err != nil { - if apierrors.IsNotFound(err) { - c.Log("Attempted to delete %q, but the resource was missing", info.Name) - } else { - c.Log("Failed to delete %q, err: %s", info.Name, err) - return res, errors.Wrapf(err, "Failed to delete %q", info.Name) - } + c.Log("Failed to delete %q, err: %s", info.ObjectName(), err) + continue } + res.Deleted = append(res.Deleted, info) } return res, nil } @@ -409,12 +412,17 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err func (c *Client) Delete(resources ResourceList) (*Result, []error) { var errs []error res := &Result{} + mtx := sync.Mutex{} err := perform(resources, func(info *resource.Info) error { c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) if err := c.skipIfNotFound(deleteResource(info)); err != nil { + mtx.Lock() + defer mtx.Unlock() // Collect the error and continue on errs = append(errs, err) } else { + mtx.Lock() + defer mtx.Unlock() res.Deleted = append(res.Deleted, info) } return nil @@ -502,7 +510,7 @@ func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- } func createResource(info *resource.Info) error { - obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil) + obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) if err != nil { return err } @@ -594,7 +602,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, if err != nil { return errors.Wrap(err, "failed to replace object") } - c.Log("Replaced %q with kind %s for kind %s\n", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) + c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) } else { // send patch to server obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil) @@ -726,9 +734,12 @@ func scrubValidationError(err error) error { // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase // and returns said phase (PodSucceeded or PodFailed qualify). func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) { - client, _ := c.Factory.KubernetesClientSet() + client, err := c.Factory.KubernetesClientSet() + if err != nil { + return v1.PodUnknown, err + } to := int64(timeout) - watcher, err := client.CoreV1().Pods(c.namespace()).Watch(metav1.ListOptions{ + watcher, err := client.CoreV1().Pods(c.namespace()).Watch(context.Background(), metav1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", name), TimeoutSeconds: &to, }) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index aa081423c..568afa094 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -164,9 +164,21 @@ func TestUpdate(t *testing.T) { t.Fatal(err) } - if _, err := c.Update(first, second, false); err != nil { + result, err := c.Update(first, second, false) + if err != nil { t.Fatal(err) } + + if len(result.Created) != 1 { + t.Errorf("expected 1 resource created, got %d", len(result.Created)) + } + if len(result.Updated) != 2 { + t.Errorf("expected 2 resource updated, got %d", len(result.Updated)) + } + if len(result.Deleted) != 1 { + t.Errorf("expected 1 resource deleted, got %d", len(result.Deleted)) + } + // TODO: Find a way to test methods that use Client Set // Test with a wait // if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil { @@ -190,8 +202,7 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods/squid:DELETE", } if len(expectedActions) != len(actions) { - t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) - return + t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) } for k, v := range expectedActions { if actions[k] != v { diff --git a/pkg/kube/config.go b/pkg/kube/config.go index 624c4a1f7..e00c9acb1 100644 --- a/pkg/kube/config.go +++ b/pkg/kube/config.go @@ -19,6 +19,8 @@ package kube // import "helm.sh/helm/v3/pkg/kube" import "k8s.io/cli-runtime/pkg/genericclioptions" // GetConfig returns a Kubernetes client config. +// +// Deprecated func GetConfig(kubeconfig, context, namespace string) *genericclioptions.ConfigFlags { cf := genericclioptions.NewConfigFlags(true) cf.Namespace = &namespace diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index 0254a60bb..90a9f8b38 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -17,6 +17,7 @@ limitations under the License. package kube // import "helm.sh/helm/v3/pkg/kube" import ( + "context" "fmt" "time" @@ -62,12 +63,12 @@ func (w *waiter) waitForResources(created ResourceList) error { ) switch value := AsVersioned(v).(type) { case *corev1.Pod: - pod, err := w.c.CoreV1().Pods(v.Namespace).Get(v.Name, metav1.GetOptions{}) + pod, err := w.c.CoreV1().Pods(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil || !w.isPodReady(pod) { return false, err } case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment: - currentDeployment, err := w.c.AppsV1().Deployments(v.Namespace).Get(v.Name, metav1.GetOptions{}) + currentDeployment, err := w.c.AppsV1().Deployments(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } @@ -84,7 +85,7 @@ func (w *waiter) waitForResources(created ResourceList) error { return false, nil } case *corev1.PersistentVolumeClaim: - claim, err := w.c.CoreV1().PersistentVolumeClaims(v.Namespace).Get(v.Name, metav1.GetOptions{}) + claim, err := w.c.CoreV1().PersistentVolumeClaims(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } @@ -92,7 +93,7 @@ func (w *waiter) waitForResources(created ResourceList) error { return false, nil } case *corev1.Service: - svc, err := w.c.CoreV1().Services(v.Namespace).Get(v.Name, metav1.GetOptions{}) + svc, err := w.c.CoreV1().Services(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } @@ -100,7 +101,7 @@ func (w *waiter) waitForResources(created ResourceList) error { return false, nil } case *extensionsv1beta1.DaemonSet, *appsv1.DaemonSet, *appsv1beta2.DaemonSet: - ds, err := w.c.AppsV1().DaemonSets(v.Namespace).Get(v.Name, metav1.GetOptions{}) + ds, err := w.c.AppsV1().DaemonSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } @@ -130,7 +131,7 @@ func (w *waiter) waitForResources(created ResourceList) error { return false, nil } case *appsv1.StatefulSet, *appsv1beta1.StatefulSet, *appsv1beta2.StatefulSet: - sts, err := w.c.AppsV1().StatefulSets(v.Namespace).Get(v.Name, metav1.GetOptions{}) + sts, err := w.c.AppsV1().StatefulSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } @@ -337,7 +338,7 @@ func (w *waiter) statefulSetReady(sts *appsv1.StatefulSet) bool { } func getPods(client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) { - list, err := client.CoreV1().Pods(namespace).List(metav1.ListOptions{ + list, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: selector, }) return list.Items, err diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 2a982d088..e7ff4cd7a 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -17,9 +17,12 @@ limitations under the License. package lint import ( + "io/ioutil" + "os" "strings" "testing" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/lint/support" ) @@ -93,7 +96,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) } } @@ -101,6 +104,39 @@ func TestBadValues(t *testing.T) { func TestGoodChart(t *testing.T) { m := All(goodChartDir, values, namespace, strict).Messages if len(m) != 0 { - t.Errorf("All failed but shouldn't have: %#v", m) + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + dir, err := ioutil.TempDir("", "-helm-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := All(createdChart, values, namespace, true).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) } } diff --git a/pkg/lint/rules/deprecations.go b/pkg/lint/rules/deprecations.go new file mode 100644 index 000000000..88921408d --- /dev/null +++ b/pkg/lint/rules/deprecations.go @@ -0,0 +1,80 @@ +/* +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 "helm.sh/helm/v3/pkg/lint/rules" + +import "fmt" + +// deprecatedAPIs lists APIs that are deprecated (left) with suggested alternatives (right). +// +// An empty rvalue indicates that the API is completely deprecated. +var deprecatedAPIs = map[string]string{ + "extensions/v1beta1 Deployment": "apps/v1 Deployment", + "extensions/v1beta1 DaemonSet": "apps/v1 DaemonSet", + "extensions/v1beta1 ReplicaSet": "apps/v1 ReplicaSet", + "extensions/v1beta1 PodSecurityPolicy": "policy/v1beta1 PodSecurityPolicy", + "extensions/v1beta1 NetworkPolicy": "networking.k8s.io/v1beta1 NetworkPolicy", + "extensions/v1beta1 Ingress": "networking.k8s.io/v1beta1 Ingress", + "apps/v1beta1 Deployment": "apps/v1 Deployment", + "apps/v1beta1 StatefulSet": "apps/v1 StatefulSet", + "apps/v1beta1 ReplicaSet": "apps/v1 ReplicaSet", + "apps/v1beta2 Deployment": "apps/v1 Deployment", + "apps/v1beta2 StatefulSet": "apps/v1 StatefulSet", + "apps/v1beta2 DaemonSet": "apps/v1 DaemonSet", + "apps/v1beta2 ReplicaSet": "apps/v1 ReplicaSet", + "apiextensions.k8s.io/v1beta1 CustomResourceDefinition": "apiextensions.k8s.io/v1 CustomResourceDefinition", + "rbac.authorization.k8s.io/v1alpha1 ClusterRole": "rbac.authorization.k8s.io/v1 ClusterRole", + "rbac.authorization.k8s.io/v1alpha1 ClusterRoleList": "rbac.authorization.k8s.io/v1 ClusterRoleList", + "rbac.authorization.k8s.io/v1alpha1 ClusterRoleBinding": "rbac.authorization.k8s.io/v1 ClusterRoleBinding", + "rbac.authorization.k8s.io/v1alpha1 ClusterRoleBindingList": "rbac.authorization.k8s.io/v1 ClusterRoleBindingList", + "rbac.authorization.k8s.io/v1alpha1 Role": "rbac.authorization.k8s.io/v1 Role", + "rbac.authorization.k8s.io/v1alpha1 RoleList": "rbac.authorization.k8s.io/v1 RoleList", + "rbac.authorization.k8s.io/v1alpha1 RoleBinding": "rbac.authorization.k8s.io/v1 RoleBinding", + "rbac.authorization.k8s.io/v1alpha1 RoleBindingList": "rbac.authorization.k8s.io/v1 RoleBindingList", + "rbac.authorization.k8s.io/v1beta1 ClusterRole": "rbac.authorization.k8s.io/v1 ClusterRole", + "rbac.authorization.k8s.io/v1beta1 ClusterRoleList": "rbac.authorization.k8s.io/v1 ClusterRoleList", + "rbac.authorization.k8s.io/v1beta1 ClusterRoleBinding": "rbac.authorization.k8s.io/v1 ClusterRoleBinding", + "rbac.authorization.k8s.io/v1beta1 ClusterRoleBindingList": "rbac.authorization.k8s.io/v1 ClusterRoleBindingList", + "rbac.authorization.k8s.io/v1beta1 Role": "rbac.authorization.k8s.io/v1 Role", + "rbac.authorization.k8s.io/v1beta1 RoleList": "rbac.authorization.k8s.io/v1 RoleList", + "rbac.authorization.k8s.io/v1beta1 RoleBinding": "rbac.authorization.k8s.io/v1 RoleBinding", + "rbac.authorization.k8s.io/v1beta1 RoleBindingList": "rbac.authorization.k8s.io/v1 RoleBindingList", +} + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Alternative string +} + +func (e deprecatedAPIError) Error() string { + msg := fmt.Sprintf("the kind %q is deprecated", e.Deprecated) + if e.Alternative != "" { + msg += fmt.Sprintf(" in favor of %q", e.Alternative) + } + return msg +} + +func validateNoDeprecations(resource *K8sYamlStruct) error { + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + if alt, ok := deprecatedAPIs[gvk]; ok { + return deprecatedAPIError{ + Deprecated: gvk, + Alternative: alt, + } + } + return nil +} diff --git a/pkg/lint/rules/deprecations_test.go b/pkg/lint/rules/deprecations_test.go new file mode 100644 index 000000000..1e8d34702 --- /dev/null +++ b/pkg/lint/rules/deprecations_test.go @@ -0,0 +1,42 @@ +/* +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 "helm.sh/helm/v3/pkg/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &K8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + + depErr := err.(deprecatedAPIError) + if depErr.Alternative != "apps/v1 Deployment" { + t.Errorf("Expected %q to be replaced by %q", depErr.Deprecated, depErr.Alternative) + } + + if err := validateNoDeprecations(&K8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 3d388f81b..787c5b26a 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -17,9 +17,11 @@ limitations under the License. package rules import ( + "fmt" "os" "path/filepath" "regexp" + "strings" "github.com/pkg/errors" "sigs.k8s.io/yaml" @@ -35,6 +37,14 @@ var ( releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`) ) +// validName is a regular expression for names. +// +// This is different than action.ValidName. It conforms to the regular expression +// `kubectl` says it uses, plus it disallows empty names. +// +// For details, see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + // Templates lints the templates in the Linter. func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) { path := "templates/" @@ -57,7 +67,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace } options := chartutil.ReleaseOptions{ - Name: "testRelease", + Name: "test-release", Namespace: namespace, } @@ -71,7 +81,6 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace return } var e engine.Engine - e.Strict = strict e.LintMode = true renderedContentMap, err := e.Render(chart, valuesToRender) @@ -111,14 +120,18 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace // linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate))) renderedContent := renderedContentMap[filepath.Join(chart.Name(), fileName)] - var yamlStruct K8sYamlStruct - // Even though K8sYamlStruct only defines Metadata namespace, an error in any other - // key will be raised as well - err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) - - // 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)) + if strings.TrimSpace(renderedContent) != "" { + var yamlStruct K8sYamlStruct + // Even though K8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) + + // 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)) + linter.RunLinterRule(support.ErrorSev, path, validateMetadataName(&yamlStruct)) + linter.RunLinterRule(support.ErrorSev, path, validateNoDeprecations(&yamlStruct)) + } } } @@ -149,6 +162,15 @@ func validateYamlContent(err error) error { return errors.Wrap(err, "unable to parse YAML") } +func validateMetadataName(obj *K8sYamlStruct) error { + // This will return an error if the characters do not abide by the standard OR if the + // name is left empty. + if validName.MatchString(obj.Metadata.Name) { + return nil + } + return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name) +} + func validateNoCRDHooks(manifest []byte) error { if crdHookSearch.Match(manifest) { return errors.New("manifest is a crd-install hook. This hook is no longer supported in v3 and all CRDs should also exist the crds/ directory at the top level of the chart") @@ -164,9 +186,16 @@ func validateNoReleaseTime(manifest []byte) error { } // K8sYamlStruct stubs a Kubernetes YAML file. -// Need to access for now to Namespace only +// +// DEPRECATED: In Helm 4, this will be made a private type, as it is for use only within +// the rules package. type K8sYamlStruct struct { - Metadata struct { - Namespace string - } + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string } diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index ddb46aba0..991c6c2f6 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -22,6 +22,9 @@ import ( "strings" "testing" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/lint/support" ) @@ -101,3 +104,125 @@ func TestV3Fail(t *testing.T) { t.Errorf("Unexpected error: %s", res[2].Err) } } + +func TestValidateMetadataName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + } + for input, expectPass := range names { + obj := K8sYamlStruct{Metadata: k8sYamlMetadata{Name: input}} + if err := validateMetadataName(&obj); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + if err != nil { + t.Log(err) + } + } + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*chart.File{ + { + Name: "templates/baddeployment.yaml", + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep"), + }, + { + Name: "templates/goodsecret.yaml", + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := ensure.TempDir(t) + defer os.RemoveAll(tmpdir) + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestSTrictTemplatePrasingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*chart.File{ + { + Name: "templates/configmap.yaml", + Data: []byte(manifest), + }, + }, + } + dir := ensure.TempDir(t) + defer os.RemoveAll(dir) + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates(linter, ch.Values, namespace, strict) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} diff --git a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/lint/rules/testdata/goodone/templates/goodone.yaml index 0e77f46f2..cd46f62c7 100644 --- a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml +++ b/pkg/lint/rules/testdata/goodone/templates/goodone.yaml @@ -1,2 +1,2 @@ metadata: - name: {{.Values.name | default "foo" | title}} + name: {{ .Values.name | default "foo" | lower }} diff --git a/pkg/lint/rules/testdata/goodone/values.yaml b/pkg/lint/rules/testdata/goodone/values.yaml index fe9abd983..92c3d9bb9 100644 --- a/pkg/lint/rules/testdata/goodone/values.yaml +++ b/pkg/lint/rules/testdata/goodone/values.yaml @@ -1 +1 @@ -name: "goodone here" +name: "goodone-here" 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/plugin/installer/base.go b/pkg/plugin/installer/base.go index a8ec97416..dcc3ad644 100644 --- a/pkg/plugin/installer/base.go +++ b/pkg/plugin/installer/base.go @@ -16,7 +16,6 @@ limitations under the License. package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( - "os" "path/filepath" "helm.sh/helm/v3/pkg/helmpath" @@ -31,13 +30,7 @@ func newBase(source string) base { return base{source} } -// link creates a symlink from the plugin source to the base path. -func (b *base) link(from string) error { - debug("symlinking %s to %s", from, b.Path()) - return os.Symlink(from, b.Path()) -} - -// Path is where the plugin will be symlinked to. +// Path is where the plugin will be installed. func (b *base) Path() string { if b.Source == "" { return "" diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index ea4ac7bcd..629bbec39 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -27,6 +27,7 @@ import ( "github.com/pkg/errors" + "helm.sh/helm/v3/internal/third_party/dep/fs" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/helmpath" @@ -68,7 +69,6 @@ func NewExtractor(source string) (Extractor, error) { // NewHTTPInstaller creates a new HttpInstaller. func NewHTTPInstaller(source string) (*HTTPInstaller, error) { - key, err := cache.Key(source) if err != nil { return nil, err @@ -108,18 +108,16 @@ func stripPluginName(name string) string { } // Install downloads and extracts the tarball into the cache directory -// and creates a symlink to the plugin directory. +// and installs into the plugin directory. // // Implements Installer. func (i *HTTPInstaller) Install() error { - pluginData, err := i.getter.Get(i.Source) if err != nil { return err } - err = i.extractor.Extract(pluginData, i.CacheDir) - if err != nil { + if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil { return err } @@ -132,7 +130,8 @@ func (i *HTTPInstaller) Install() error { return err } - return i.link(src) + debug("copying %s to %s", src, i.Path()) + return fs.CopyDir(src, i.Path()) } // Update updates a local repository @@ -141,12 +140,6 @@ func (i *HTTPInstaller) Update() error { return errors.Errorf("method Update() not implemented for HttpInstaller") } -// Override link because we want to use HttpInstaller.Path() not base.Path() -func (i *HTTPInstaller) link(from string) error { - debug("symlinking %s to %s", from, i.Path()) - return os.Symlink(from, i.Path()) -} - // Path is overridden because we want to join on the plugin name not the file name func (i HTTPInstaller) Path() string { if i.base.Source == "" { @@ -164,17 +157,16 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { return err } - tarReader := tar.NewReader(uncompressedStream) - - os.MkdirAll(targetDir, 0755) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return err + } + tarReader := tar.NewReader(uncompressedStream) for { header, err := tarReader.Next() - if err == io.EOF { break } - if err != nil { return err } @@ -200,7 +192,5 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) } } - return nil - } diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index cfa2e4cbe..b496a1b01 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -73,13 +73,13 @@ func TestHTTPInstaller(t *testing.T) { i, err := NewForSource(source, "0.0.1") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with minimal plugin tarball @@ -94,17 +94,17 @@ func TestHTTPInstaller(t *testing.T) { // install the plugin if err := Install(i); err != nil { - t.Error(err) + t.Fatal(err) } if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { - t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) } // Install again to test plugin exists error if err := Install(i); err == nil { - t.Error("expected error for plugin exists, got none") + t.Fatal("expected error for plugin exists, got none") } else if err.Error() != "plugin already exists" { - t.Errorf("expected error for plugin exists, got (%v)", err) + t.Fatalf("expected error for plugin exists, got (%v)", err) } } @@ -119,13 +119,13 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) { i, err := NewForSource(source, "0.0.2") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with error @@ -135,7 +135,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) { // attempt to install the plugin if err := Install(i); err == nil { - t.Error("expected error from http client") + t.Fatal("expected error from http client") } } @@ -150,13 +150,13 @@ func TestHTTPInstallerUpdate(t *testing.T) { i, err := NewForSource(source, "0.0.1") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with minimal plugin tarball @@ -171,15 +171,15 @@ func TestHTTPInstallerUpdate(t *testing.T) { // install the plugin before updating if err := Install(i); err != nil { - t.Error(err) + t.Fatal(err) } if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { - t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) } // Update plugin, should fail because it is not implemented if err := Update(i); err == nil { - t.Error("update method not implemented for http installer") + t.Fatal("update method not implemented for http installer") } } @@ -240,29 +240,27 @@ func TestExtract(t *testing.T) { } if err = extractor.Extract(&buf, tempDir); err != nil { - t.Errorf("Did not expect error but got error: %v", err) + t.Fatalf("Did not expect error but got error: %v", err) } pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml") if info, err := os.Stat(pluginYAMLFullPath); err != nil { if os.IsNotExist(err) { - t.Errorf("Expected %s to exist but doesn't", pluginYAMLFullPath) - } else { - t.Error(err) + t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath) } + t.Fatal(err) } else if info.Mode().Perm() != 0600 { - t.Errorf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm()) + t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm()) } readmeFullPath := filepath.Join(tempDir, "README.md") if info, err := os.Stat(readmeFullPath); err != nil { if os.IsNotExist(err) { - t.Errorf("Expected %s to exist but doesn't", readmeFullPath) - } else { - t.Error(err) + t.Fatalf("Expected %s to exist but doesn't", readmeFullPath) } + t.Fatal(err) } else if info.Mode().Perm() != 0777 { - t.Errorf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm()) + t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm()) } } diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 14a02a87e..61b49ab3b 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -17,11 +17,14 @@ package installer import ( "fmt" + "log" "os" "path/filepath" "strings" "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/plugin" ) // ErrMissingMetadata indicates that plugin.yaml is missing. @@ -99,13 +102,14 @@ func isRemoteHTTPArchive(source string) bool { // isPlugin checks if the directory contains a plugin.yaml file. func isPlugin(dirname string) bool { - _, err := os.Stat(filepath.Join(dirname, "plugin.yaml")) + _, err := os.Stat(filepath.Join(dirname, plugin.PluginFileName)) return err == nil } +var logger = log.New(os.Stderr, "[debug] ", log.Lshortfile) + func debug(format string, args ...interface{}) { if Debug { - format = fmt.Sprintf("[debug] %s\n", format) - fmt.Printf(format, args...) + logger.Output(2, fmt.Sprintf(format, args...)) } } diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index 662ef5b29..c92bc3fb0 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -16,6 +16,7 @@ limitations under the License. package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( + "os" "path/filepath" "github.com/pkg/errors" @@ -45,7 +46,8 @@ func (i *LocalInstaller) Install() error { if !isPlugin(i.Source) { return ErrMissingMetadata } - return i.link(i.Source) + debug("symlinking %s to %s", i.Source, i.Path()) + return os.Symlink(i.Source, i.Path()) } // Update updates a local repository diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index d11e44860..3d9607331 100644 --- a/pkg/plugin/installer/local_installer_test.go +++ b/pkg/plugin/installer/local_installer_test.go @@ -40,7 +40,7 @@ func TestLocalInstaller(t *testing.T) { source := "../testdata/plugdir/echo" i, err := NewForSource(source, "") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := Install(i); err != nil { @@ -48,6 +48,6 @@ func TestLocalInstaller(t *testing.T) { } if i.Path() != helmpath.DataPath("plugins", "echo") { - t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) } } diff --git a/pkg/plugin/installer/vcs_installer.go b/pkg/plugin/installer/vcs_installer.go index 1a5d74cca..f7df5b322 100644 --- a/pkg/plugin/installer/vcs_installer.go +++ b/pkg/plugin/installer/vcs_installer.go @@ -23,6 +23,7 @@ import ( "github.com/Masterminds/vcs" "github.com/pkg/errors" + "helm.sh/helm/v3/internal/third_party/dep/fs" "helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/plugin/cache" ) @@ -43,7 +44,7 @@ func existingVCSRepo(location string) (Installer, error) { Repo: repo, base: newBase(repo.Remote()), } - return i, err + return i, nil } // NewVCSInstaller creates a new VCSInstaller. @@ -65,7 +66,7 @@ func NewVCSInstaller(source, version string) (*VCSInstaller, error) { return i, err } -// Install clones a remote repository and creates a symlink to the plugin directory. +// Install clones a remote repository and installs into the plugin directory. // // Implements Installer. func (i *VCSInstaller) Install() error { @@ -87,7 +88,8 @@ func (i *VCSInstaller) Install() error { return ErrMissingMetadata } - return i.link(i.Repo.LocalPath()) + debug("copying %s to %s", i.Repo.LocalPath(), i.Path()) + return fs.CopyDir(i.Repo.LocalPath(), i.Path()) } // Update updates a remote repository diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index ce1ee609e..b8dc6b1e2 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -80,24 +80,24 @@ func TestVCSInstaller(t *testing.T) { t.Fatal(err) } if repo.current != "0.1.1" { - t.Errorf("expected version '0.1.1', got %q", repo.current) + t.Fatalf("expected version '0.1.1', got %q", repo.current) } if i.Path() != helmpath.DataPath("plugins", "helm-env") { - t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) } // Install again to test plugin exists error if err := Install(i); err == nil { - t.Error("expected error for plugin exists, got none") + t.Fatalf("expected error for plugin exists, got none") } else if err.Error() != "plugin already exists" { - t.Errorf("expected error for plugin exists, got (%v)", err) + t.Fatalf("expected error for plugin exists, got (%v)", err) } // Testing FindSource method, expect error because plugin code is not a cloned repository if _, err := FindSource(i.Path()); err == nil { - t.Error("expected error for inability to find plugin source, got none") + t.Fatalf("expected error for inability to find plugin source, got none") } else if err.Error() != "cannot get information about plugin source" { - t.Errorf("expected error for inability to find plugin source, got (%v)", err) + t.Fatalf("expected error for inability to find plugin source, got (%v)", err) } } @@ -113,15 +113,14 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) { } // ensure a VCSInstaller was returned - _, ok := i.(*VCSInstaller) - if !ok { + if _, ok := i.(*VCSInstaller); !ok { t.Fatal("expected a VCSInstaller") } if err := Install(i); err == nil { - t.Error("expected error for version does not exists, got none") + t.Fatalf("expected error for version does not exists, got none") } else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) { - t.Errorf("expected error for version does not exists, got (%v)", err) + t.Fatalf("expected error for version does not exists, got (%v)", err) } } func TestVCSInstallerUpdate(t *testing.T) { @@ -135,8 +134,7 @@ func TestVCSInstallerUpdate(t *testing.T) { } // ensure a VCSInstaller was returned - _, ok := i.(*VCSInstaller) - if !ok { + if _, ok := i.(*VCSInstaller); !ok { t.Fatal("expected a VCSInstaller") } @@ -157,7 +155,9 @@ func TestVCSInstallerUpdate(t *testing.T) { t.Fatal(err) } - repoRemote := pluginInfo.(*VCSInstaller).Repo.Remote() + vcsInstaller := pluginInfo.(*VCSInstaller) + + repoRemote := vcsInstaller.Repo.Remote() if repoRemote != source { t.Fatalf("invalid source found, expected %q got %q", source, repoRemote) } @@ -168,12 +168,14 @@ func TestVCSInstallerUpdate(t *testing.T) { } // Test update failure - os.Remove(filepath.Join(i.Path(), "plugin.yaml")) + if err := os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")); err != nil { + t.Fatal(err) + } // Testing update for error - if err := Update(i); err == nil { - t.Error("expected error for plugin modified, got none") + if err := Update(vcsInstaller); err == nil { + t.Fatalf("expected error for plugin modified, got none") } else if err.Error() != "plugin repo was modified" { - t.Errorf("expected error for plugin modified, got (%v)", err) + t.Fatalf("expected error for plugin modified, got (%v)", err) } } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 2eb354fca..caa34fbd3 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -28,7 +28,7 @@ import ( "helm.sh/helm/v3/pkg/cli" ) -const pluginFileName = "plugin.yaml" +const PluginFileName = "plugin.yaml" // Downloaders represents the plugins capability if it can retrieve // charts from special sources @@ -159,7 +159,7 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { // LoadDir loads a plugin from the given directory. func LoadDir(dirname string) (*Plugin, error) { - data, err := ioutil.ReadFile(filepath.Join(dirname, pluginFileName)) + data, err := ioutil.ReadFile(filepath.Join(dirname, PluginFileName)) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func LoadDir(dirname string) (*Plugin, error) { func LoadAll(basedir string) ([]*Plugin, error) { plugins := []*Plugin{} // We want basedir/*/plugin.yaml - scanpath := filepath.Join(basedir, "*", pluginFileName) + scanpath := filepath.Join(basedir, "*", PluginFileName) matches, err := filepath.Glob(scanpath) if err != nil { return plugins, err diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index c869e4c86..af0b61846 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -28,14 +28,14 @@ import ( func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) { cmd, args, err := p.PrepareCommand(extraArgs) if err != nil { - t.Errorf(err.Error()) + t.Fatal(err) } if cmd != "echo" { - t.Errorf("Expected echo, got %q", cmd) + t.Fatalf("Expected echo, got %q", cmd) } if l := len(args); l != 5 { - t.Errorf("expected 5 args, got %d", l) + t.Fatalf("expected 5 args, got %d", l) } expect := []string{"-n", osStrCmp, "--debug", "--foo", "bar"} @@ -49,13 +49,13 @@ func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) p.Metadata.IgnoreFlags = true cmd, args, err = p.PrepareCommand(extraArgs) if err != nil { - t.Errorf(err.Error()) + t.Fatal(err) } if cmd != "echo" { - t.Errorf("Expected echo, got %q", cmd) + t.Fatalf("Expected echo, got %q", cmd) } if l := len(args); l != 2 { - t.Errorf("expected 2 args, got %d", l) + t.Fatalf("expected 2 args, got %d", l) } expect = []string{"-n", osStrCmp} for i := 0; i < len(args); i++ { @@ -155,7 +155,7 @@ func TestNoPrepareCommand(t *testing.T) { _, _, err := p.PrepareCommand(argv) if err == nil { - t.Errorf("Expected error to be returned") + t.Fatalf("Expected error to be returned") } } @@ -172,7 +172,7 @@ func TestNoMatchPrepareCommand(t *testing.T) { argv := []string{"--debug", "--foo", "bar"} if _, _, err := p.PrepareCommand(argv); err == nil { - t.Errorf("Expected error to be returned") + t.Fatalf("Expected error to be returned") } } @@ -184,7 +184,7 @@ func TestLoadDir(t *testing.T) { } if plug.Dir != dirname { - t.Errorf("Expected dir %q, got %q", dirname, plug.Dir) + t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) } expect := &Metadata{ @@ -200,7 +200,7 @@ func TestLoadDir(t *testing.T) { } if !reflect.DeepEqual(expect, plug.Metadata) { - t.Errorf("Expected plugin metadata %v, got %v", expect, plug.Metadata) + t.Fatalf("Expected plugin metadata %v, got %v", expect, plug.Metadata) } } @@ -212,7 +212,7 @@ func TestDownloader(t *testing.T) { } if plug.Dir != dirname { - t.Errorf("Expected dir %q, got %q", dirname, plug.Dir) + t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) } expect := &Metadata{ @@ -230,7 +230,7 @@ func TestDownloader(t *testing.T) { } if !reflect.DeepEqual(expect, plug.Metadata) { - t.Errorf("Expected metadata %v, got %v", expect, plug.Metadata) + t.Fatalf("Expected metadata %v, got %v", expect, plug.Metadata) } } @@ -264,13 +264,49 @@ func TestLoadAll(t *testing.T) { } } +func TestFindPlugins(t *testing.T) { + cases := []struct { + name string + plugdirs string + expected int + }{ + { + name: "plugdirs is empty", + plugdirs: "", + expected: 0, + }, + { + name: "plugdirs isn't dir", + plugdirs: "./plugin_test.go", + expected: 0, + }, + { + name: "plugdirs doens't have plugin", + plugdirs: ".", + expected: 0, + }, + { + name: "normal", + plugdirs: "./testdata/plugdir", + expected: 3, + }, + } + for _, c := range cases { + t.Run(t.Name(), func(t *testing.T) { + plugin, _ := FindPlugins(c.plugdirs) + if len(plugin) != c.expected { + t.Errorf("expected: %v, got: %v", c.expected, len(plugin)) + } + }) + } +} + func TestSetupEnv(t *testing.T) { name := "pequod" base := filepath.Join("testdata/helmhome/helm/plugins", name) - s := &cli.EnvSettings{ - PluginsDirectory: "testdata/helmhome/helm/plugins", - } + s := cli.New() + s.PluginsDirectory = "testdata/helmhome/helm/plugins" SetupPluginEnv(s, name, base) for _, tt := range []struct { diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index 92ffa03f2..5b131b3b0 100644 --- a/pkg/releaseutil/kind_sorter.go +++ b/pkg/releaseutil/kind_sorter.go @@ -16,7 +16,11 @@ limitations under the License. package releaseutil -import "sort" +import ( + "sort" + + "helm.sh/helm/v3/pkg/release" +) // KindSortOrder is an ordering of Kinds. type KindSortOrder []string @@ -99,46 +103,42 @@ var UninstallOrder KindSortOrder = []string{ "Namespace", } -// sortByKind does an in-place sort of manifests by Kind. +// sort manifests by kind. // // Results are sorted by 'ordering', keeping order of items with equal kind/priority -func sortByKind(manifests []Manifest, ordering KindSortOrder) []Manifest { - ks := newKindSorter(manifests, ordering) - sort.Stable(ks) - return ks.manifests -} +func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest { + sort.SliceStable(manifests, func(i, j int) bool { + return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering) + }) -type kindSorter struct { - ordering map[string]int - manifests []Manifest + return manifests } -func newKindSorter(m []Manifest, s KindSortOrder) *kindSorter { - o := make(map[string]int, len(s)) - for v, k := range s { - o[k] = v - } +// sort hooks by kind, using an out-of-place sort to preserve the input parameters. +// +// Results are sorted by 'ordering', keeping order of items with equal kind/priority +func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook { + h := hooks + sort.SliceStable(h, func(i, j int) bool { + return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering) + }) - return &kindSorter{ - manifests: m, - ordering: o, - } + return h } -func (k *kindSorter) Len() int { return len(k.manifests) } - -func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } +func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o KindSortOrder) bool { + ordering := make(map[string]int, len(o)) + for v, k := range o { + ordering[k] = v + } -func (k *kindSorter) Less(i, j int) bool { - a := k.manifests[i] - b := k.manifests[j] - first, aok := k.ordering[a.Head.Kind] - second, bok := k.ordering[b.Head.Kind] + first, aok := ordering[kindA] + second, bok := ordering[kindB] if !aok && !bok { // if both are unknown then sort alphabetically by kind, keep original order if same kind - if a.Head.Kind != b.Head.Kind { - return a.Head.Kind < b.Head.Kind + if kindA != kindB { + return kindA < kindB } return first < second } diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go index 4747e8252..341f528a0 100644 --- a/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/releaseutil/kind_sorter_test.go @@ -19,6 +19,8 @@ package releaseutil import ( "bytes" "testing" + + "helm.sh/helm/v3/pkg/release" ) func TestKindSorter(t *testing.T) { @@ -175,12 +177,18 @@ func TestKindSorter(t *testing.T) { t.Fatalf("Expected %d names in order, got %d", want, got) } defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { + orig := manifests + for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { t.Errorf("Expected %q, got %q", test.expected, got) } + for i, manifest := range orig { + if manifest != manifests[i] { + t.Fatal("Expected input to sortManifestsByKind to stay the same") + } + } }) } } @@ -236,7 +244,7 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { + for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { @@ -257,7 +265,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { } manifests := []Manifest{unknown, namespace} - sortByKind(manifests, InstallOrder) + manifests = sortManifestsByKind(manifests, InstallOrder) expectedOrder := []Manifest{namespace, unknown} for i, manifest := range manifests { @@ -266,3 +274,54 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { } } } + +// test hook sorting with a small subset of kinds, since it uses the same algorithm as sortManifestsByKind +func TestKindSorterForHooks(t *testing.T) { + hooks := []*release.Hook{ + { + Name: "i", + Kind: "ClusterRole", + }, + { + Name: "j", + Kind: "ClusterRoleBinding", + }, + { + Name: "c", + Kind: "LimitRange", + }, + { + Name: "a", + Kind: "Namespace", + }, + } + + for _, test := range []struct { + description string + order KindSortOrder + expected string + }{ + {"install", InstallOrder, "acij"}, + {"uninstall", UninstallOrder, "jica"}, + } { + var buf bytes.Buffer + t.Run(test.description, func(t *testing.T) { + if got, want := len(test.expected), len(hooks); got != want { + t.Fatalf("Expected %d names in order, got %d", want, got) + } + defer buf.Reset() + orig := hooks + for _, r := range sortHooksByKind(hooks, test.order) { + buf.WriteString(r.Name) + } + for i, hook := range orig { + if hook != hooks[i] { + t.Fatal("Expected input to sortHooksByKind to stay the same") + } + } + if got := buf.String(); got != test.expected { + t.Errorf("Expected %q, got %q", test.expected, got) + } + }) + } +} diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index 24b0c3c95..e83414500 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -108,7 +108,7 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering } } - return result.hooks, sortByKind(result.generic, ordering), nil + return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil } // sort takes a manifestFile object which may contain multiple resource definition diff --git a/pkg/releaseutil/manifest_sorter_test.go b/pkg/releaseutil/manifest_sorter_test.go index 0d2d6660a..20d809317 100644 --- a/pkg/releaseutil/manifest_sorter_test.go +++ b/pkg/releaseutil/manifest_sorter_test.go @@ -219,7 +219,7 @@ metadata: } } - sorted = sortByKind(sorted, InstallOrder) + sorted = sortManifestsByKind(sorted, InstallOrder) for i, m := range generic { if m.Content != sorted[i].Content { t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) 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_test.go b/pkg/repo/chartrepo_test.go index c6b227acf..f50d6a2b6 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -308,7 +308,7 @@ func TestErrorFindChartInRepoURL(t *testing.T) { if _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", g); err == nil { t.Errorf("Expected error for bad chart URL, but did not get any errors") - } else if !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached: Get http://someserver/something/index.yaml`) { + } else if !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached`) { t.Errorf("Expected error for bad chart URL, but got a different error (%v)", err) } diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 36386665e..6ef2cf8b5 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -17,6 +17,7 @@ limitations under the License. package repo import ( + "bytes" "io/ioutil" "os" "path" @@ -29,6 +30,7 @@ import ( "github.com/pkg/errors" "sigs.k8s.io/yaml" + "helm.sh/helm/v3/internal/fileutil" "helm.sh/helm/v3/internal/urlutil" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -197,7 +199,7 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { if err != nil { return err } - return ioutil.WriteFile(dest, b, mode) + return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode) } // Merge merges the given index file into this index. diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 5dbd5e551..466a2c306 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -428,3 +428,23 @@ func TestIndexAdd(t *testing.T) { t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) } } + +func TestIndexWrite(t *testing.T) { + i := NewIndexFile() + i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + testpath := filepath.Join(dir, "test") + i.WriteFile(testpath, 0600) + + got, err := ioutil.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(got), "clipper-0.1.0.tgz") { + t.Fatal("Index files doesn't contain expected content") + } +} 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..71e635975 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -17,6 +17,7 @@ limitations under the License. package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "context" "strconv" "strings" "time" @@ -62,7 +63,7 @@ func (cfgmaps *ConfigMaps) Name() string { // or error if not found. func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // fetch the configmap holding the release named by key - obj, err := cfgmaps.impl.Get(key, metav1.GetOptions{}) + obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { return nil, ErrReleaseNotFound @@ -88,7 +89,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} - list, err := cfgmaps.impl.List(opts) + list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { cfgmaps.Log("list: failed to list: %s", err) return nil, err @@ -124,7 +125,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} - list, err := cfgmaps.impl.List(opts) + list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { cfgmaps.Log("query: failed to query with labels: %s", err) return nil, err @@ -162,7 +163,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { return err } // push the configmap object out into the kubiverse - if _, err := cfgmaps.impl.Create(obj); err != nil { + if _, err := cfgmaps.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { if apierrors.IsAlreadyExists(err) { return ErrReleaseExists } @@ -189,7 +190,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { return err } // push the configmap object out into the kubiverse - _, err = cfgmaps.impl.Update(obj) + _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) if err != nil { cfgmaps.Log("update: failed to update: %s", err) return err @@ -201,15 +202,10 @@ 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 - if err = cfgmaps.impl.Delete(key, &metav1.DeleteOptions{}); err != nil { + if err = cfgmaps.impl.Delete(context.Background(), key, metav1.DeleteOptions{}); err != nil { return rls, err } return rls, nil diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index a36cee1be..626c36cb9 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -130,6 +130,30 @@ func TestConfigMapList(t *testing.T) { } } +func TestConfigMapQuery(t *testing.T) { + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + }...) + + rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) + if err != nil { + t.Errorf("Failed to query: %s", err) + } + if len(rls) != 2 { + t.Errorf("Expected 2 results, got %d", len(rls)) + } + + _, err = cfgmaps.Query(map[string]string{"name": "notExist"}) + if err != ErrReleaseNotFound { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} + func TestConfigMapCreate(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t) @@ -194,6 +218,12 @@ func TestConfigMapDelete(t *testing.T) { 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 { diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 9a1fbc579..9c01f3766 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -17,6 +17,8 @@ limitations under the License. package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "fmt" + "github.com/pkg/errors" rspb "helm.sh/helm/v3/pkg/release" @@ -28,9 +30,30 @@ var ( // ErrReleaseExists indicates that a release already exists. ErrReleaseExists = errors.New("release: already exists") // ErrInvalidKey indicates that a release key could not be parsed. - ErrInvalidKey = errors.Errorf("release: invalid key") + ErrInvalidKey = errors.New("release: invalid key") + // ErrNoDeployedReleases indicates that there are no releases with the given key in the deployed state + ErrNoDeployedReleases = errors.New("has no deployed releases") ) +// StorageDriverError records an error and the release name that caused it +type StorageDriverError struct { + ReleaseName string + Err error +} + +func (e *StorageDriverError) Error() string { + return fmt.Sprintf("%q %s", e.ReleaseName, e.Err.Error()) +} + +func (e *StorageDriverError) Unwrap() error { return e.Err } + +func NewErrNoDeployedReleases(releaseName string) error { + return &StorageDriverError{ + ReleaseName: releaseName, + Err: ErrNoDeployedReleases, + } +} + // Creator is the interface that wraps the Create method. // // Create stores the release or returns ErrReleaseExists diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index a99b36ef0..91378f588 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -141,6 +141,11 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { break } } + + if len(ls) == 0 { + return nil, ErrReleaseNotFound + } + return ls, nil } diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index 3cb3773c2..c0236ece8 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -17,12 +17,18 @@ limitations under the License. package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "context" "fmt" "testing" + sqlmock "github.com/DATA-DOG/go-sqlmock" + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kblabels "k8s.io/apimachinery/pkg/labels" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" rspb "helm.sh/helm/v3/pkg/release" @@ -102,7 +108,7 @@ func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Releas } // Get returns the ConfigMap by name. -func (mock *MockConfigMapsInterface) Get(name string, options metav1.GetOptions) (*v1.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.ConfigMap, error) { object, ok := mock.objects[name] if !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) @@ -111,16 +117,24 @@ func (mock *MockConfigMapsInterface) Get(name string, options metav1.GetOptions) } // List returns the a of ConfigMaps. -func (mock *MockConfigMapsInterface) List(opts metav1.ListOptions) (*v1.ConfigMapList, error) { +func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) { var list v1.ConfigMapList + + labelSelector, err := kblabels.Parse(opts.LabelSelector) + if err != nil { + return nil, err + } + for _, cfgmap := range mock.objects { - list.Items = append(list.Items, *cfgmap) + if labelSelector.Matches(kblabels.Set(cfgmap.ObjectMeta.Labels)) { + list.Items = append(list.Items, *cfgmap) + } } return &list, nil } // Create creates a new ConfigMap. -func (mock *MockConfigMapsInterface) Create(cfgmap *v1.ConfigMap) (*v1.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Create(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.CreateOptions) (*v1.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if object, ok := mock.objects[name]; ok { return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) @@ -130,7 +144,7 @@ func (mock *MockConfigMapsInterface) Create(cfgmap *v1.ConfigMap) (*v1.ConfigMap } // Update updates a ConfigMap. -func (mock *MockConfigMapsInterface) Update(cfgmap *v1.ConfigMap) (*v1.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Update(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.UpdateOptions) (*v1.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if _, ok := mock.objects[name]; !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) @@ -140,7 +154,7 @@ func (mock *MockConfigMapsInterface) Update(cfgmap *v1.ConfigMap) (*v1.ConfigMap } // Delete deletes a ConfigMap by name. -func (mock *MockConfigMapsInterface) Delete(name string, opts *metav1.DeleteOptions) error { +func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error { if _, ok := mock.objects[name]; !ok { return apierrors.NewNotFound(v1.Resource("tests"), name) } @@ -180,7 +194,7 @@ func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) } // Get returns the Secret by name. -func (mock *MockSecretsInterface) Get(name string, options metav1.GetOptions) (*v1.Secret, error) { +func (mock *MockSecretsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.Secret, error) { object, ok := mock.objects[name] if !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) @@ -189,16 +203,24 @@ func (mock *MockSecretsInterface) Get(name string, options metav1.GetOptions) (* } // List returns the a of Secret. -func (mock *MockSecretsInterface) List(opts metav1.ListOptions) (*v1.SecretList, error) { +func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.SecretList, error) { var list v1.SecretList + + labelSelector, err := kblabels.Parse(opts.LabelSelector) + if err != nil { + return nil, err + } + for _, secret := range mock.objects { - list.Items = append(list.Items, *secret) + if labelSelector.Matches(kblabels.Set(secret.ObjectMeta.Labels)) { + list.Items = append(list.Items, *secret) + } } return &list, nil } // Create creates a new Secret. -func (mock *MockSecretsInterface) Create(secret *v1.Secret) (*v1.Secret, error) { +func (mock *MockSecretsInterface) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) { name := secret.ObjectMeta.Name if object, ok := mock.objects[name]; ok { return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) @@ -208,7 +230,7 @@ func (mock *MockSecretsInterface) Create(secret *v1.Secret) (*v1.Secret, error) } // Update updates a Secret. -func (mock *MockSecretsInterface) Update(secret *v1.Secret) (*v1.Secret, error) { +func (mock *MockSecretsInterface) Update(_ context.Context, secret *v1.Secret, _ metav1.UpdateOptions) (*v1.Secret, error) { name := secret.ObjectMeta.Name if _, ok := mock.objects[name]; !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) @@ -218,10 +240,26 @@ func (mock *MockSecretsInterface) Update(secret *v1.Secret) (*v1.Secret, error) } // Delete deletes a Secret by name. -func (mock *MockSecretsInterface) Delete(name string, opts *metav1.DeleteOptions) error { +func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error { if _, ok := mock.objects[name]; !ok { return apierrors.NewNotFound(v1.Resource("tests"), name) } delete(mock.objects, name) return nil } + +// newTestFixtureSQL mocks the SQL database (for testing purposes) +func newTestFixtureSQL(t *testing.T, releases ...*rspb.Release) (*SQL, sqlmock.Sqlmock) { + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error when opening stub database connection: %v", err) + } + + sqlxDB := sqlx.NewDb(sqlDB, "sqlmock") + return &SQL{ + db: sqlxDB, + Log: func(a string, b ...interface{}) {}, + namespace: "default", + statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), + }, mock +} diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index dcb2ecfcf..44280f70f 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -17,6 +17,7 @@ limitations under the License. package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "context" "strconv" "strings" "time" @@ -62,7 +63,7 @@ func (secrets *Secrets) Name() string { // or error if not found. func (secrets *Secrets) Get(key string) (*rspb.Release, error) { // fetch the secret holding the release named by key - obj, err := secrets.impl.Get(key, metav1.GetOptions{}) + obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { return nil, ErrReleaseNotFound @@ -81,7 +82,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} - list, err := secrets.impl.List(opts) + list, err := secrets.impl.List(context.Background(), opts) if err != nil { return nil, errors.Wrap(err, "list: failed to list") } @@ -116,7 +117,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} - list, err := secrets.impl.List(opts) + list, err := secrets.impl.List(context.Background(), opts) if err != nil { return nil, errors.Wrap(err, "query: failed to query with labels") } @@ -152,7 +153,7 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { return errors.Wrapf(err, "create: failed to encode release %q", rls.Name) } // push the secret object out into the kubiverse - if _, err := secrets.impl.Create(obj); err != nil { + if _, err := secrets.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { if apierrors.IsAlreadyExists(err) { return ErrReleaseExists } @@ -177,7 +178,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { return errors.Wrapf(err, "update: failed to encode release %q", rls.Name) } // push the secret object out into the kubiverse - _, err = secrets.impl.Update(obj) + _, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) return errors.Wrap(err, "update: failed to update") } @@ -185,14 +186,10 @@ 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{}) + err = secrets.impl.Delete(context.Background(), key, metav1.DeleteOptions{}) return rls, err } diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index e4420704d..d509c7b3a 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -130,6 +130,30 @@ func TestSecretList(t *testing.T) { } } +func TestSecretQuery(t *testing.T) { + secrets := newTestFixtureSecrets(t, []*rspb.Release{ + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + }...) + + rls, err := secrets.Query(map[string]string{"status": "deployed"}) + if err != nil { + t.Fatalf("Failed to query: %s", err) + } + if len(rls) != 2 { + t.Fatalf("Expected 2 results, actual %d", len(rls)) + } + + _, err = secrets.Query(map[string]string{"name": "notExist"}) + if err != ErrReleaseNotFound { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} + func TestSecretCreate(t *testing.T) { secrets := newTestFixtureSecrets(t) @@ -194,6 +218,12 @@ func TestSecretDelete(t *testing.T) { 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 { diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go new file mode 100644 index 000000000..f68f50f54 --- /dev/null +++ b/pkg/storage/driver/sql.go @@ -0,0 +1,492 @@ +/* +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 driver // import "helm.sh/helm/v3/pkg/storage/driver" + +import ( + "fmt" + "sort" + "time" + + "github.com/jmoiron/sqlx" + migrate "github.com/rubenv/sql-migrate" + + sq "github.com/Masterminds/squirrel" + + // Import pq for postgres dialect + _ "github.com/lib/pq" + + rspb "helm.sh/helm/v3/pkg/release" +) + +var _ Driver = (*SQL)(nil) + +var labelMap = map[string]struct{}{ + "modifiedAt": {}, + "createdAt": {}, + "version": {}, + "status": {}, + "owner": {}, + "name": {}, +} + +const postgreSQLDialect = "postgres" + +// SQLDriverName is the string name of this driver. +const SQLDriverName = "SQL" + +const sqlReleaseTableName = "releases_v1" + +const ( + sqlReleaseTableKeyColumn = "key" + sqlReleaseTableTypeColumn = "type" + sqlReleaseTableBodyColumn = "body" + sqlReleaseTableNameColumn = "name" + sqlReleaseTableNamespaceColumn = "namespace" + sqlReleaseTableVersionColumn = "version" + sqlReleaseTableStatusColumn = "status" + sqlReleaseTableOwnerColumn = "owner" + sqlReleaseTableCreatedAtColumn = "createdAt" + sqlReleaseTableModifiedAtColumn = "modifiedAt" +) + +const ( + sqlReleaseDefaultOwner = "helm" + sqlReleaseDefaultType = "helm.sh/release.v1" +) + +// SQL is the sql storage driver implementation. +type SQL struct { + db *sqlx.DB + namespace string + statementBuilder sq.StatementBuilderType + + Log func(string, ...interface{}) +} + +// Name returns the name of the driver. +func (s *SQL) Name() string { + return SQLDriverName +} + +func (s *SQL) ensureDBSetup() error { + // Populate the database with the relations we need if they don't exist yet + migrations := &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "init", + Up: []string{ + fmt.Sprintf(` + CREATE TABLE %s ( + %s VARCHAR(67), + %s VARCHAR(64) NOT NULL, + %s TEXT NOT NULL, + %s VARCHAR(64) NOT NULL, + %s VARCHAR(64) NOT NULL, + %s INTEGER NOT NULL, + %s TEXT NOT NULL, + %s TEXT NOT NULL, + %s INTEGER NOT NULL, + %s INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(%s, %s) + ); + CREATE INDEX ON %s (%s, %s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + + GRANT ALL ON %s TO PUBLIC; + + ALTER TABLE %s ENABLE ROW LEVEL SECURITY; + `, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableName, + sqlReleaseTableVersionColumn, + sqlReleaseTableName, + sqlReleaseTableStatusColumn, + sqlReleaseTableName, + sqlReleaseTableOwnerColumn, + sqlReleaseTableName, + sqlReleaseTableCreatedAtColumn, + sqlReleaseTableName, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableName, + sqlReleaseTableName, + ), + }, + Down: []string{ + fmt.Sprintf(` + DROP TABLE %s; + `, sqlReleaseTableName), + }, + }, + }, + } + + _, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up) + return err +} + +// SQLReleaseWrapper describes how Helm releases are stored in an SQL database +type SQLReleaseWrapper struct { + // The primary key, made of {release-name}.{release-version} + Key string `db:"key"` + + // See https://github.com/helm/helm/blob/master/pkg/storage/driver/secrets.go#L236 + Type string `db:"type"` + + // The rspb.Release body, as a base64-encoded string + Body string `db:"body"` + + // Release "labels" that can be used as filters in the storage.Query(labels map[string]string) + // we implemented. Note that allowing Helm users to filter against new dimensions will require a + // new migration to be added, and the Create and/or update functions to be updated accordingly. + Name string `db:"name"` + Namespace string `db:"namespace"` + Version int `db:"version"` + Status string `db:"status"` + Owner string `db:"owner"` + CreatedAt int `db:"createdAt"` + ModifiedAt int `db:"modifiedAt"` +} + +// NewSQL initializes a new sql driver. +func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) { + db, err := sqlx.Connect(postgreSQLDialect, connectionString) + if err != nil { + return nil, err + } + + driver := &SQL{ + db: db, + Log: logger, + statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), + } + + if err := driver.ensureDBSetup(); err != nil { + return nil, err + } + + driver.namespace = namespace + + return driver, nil +} + +// Get returns the release named by key. +func (s *SQL) Get(key string) (*rspb.Release, error) { + var record SQLReleaseWrapper + + qb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + + query, args, err := qb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + // Get will return an error if the result is empty + if err := s.db.Get(&record, query, args...); err != nil { + s.Log("got SQL error when getting release %s: %v", key, err) + return nil, ErrReleaseNotFound + } + + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("get: failed to decode data %q: %v", key, err) + return nil, err + } + + return release, nil +} + +// List returns the list of all releases such that filter(release) == true +func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { + sb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner}) + + // If a namespace was specified, we only list releases from that namespace + if s.namespace != "" { + sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + } + + query, args, err := sb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + var records = []SQLReleaseWrapper{} + if err := s.db.Select(&records, query, args...); err != nil { + s.Log("list: failed to list: %v", err) + return nil, err + } + + var releases []*rspb.Release + for _, record := range records { + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("list: failed to decode release: %v: %v", record, err) + continue + } + if filter(release) { + releases = append(releases, release) + } + } + + return releases, nil +} + +// Query returns the set of releases that match the provided set of labels. +func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { + sb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName) + + keys := make([]string, 0, len(labels)) + for key := range labels { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if _, ok := labelMap[key]; ok { + sb = sb.Where(sq.Eq{key: labels[key]}) + } else { + s.Log("unknown label %s", key) + return nil, fmt.Errorf("unknow label %s", key) + } + } + + // If a namespace was specified, we only list releases from that namespace + if s.namespace != "" { + sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + } + + // Build our query + query, args, err := sb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + var records = []SQLReleaseWrapper{} + if err := s.db.Select(&records, query, args...); err != nil { + s.Log("list: failed to query with labels: %v", err) + return nil, err + } + + var releases []*rspb.Release + for _, record := range records { + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("list: failed to decode release: %v: %v", record, err) + continue + } + releases = append(releases, release) + } + + if len(releases) == 0 { + return nil, ErrReleaseNotFound + } + + return releases, nil +} + +// Create creates a new release. +func (s *SQL) Create(key string, rls *rspb.Release) error { + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + s.namespace = namespace + + body, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + transaction, err := s.db.Beginx() + if err != nil { + s.Log("failed to start SQL transaction: %v", err) + return fmt.Errorf("error beginning transaction: %v", err) + } + + insertQuery, args, err := s.statementBuilder. + Insert(sqlReleaseTableName). + Columns( + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ). + Values( + key, + sqlReleaseDefaultType, + body, + rls.Name, + namespace, + int(rls.Version), + rls.Info.Status.String(), + sqlReleaseDefaultOwner, + int(time.Now().Unix()), + ).ToSql() + if err != nil { + s.Log("failed to build insert query: %v", err) + return err + } + + if _, err := transaction.Exec(insertQuery, args...); err != nil { + defer transaction.Rollback() + + selectQuery, args, buildErr := s.statementBuilder. + Select(sqlReleaseTableKeyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if buildErr != nil { + s.Log("failed to build select query: %v", buildErr) + return err + } + + var record SQLReleaseWrapper + if err := transaction.Get(&record, selectQuery, args...); err == nil { + s.Log("release %s already exists", key) + return ErrReleaseExists + } + + s.Log("failed to store release %s in SQL database: %v", key, err) + return err + } + defer transaction.Commit() + + return nil +} + +// Update updates a release. +func (s *SQL) Update(key string, rls *rspb.Release) error { + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + s.namespace = namespace + + body, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + query, args, err := s.statementBuilder. + Update(sqlReleaseTableName). + Set(sqlReleaseTableBodyColumn, body). + Set(sqlReleaseTableNameColumn, rls.Name). + Set(sqlReleaseTableVersionColumn, int(rls.Version)). + Set(sqlReleaseTableStatusColumn, rls.Info.Status.String()). + Set(sqlReleaseTableOwnerColumn, sqlReleaseDefaultOwner). + Set(sqlReleaseTableModifiedAtColumn, int(time.Now().Unix())). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: namespace}). + ToSql() + + if err != nil { + s.Log("failed to build update query: %v", err) + return err + } + + if _, err := s.db.Exec(query, args...); err != nil { + s.Log("failed to update release %s in SQL database: %v", key, err) + return err + } + + return nil +} + +// Delete deletes a release or returns ErrReleaseNotFound. +func (s *SQL) Delete(key string) (*rspb.Release, error) { + transaction, err := s.db.Beginx() + if err != nil { + s.Log("failed to start SQL transaction: %v", err) + return nil, fmt.Errorf("error beginning transaction: %v", err) + } + + selectQuery, args, err := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if err != nil { + s.Log("failed to build select query: %v", err) + return nil, err + } + + var record SQLReleaseWrapper + err = transaction.Get(&record, selectQuery, args...) + if err != nil { + s.Log("release %s not found: %v", key, err) + return nil, ErrReleaseNotFound + } + + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("failed to decode release %s: %v", key, err) + transaction.Rollback() + return nil, err + } + defer transaction.Commit() + + deleteQuery, args, err := s.statementBuilder. + Delete(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if err != nil { + s.Log("failed to build select query: %v", err) + return nil, err + } + + _, err = transaction.Exec(deleteQuery, args...) + return release, err +} diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go new file mode 100644 index 000000000..1562a90aa --- /dev/null +++ b/pkg/storage/driver/sql_test.go @@ -0,0 +1,442 @@ +/* +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 driver + +import ( + "fmt" + "reflect" + "regexp" + "testing" + "time" + + sqlmock "github.com/DATA-DOG/go-sqlmock" + + rspb "helm.sh/helm/v3/pkg/release" +) + +func TestSQLName(t *testing.T) { + sqlDriver, _ := newTestFixtureSQL(t) + if sqlDriver.Name() != SQLDriverName { + t.Errorf("Expected name to be %s, got %s", SQLDriverName, sqlDriver.Name()) + } +} + +func TestSQLGet(t *testing.T) { + vers := int(1) + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + body, _ := encodeRelease(rel) + + sqlDriver, mock := newTestFixtureSQL(t) + + query := fmt.Sprintf( + regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"), + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(query). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + body, + ), + ).RowsWillBeClosed() + + got, err := sqlDriver.Get(key) + if err != nil { + t.Fatalf("Failed to get release: %v", err) + } + + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected release {%v}, got {%v}", rel, got) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSQLList(t *testing.T) { + body1, _ := encodeRelease(releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) + body2, _ := encodeRelease(releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) + body3, _ := encodeRelease(releaseStub("key-3", 1, "default", rspb.StatusDeployed)) + body4, _ := encodeRelease(releaseStub("key-4", 1, "default", rspb.StatusDeployed)) + body5, _ := encodeRelease(releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) + body6, _ := encodeRelease(releaseStub("key-6", 1, "default", rspb.StatusSuperseded)) + + sqlDriver, mock := newTestFixtureSQL(t) + + for i := 0; i < 3; i++ { + query := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableOwnerColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs(sqlReleaseDefaultOwner, sqlDriver.namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }). + AddRow(body1). + AddRow(body2). + AddRow(body3). + AddRow(body4). + AddRow(body5). + AddRow(body6), + ).RowsWillBeClosed() + } + + // list all deleted releases + del, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusUninstalled + }) + // check + if err != nil { + t.Errorf("Failed to list deleted: %v", err) + } + if len(del) != 2 { + t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) + } + + // list all deployed releases + dpl, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusDeployed + }) + // check + if err != nil { + t.Errorf("Failed to list deployed: %v", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d:\n%v\n", len(dpl), dpl) + } + + // list all superseded releases + ssd, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusSuperseded + }) + // check + if err != nil { + t.Errorf("Failed to list superseded: %v", err) + } + if len(ssd) != 2 { + t.Errorf("Expected 2 superseded, got %d:\n%v\n", len(ssd), ssd) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlCreate(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + query := fmt.Sprintf( + "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ) + + mock.ExpectBegin() + mock. + ExpectExec(regexp.QuoteMeta(query)). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err := sqlDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlCreateAlreadyExists(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + insertQuery := fmt.Sprintf( + "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ) + + // Insert fails (primary key already exists) + mock.ExpectBegin() + mock. + ExpectExec(regexp.QuoteMeta(insertQuery)). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WillReturnError(fmt.Errorf("dialect dependent SQL error")) + + selectQuery := fmt.Sprintf( + regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"), + sqlReleaseTableKeyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + // Let's check that we do make sure the error is due to a release already existing + mock. + ExpectQuery(selectQuery). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableKeyColumn, + }).AddRow( + key, + ), + ).RowsWillBeClosed() + mock.ExpectRollback() + + if err := sqlDriver.Create(key, rel); err == nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlUpdate(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + query := fmt.Sprintf( + "UPDATE %s SET %s = $1, %s = $2, %s = $3, %s = $4, %s = $5, %s = $6 WHERE %s = $7 AND %s = $8", + sqlReleaseTableName, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectExec(regexp.QuoteMeta(query)). + WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix()), key, namespace). + WillReturnResult(sqlmock.NewResult(0, 1)) + + if err := sqlDriver.Update(key, rel); err != nil { + t.Fatalf("failed to update release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlQuery(t *testing.T) { + // Reflect actual use cases in ../storage.go + labelSetDeployed := map[string]string{ + "name": "smug-pigeon", + "owner": sqlReleaseDefaultOwner, + "status": "deployed", + } + labelSetAll := map[string]string{ + "name": "smug-pigeon", + "owner": sqlReleaseDefaultOwner, + } + + supersededRelease := releaseStub("smug-pigeon", 1, "default", rspb.StatusSuperseded) + supersededReleaseBody, _ := encodeRelease(supersededRelease) + deployedRelease := releaseStub("smug-pigeon", 2, "default", rspb.StatusDeployed) + deployedReleaseBody, _ := encodeRelease(deployedRelease) + + // Let's actually start our test + sqlDriver, mock := newTestFixtureSQL(t) + + query := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3 AND %s = $4", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableNameColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "deployed", "default"). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + deployedReleaseBody, + ), + ).RowsWillBeClosed() + + query = fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableNameColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "default"). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + supersededReleaseBody, + ).AddRow( + deployedReleaseBody, + ), + ).RowsWillBeClosed() + + results, err := sqlDriver.Query(labelSetDeployed) + if err != nil { + t.Fatalf("failed to query for deployed smug-pigeon release: %v", err) + } + + for _, res := range results { + if !reflect.DeepEqual(res, deployedRelease) { + t.Errorf("Expected release {%v}, got {%v}", deployedRelease, res) + } + } + + results, err = sqlDriver.Query(labelSetAll) + if err != nil { + t.Fatalf("failed to query release history for smug-pigeon: %v", err) + } + + if len(results) != 2 { + t.Errorf("expected a resultset of size 2, got %d", len(results)) + } + + for _, res := range results { + if !reflect.DeepEqual(res, deployedRelease) && !reflect.DeepEqual(res, supersededRelease) { + t.Errorf("Expected release {%v} or {%v}, got {%v}", deployedRelease, supersededRelease, res) + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + body, _ := encodeRelease(rel) + + sqlDriver, mock := newTestFixtureSQL(t) + + selectQuery := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock.ExpectBegin() + mock. + ExpectQuery(regexp.QuoteMeta(selectQuery)). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + body, + ), + ).RowsWillBeClosed() + + deleteQuery := fmt.Sprintf( + "DELETE FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectExec(regexp.QuoteMeta(deleteQuery)). + WithArgs(key, namespace). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + deletedRelease, err := sqlDriver.Delete(key) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } + if err != nil { + t.Fatalf("failed to delete release with key %q: %v", key, err) + } + + if !reflect.DeepEqual(rel, deletedRelease) { + t.Errorf("Expected release {%v}, got {%v}", rel, deletedRelease) + } +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 89183355f..c195120cd 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -116,7 +116,7 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) { } if len(ls) == 0 { - return nil, errors.Errorf("%q has no deployed releases", name) + return nil, driver.NewErrNoDeployedReleases(name) } // If executed concurrently, Helm's database gets corrupted @@ -140,7 +140,7 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { return ls, nil } if strings.Contains(err.Error(), "not found") { - return nil, errors.Errorf("%q has no deployed releases", name) + return nil, driver.NewErrNoDeployedReleases(name) } return nil, err } @@ -169,21 +169,37 @@ func (s *Storage) removeLeastRecent(name string, max int) error { if len(h) <= max { return nil } - overage := len(h) - max // We want oldest to newest relutil.SortByRevision(h) + lastDeployed, err := s.Deployed(name) + if err != nil { + return err + } + + var toDelete []*rspb.Release + for _, rel := range h { + // once we have enough releases to delete to reach the max, stop + if len(h)-len(toDelete) == max { + break + } + if lastDeployed != nil { + if rel.Version != lastDeployed.Version { + toDelete = append(toDelete, rel) + } + } else { + toDelete = append(toDelete, rel) + } + } + // Delete as many as possible. In the case of API throughput limitations, // multiple invocations of this function will eventually delete them all. - toDelete := h[0:overage] errs := []error{} for _, rel := range toDelete { - key := makeKey(name, rel.Version) - _, innerErr := s.Delete(name, rel.Version) - if innerErr != nil { - s.Log("error pruning %s from release history: %s", key, innerErr) - errs = append(errs, innerErr) + err = s.deleteReleaseVersion(name, rel.Version) + if err != nil { + errs = append(errs, err) } } @@ -198,6 +214,16 @@ func (s *Storage) removeLeastRecent(name string, max int) error { } } +func (s *Storage) deleteReleaseVersion(name string, version int) error { + key := makeKey(name, version) + _, err := s.Delete(name, version) + if err != nil { + s.Log("error pruning %s from release history: %s", key, err) + return err + } + return nil +} + // Last fetches the last revision of the named release. func (s *Storage) Last(name string) (*rspb.Release, error) { s.Log("getting last revision of %q", name) diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index ee9c68b80..934a3842c 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -333,6 +333,57 @@ func TestStorageRemoveLeastRecent(t *testing.T) { } } +func TestStorageDontDeleteDeployed(t *testing.T) { + storage := Init(driver.NewMemory()) + storage.Log = t.Logf + storage.MaxHistory = 3 + + const name = "angry-bird" + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + setup() + + rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") + + // On inserting the 5th record, we expect a total of 3 releases, but we expect version 2 + // (the only deployed release), to still exist + hist, err := storage.History(name) + if err != nil { + t.Fatal(err) + } else if len(hist) != storage.MaxHistory { + for _, item := range hist { + t.Logf("%s %v", item.Name, item.Version) + } + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + } + + expectedVersions := map[int]bool{ + 2: true, + 4: true, + 5: true, + } + + for _, item := range hist { + if !expectedVersions[item.Version] { + t.Errorf("Release version %d, found when not expected", item.Version) + } + } +} + func TestStorageLast(t *testing.T) { storage := Init(driver.NewMemory()) diff --git a/scripts/get b/scripts/get index 3da11d4a4..afa02bbb1 100755 --- a/scripts/get +++ b/scripts/get @@ -82,9 +82,9 @@ checkDesiredVersion() { 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}') + TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') elif type "wget" > /dev/null; then - 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}') + TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') fi else TAG=$DESIRED_VERSION diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 index a974d97b6..201065717 100755 --- a/scripts/get-helm-3 +++ b/scripts/get-helm-3 @@ -17,8 +17,7 @@ # The install script is based off of the MIT-licensed script from glide, # the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get -PROJECT_NAME="helm" - +: ${BINARY_NAME:="helm"} : ${USE_SUDO:="true"} : ${HELM_INSTALL_DIR:="/usr/local/bin"} @@ -80,9 +79,9 @@ checkDesiredVersion() { # Get tag from release URL local latest_release_url="https://github.com/helm/helm/releases" if type "curl" > /dev/null; then - 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}') + TAG=$(curl -Ls $latest_release_url | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') elif type "wget" > /dev/null; then - 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}') + TAG=$(wget $latest_release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') fi else TAG=$DESIRED_VERSION @@ -92,8 +91,8 @@ checkDesiredVersion() { # checkHelmInstalledVersion checks which version of helm is installed and # if it needs to be changed. checkHelmInstalledVersion() { - if [[ -f "${HELM_INSTALL_DIR}/${PROJECT_NAME}" ]]; then - local version=$("${HELM_INSTALL_DIR}/${PROJECT_NAME}" version --template="{{ .Version }}") + if [[ -f "${HELM_INSTALL_DIR}/${BINARY_NAME}" ]]; then + local version=$("${HELM_INSTALL_DIR}/${BINARY_NAME}" version --template="{{ .Version }}") if [[ "$version" == "$TAG" ]]; then echo "Helm ${version} is already ${DESIRED_VERSION:-latest}" return 0 @@ -131,7 +130,7 @@ downloadFile() { # installFile verifies the SHA256 for the file, then unpacks and # installs it. installFile() { - HELM_TMP="$HELM_TMP_ROOT/$PROJECT_NAME" + HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME" local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}') local expected_sum=$(cat ${HELM_SUM_FILE}) if [ "$sum" != "$expected_sum" ]; then @@ -141,10 +140,10 @@ installFile() { mkdir -p "$HELM_TMP" tar xf "$HELM_TMP_FILE" -C "$HELM_TMP" - HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/$PROJECT_NAME" - echo "Preparing to install $PROJECT_NAME into ${HELM_INSTALL_DIR}" - runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR" - echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME" + HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm" + echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}" + runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME" + echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME" } # fail_trap is executed if an error occurs. @@ -152,10 +151,10 @@ fail_trap() { result=$? if [ "$result" != "0" ]; then if [[ -n "$INPUT_ARGUMENTS" ]]; then - echo "Failed to install $PROJECT_NAME with the arguments provided: $INPUT_ARGUMENTS" + echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS" help else - echo "Failed to install $PROJECT_NAME" + echo "Failed to install $BINARY_NAME" fi echo -e "\tFor support, go to https://github.com/helm/helm." fi @@ -166,9 +165,9 @@ fail_trap() { # testVersion tests the installed client to make sure it is working. testVersion() { set +e - HELM="$(which $PROJECT_NAME)" + HELM="$(which $BINARY_NAME)" if [ "$?" = "1" ]; then - echo "$PROJECT_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?' + echo "$BINARY_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?' exit 1 fi set -e 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