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/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/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..e3013d713 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", 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 51b4f0d58..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") } diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 3e3dfa012..143745f29 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -43,32 +43,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") | -+------------------+-----------------------------------------------------------------------------+ - -Helm stores configuration based on the XDG base directory specification, so - -- 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 +| 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 cache, configuration, and data based on the following configuration order: + +- 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 { 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/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/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-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 48eac0b0f..c263d32e7 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -75,21 +75,8 @@ 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. histClient := action.NewHistory(cfg) @@ -124,6 +111,21 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } + 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 { diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 4956c0247..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) } @@ -256,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 a77aa58c7..686275867 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 @@ -20,18 +22,21 @@ 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/theupdateframework/notary v0.6.1 + github.com/stretchr/testify v1.5.1 github.com/xeipuuv/gojsonschema v1.1.0 - golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 + 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 diff --git a/go.sum b/go.sum index 8b44a3682..d08d65da3 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= @@ -56,6 +62,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= @@ -81,6 +88,7 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= 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/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= @@ -97,6 +105,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= @@ -106,22 +115,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= @@ -149,6 +162,7 @@ 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= @@ -175,6 +189,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= @@ -223,7 +238,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= @@ -231,13 +258,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= @@ -255,7 +284,6 @@ 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= @@ -283,11 +311,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= @@ -296,15 +328,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= @@ -315,8 +354,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= @@ -324,6 +364,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= @@ -338,14 +386,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-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= @@ -359,7 +413,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= @@ -370,11 +423,15 @@ 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= @@ -402,12 +459,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= @@ -418,19 +476,33 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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= @@ -439,14 +511,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= @@ -455,19 +532,23 @@ 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/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= 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= @@ -488,6 +569,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= @@ -497,6 +581,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= @@ -505,23 +590,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= @@ -535,6 +619,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= @@ -554,6 +640,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= @@ -562,12 +649,12 @@ 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= @@ -590,39 +677,36 @@ 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= @@ -635,9 +719,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= @@ -648,8 +735,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= @@ -657,66 +744,37 @@ 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/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= -k8s.io/apiextensions-apiserver v0.17.3 h1:WDZWkPcbgvchEdDd7ysL21GGPx3UKZQLDZXEkevT6n4= -k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY= 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.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= -k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= 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.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY= k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= -k8s.io/cli-runtime v0.17.3 h1:0ZlDdJgJBKsu77trRUynNiWsRuAvAVPBNaQfnt/1qtc= -k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA= 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.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU= -k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= 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.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= -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/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/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.17.3 h1:9HHYj07kuFkM+sMJMOyQX29CKWq4lvKAG1UIPxNPMQ4= -k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28= 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/metrics v0.18.0/go.mod h1:8aYTW18koXqjLVKL7Ds05RPMX9ipJZI3mywYvBOxXd4= -k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= -k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/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= 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= 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/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 05a133abf..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) @@ -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/install.go b/pkg/action/install.go index 4b4dd9214..10a9644dd 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -39,7 +39,6 @@ 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" @@ -232,7 +231,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() @@ -475,125 +474,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)) 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/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/upgrade.go b/pkg/action/upgrade.go index b73e2ac8b..fc289dbab 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -33,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. @@ -43,27 +44,57 @@ type Upgrade struct { ChartPathOptions - Install bool - Devel 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 skip installing CRDs when install flag is enabled during upgrade - SkipCRDs bool - Timeout time.Duration - Wait bool + // 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 } @@ -129,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 { @@ -145,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 @@ -171,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 } @@ -203,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) diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go index 4f19bf87d..ef8cec3ad 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -96,3 +96,116 @@ func TestMetadata(t *testing.T) { 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 daaaa897c..0e87c7b47 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -99,7 +99,7 @@ replicaCount: 1 image: repository: nginx pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart version. + # Overrides the image tag whose default is the chart appVersion. tag: "" imagePullSecrets: [] @@ -113,7 +113,7 @@ 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: {} @@ -214,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 @@ -261,10 +261,10 @@ spec: 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 }} @@ -292,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 @@ -330,7 +330,7 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} -{{- end -}} +{{- end }} ` const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} @@ -391,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. @@ -400,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 @@ -429,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 @@ -437,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 diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index be5d151d7..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()) 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/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 0013dbdf0..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" @@ -72,31 +72,6 @@ type ChartDownloader struct { RepositoryCache string } -// atomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a -// disk. -func atomicWriteFile(filename string, body 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, body); 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 os.Rename(tempName, filename) -} - // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. // // If Verify is set to VerifyNever, the verification will be nil. @@ -126,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 := atomicWriteFile(destfile, data, 0644); err != nil { + if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil { return destfile, nil, err } @@ -142,7 +117,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return destfile, ver, nil } provfile := destfile + ".prov" - if err := atomicWriteFile(provfile, body, 0644); err != nil { + if err := fileutil.AtomicWriteFile(provfile, body, 0644); err != nil { return destfile, nil, err } diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index ff451a6e8..00198de0c 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -239,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 == "" { @@ -278,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) @@ -288,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, @@ -304,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 e5769cbe0..92b4c3383 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -62,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 { diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index a405c1c47..62c63ec2b 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -94,6 +94,11 @@ func TestFuncs(t *testing.T) { 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 { @@ -103,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 2527954fa..20be9189e 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -32,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 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_test.go b/pkg/getter/httpgetter_test.go index a1288bf47..4d7ada852 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -122,7 +122,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) } 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/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 05b26b12a..c1de2b299 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -223,6 +223,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 { @@ -232,16 +233,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 } @@ -443,7 +439,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) 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/lint/lint_test.go b/pkg/lint/lint_test.go index b51939d76..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" ) @@ -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/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/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/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index e40247d3c..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) 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 22e6454a1..c0236ece8 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -21,9 +21,14 @@ import ( "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" @@ -112,10 +117,18 @@ func (mock *MockConfigMapsInterface) Get(_ context.Context, name string, _ metav } // List returns the a of ConfigMaps. -func (mock *MockConfigMapsInterface) List(_ context.Context, _ 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 } @@ -190,10 +203,18 @@ func (mock *MockSecretsInterface) Get(_ context.Context, name string, _ metav1.G } // List returns the a of Secret. -func (mock *MockSecretsInterface) List(_ context.Context, _ 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 } @@ -226,3 +247,19 @@ func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav 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_test.go b/pkg/storage/driver/secrets_test.go index 5f0ecc8bb..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) 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