diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 8e1ce8386..a97f34efd 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -35,7 +35,7 @@ func TestCreateCmd(t *testing.T) { cname := "testchart" // Run a create - if _, err := executeCommand(nil, "create "+cname); err != nil { + if _, _, err := executeActionCommand("create " + cname); err != nil { t.Errorf("Failed to run create: %s", err) return } @@ -86,7 +86,7 @@ func TestCreateStarterCmd(t *testing.T) { defer testChdir(t, tdir)() // Run a create - if _, err := executeCommand(nil, fmt.Sprintf("--home='%s' create --starter=starterchart %s", hh.String(), cname)); err != nil { + if _, _, err := executeActionCommand(fmt.Sprintf("--home='%s' create --starter=starterchart %s", hh.String(), cname)); err != nil { t.Errorf("Failed to run create: %s", err) return } diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index f28e8eff0..e497158aa 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -16,18 +16,13 @@ limitations under the License. package main import ( - "fmt" "io" - "os" "path/filepath" - "github.com/Masterminds/semver" - "github.com/gosuri/uitable" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" + "k8s.io/helm/pkg/action" ) const dependencyDesc = ` @@ -103,14 +98,8 @@ func newDependencyCmd(out io.Writer) *cobra.Command { return cmd } -type dependencyLisOptions struct { - chartpath string -} - func newDependencyListCmd(out io.Writer) *cobra.Command { - o := &dependencyLisOptions{ - chartpath: ".", - } + client := action.NewDependency() cmd := &cobra.Command{ Use: "list CHART", @@ -119,151 +108,12 @@ func newDependencyListCmd(out io.Writer) *cobra.Command { Long: dependencyListDesc, Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." if len(args) > 0 { - o.chartpath = filepath.Clean(args[0]) + chartpath = filepath.Clean(args[0]) } - return o.run(out) + return client.List(chartpath, out) }, } return cmd } - -func (o *dependencyLisOptions) run(out io.Writer) error { - c, err := loader.Load(o.chartpath) - if err != nil { - return err - } - - if c.Metadata.Dependencies == nil { - fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(o.chartpath, "charts")) - return nil - } - - o.printDependencies(out, c.Metadata.Dependencies) - fmt.Fprintln(out) - o.printMissing(out, c.Metadata.Dependencies) - return nil -} - -func (o *dependencyLisOptions) dependencyStatus(dep *chart.Dependency) string { - filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") - archives, err := filepath.Glob(filepath.Join(o.chartpath, "charts", filename)) - if err != nil { - return "bad pattern" - } else if len(archives) > 1 { - return "too many matches" - } else if len(archives) == 1 { - archive := archives[0] - if _, err := os.Stat(archive); err == nil { - c, err := loader.Load(archive) - if err != nil { - return "corrupt" - } - if c.Name() != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "ok" - } - return "wrong version" - } - return "ok" - } - } - - folder := filepath.Join(o.chartpath, "charts", dep.Name) - if fi, err := os.Stat(folder); err != nil { - return "missing" - } else if !fi.IsDir() { - return "mispackaged" - } - - c, err := loader.Load(folder) - if err != nil { - return "corrupt" - } - - if c.Name() != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "unpacked" - } - return "wrong version" - } - - return "unpacked" -} - -// printDependencies prints all of the dependencies in the yaml file. -func (o *dependencyLisOptions) printDependencies(out io.Writer, reqs []*chart.Dependency) { - table := uitable.New() - table.MaxColWidth = 80 - table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") - for _, row := range reqs { - table.AddRow(row.Name, row.Version, row.Repository, o.dependencyStatus(row)) - } - fmt.Fprintln(out, table) -} - -// printMissing prints warnings about charts that are present on disk, but are -// not in Charts.yaml. -func (o *dependencyLisOptions) printMissing(out io.Writer, reqs []*chart.Dependency) { - folder := filepath.Join(o.chartpath, "charts/*") - files, err := filepath.Glob(folder) - if err != nil { - fmt.Fprintln(out, err) - return - } - - for _, f := range files { - fi, err := os.Stat(f) - if err != nil { - fmt.Fprintf(out, "Warning: %s\n", err) - } - // Skip anything that is not a directory and not a tgz file. - if !fi.IsDir() && filepath.Ext(f) != ".tgz" { - continue - } - c, err := loader.Load(f) - if err != nil { - fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) - continue - } - found := false - for _, d := range reqs { - if d.Name == c.Name() { - found = true - break - } - } - if !found { - fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) - } - } - -} diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go index 67fe7cf26..2b05b355e 100644 --- a/cmd/helm/dependency_build.go +++ b/cmd/helm/dependency_build.go @@ -17,10 +17,14 @@ package main import ( "io" + "os" + "path/filepath" "github.com/spf13/cobra" + "k8s.io/client-go/util/homedir" "k8s.io/helm/cmd/helm/require" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/getter" ) @@ -36,17 +40,8 @@ If no lock file is found, 'helm dependency build' will mirror the behavior of 'helm dependency update'. ` -type dependencyBuildOptions struct { - keyring string // --keyring - verify bool // --verify - - chartpath string -} - func newDependencyBuildCmd(out io.Writer) *cobra.Command { - o := &dependencyBuildOptions{ - chartpath: ".", - } + client := action.NewDependency() cmd := &cobra.Command{ Use: "build CHART", @@ -54,31 +49,38 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { Long: dependencyBuildDesc, Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." if len(args) > 0 { - o.chartpath = args[0] + chartpath = filepath.Clean(args[0]) + } + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + HelmHome: settings.Home, + Keyring: client.Keyring, + Getters: getter.All(settings), } - return o.run(out) + if client.Verify { + man.Verify = downloader.VerifyIfPossible + } + if settings.Debug { + man.Debug = true + } + return man.Build() }, } f := cmd.Flags() - f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures") - f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") return cmd } -func (o *dependencyBuildOptions) run(out io.Writer) error { - man := &downloader.Manager{ - Out: out, - ChartPath: o.chartpath, - HelmHome: settings.Home, - Keyring: o.keyring, - Getters: getter.All(settings), +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + if v, ok := os.LookupEnv("GNUPGHOME"); ok { + return filepath.Join(v, "pubring.gpg") } - if o.verify { - man.Verify = downloader.VerifyIfPossible - } - - return man.Build() + return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") } diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 64e32a4b3..67dc0db91 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -44,7 +44,7 @@ func TestDependencyBuildCmd(t *testing.T) { } cmd := fmt.Sprintf("--home='%s' dependency build '%s'", hh, hh.Path(chartname)) - out, err := executeCommand(nil, cmd) + _, out, err := executeActionCommand(cmd) // In the first pass, we basically want the same results as an update. if err != nil { @@ -72,7 +72,7 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - out, err = executeCommand(nil, cmd) + _, out, err = executeActionCommand(cmd) if err != nil { t.Logf("Output: %s", out) t.Fatal(err) diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index 1ce07fb52..bad61e709 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -22,9 +22,9 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" ) const dependencyUpDesc = ` @@ -42,23 +42,9 @@ reason, an update command will not remove charts unless they are (a) present in the Chart.yaml file, but (b) at the wrong version. ` -// dependencyUpdateOptions describes a 'helm dependency update' -type dependencyUpdateOptions struct { - keyring string // --keyring - skipRefresh bool // --skip-refresh - verify bool // --verify - - // args - chartpath string - - helmhome helmpath.Home -} - // newDependencyUpdateCmd creates a new dependency update command. func newDependencyUpdateCmd(out io.Writer) *cobra.Command { - o := &dependencyUpdateOptions{ - chartpath: ".", - } + client := action.NewDependency() cmd := &cobra.Command{ Use: "update CHART", @@ -67,37 +53,32 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { Long: dependencyUpDesc, Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." if len(args) > 0 { - o.chartpath = filepath.Clean(args[0]) + chartpath = filepath.Clean(args[0]) + } + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + HelmHome: settings.Home, + Keyring: client.Keyring, + SkipUpdate: client.SkipRefresh, + Getters: getter.All(settings), + } + if client.Verify { + man.Verify = downloader.VerifyAlways } - o.helmhome = settings.Home - return o.run(out) + if settings.Debug { + man.Debug = true + } + return man.Update() }, } f := cmd.Flags() - f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures") - f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") return cmd } - -// run runs the full dependency update process. -func (o *dependencyUpdateOptions) run(out io.Writer) error { - man := &downloader.Manager{ - Out: out, - ChartPath: o.chartpath, - HelmHome: o.helmhome, - Keyring: o.keyring, - SkipUpdate: o.skipRefresh, - Getters: getter.All(settings), - } - if o.verify { - man.Verify = downloader.VerifyAlways - } - if settings.Debug { - man.Debug = true - } - return man.Update() -} diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index fba560ee8..1cbe5979b 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -16,7 +16,6 @@ limitations under the License. package main import ( - "bytes" "fmt" "io/ioutil" "os" @@ -52,7 +51,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - out, err := executeCommand(nil, fmt.Sprintf("--home='%s' dependency update '%s'", hh.String(), hh.Path(chartname))) + _, out, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update '%s'", hh.String(), hh.Path(chartname))) if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -95,7 +94,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - out, err = executeCommand(nil, fmt.Sprintf("--home='%s' dependency update '%s'", hh, hh.Path(chartname))) + _, out, err = executeActionCommand(fmt.Sprintf("--home='%s' dependency update '%s'", hh, hh.Path(chartname))) if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -133,7 +132,7 @@ func TestDependencyUpdateCmd_SkipRefresh(t *testing.T) { t.Fatal(err) } - out, err := executeCommand(nil, fmt.Sprintf("--home='%s' dependency update --skip-refresh %s", hh, hh.Path(chartname))) + _, out, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update --skip-refresh %s", hh, hh.Path(chartname))) if err == nil { t.Fatal("Expected failure to find the repo with skipRefresh") } @@ -164,13 +163,8 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { t.Fatal(err) } - out := bytes.NewBuffer(nil) - o := &dependencyUpdateOptions{} - o.helmhome = hh - o.chartpath = hh.Path(chartname) - - if err := o.run(out); err != nil { - output := out.String() + _, output, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname))) + if err != nil { t.Logf("Output: %s", output) t.Fatal(err) } @@ -178,14 +172,14 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { // Chart repo is down srv.Stop() - if err := o.run(out); err == nil { - output := out.String() + _, output, err = executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname))) + if err == nil { t.Logf("Output: %s", output) t.Fatal("Expected error, got nil") } // Make sure charts dir still has dependencies - files, err := ioutil.ReadDir(filepath.Join(o.chartpath, "charts")) + files, err := ioutil.ReadDir(filepath.Join(hh.Path(chartname), "charts")) if err != nil { t.Fatal(err) } @@ -201,7 +195,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { } // Make sure tmpcharts is deleted - if _, err := os.Stat(filepath.Join(o.chartpath, "tmpcharts")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(hh.Path(chartname), "tmpcharts")); !os.IsNotExist(err) { t.Fatalf("tmpcharts dir still exists") } } diff --git a/cmd/helm/get.go b/cmd/helm/get.go index cf826a747..e6e853305 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) var getHelp = ` @@ -38,16 +38,8 @@ By default, this prints a human readable collection of information about the chart, the supplied values, and the generated manifest file. ` -type getOptions struct { - version int // --revision - - release string - - client helm.Interface -} - -func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &getOptions{client: client} +func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) cmd := &cobra.Command{ Use: "get RELEASE_NAME", @@ -55,25 +47,19 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: getHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.release = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + res, err := client.Run(args[0]) + if err != nil { + return err + } + return printRelease(out, res) }, } - cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") + cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") - cmd.AddCommand(newGetValuesCmd(client, out)) - cmd.AddCommand(newGetManifestCmd(client, out)) - cmd.AddCommand(newGetHooksCmd(client, out)) + cmd.AddCommand(newGetValuesCmd(cfg, out)) + cmd.AddCommand(newGetManifestCmd(cfg, out)) + cmd.AddCommand(newGetHooksCmd(cfg, out)) return cmd } - -func (g *getOptions) run(out io.Writer) error { - res, err := g.client.ReleaseContent(g.release, g.version) - if err != nil { - return err - } - return printRelease(out, res) -} diff --git a/cmd/helm/get_hooks.go b/cmd/helm/get_hooks.go index f455f4914..6ac8aed6a 100644 --- a/cmd/helm/get_hooks.go +++ b/cmd/helm/get_hooks.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) const getHooksHelp = ` @@ -32,14 +32,8 @@ This command downloads hooks for a given release. Hooks are formatted in YAML and separated by the YAML '---\n' separator. ` -type getHooksOptions struct { - release string - client helm.Interface - version int -} - -func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &getHooksOptions{client: client} +func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) cmd := &cobra.Command{ Use: "hooks RELEASE_NAME", @@ -47,24 +41,18 @@ func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: getHooksHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.release = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + res, err := client.Run(args[0]) + if err != nil { + return err + } + for _, hook := range res.Hooks { + fmt.Fprintf(out, "---\n# %s\n%s", hook.Name, hook.Manifest) + } + return nil }, } - cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") - return cmd -} -func (o *getHooksOptions) run(out io.Writer) error { - res, err := o.client.ReleaseContent(o.release, o.version) - if err != nil { - fmt.Fprintln(out, o.release) - return err - } + cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") - for _, hook := range res.Hooks { - fmt.Fprintf(out, "---\n# %s\n%s", hook.Name, hook.Manifest) - } - return nil + return cmd } diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go index 8ab99722d..6b58ee3f1 100644 --- a/cmd/helm/get_hooks_test.go +++ b/cmd/helm/get_hooks_test.go @@ -19,8 +19,7 @@ package main import ( "testing" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestGetHooks(t *testing.T) { @@ -28,7 +27,7 @@ func TestGetHooks(t *testing.T) { name: "get hooks with release", cmd: "get hooks aeneas", golden: "output/get-hooks.txt", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, { name: "get hooks without args", cmd: "get hooks", diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go index 7e0f43a56..8a76c7f2e 100644 --- a/cmd/helm/get_manifest.go +++ b/cmd/helm/get_manifest.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) var getManifestHelp = ` @@ -34,16 +34,8 @@ were generated from this release's chart(s). If a chart is dependent on other charts, those resources will also be included in the manifest. ` -type getManifestOptions struct { - version int // --revision - - release string - - client helm.Interface -} - -func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &getManifestOptions{client: client} +func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) cmd := &cobra.Command{ Use: "manifest RELEASE_NAME", @@ -51,22 +43,16 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: getManifestHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.release = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + res, err := client.Run(args[0]) + if err != nil { + return err + } + fmt.Fprintln(out, res.Manifest) + return nil }, } - cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") - return cmd -} + cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") -// getManifest implements 'helm get manifest' -func (o *getManifestOptions) run(out io.Writer) error { - res, err := o.client.ReleaseContent(o.release, o.version) - if err != nil { - return err - } - fmt.Fprintln(out, res.Manifest) - return nil + return cmd } diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go index f3737d968..28a0ec1f0 100644 --- a/cmd/helm/get_manifest_test.go +++ b/cmd/helm/get_manifest_test.go @@ -19,8 +19,7 @@ package main import ( "testing" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestGetManifest(t *testing.T) { @@ -28,7 +27,7 @@ func TestGetManifest(t *testing.T) { name: "get manifest with release", cmd: "get manifest juno", golden: "output/get-manifest.txt", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "juno"})}, }, { name: "get manifest without args", cmd: "get manifest", diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index e6943475b..0cecd6802 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -19,8 +19,7 @@ package main import ( "testing" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestGetCmd(t *testing.T) { @@ -28,7 +27,7 @@ func TestGetCmd(t *testing.T) { name: "get with a release", cmd: "get thomas-guide", golden: "output/get-release.txt", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, }, { name: "get requires release name arg", cmd: "get", diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go index b13d0ba75..f29f6c865 100644 --- a/cmd/helm/get_values.go +++ b/cmd/helm/get_values.go @@ -20,29 +20,18 @@ import ( "fmt" "io" - "github.com/ghodss/yaml" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) var getValuesHelp = ` This command downloads a values file for a given release. ` -type getValuesOptions struct { - allValues bool // --all - version int // --revision - - release string - - client helm.Interface -} - -func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &getValuesOptions{client: client} +func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGetValues(cfg) cmd := &cobra.Command{ Use: "values RELEASE_NAME", @@ -50,43 +39,17 @@ func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: getValuesHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.release = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + res, err := client.Run(args[0]) + if err != nil { + return err + } + fmt.Fprintln(out, res) + return nil }, } - cmd.Flags().BoolVarP(&o.allValues, "all", "a", false, "dump all (computed) values") - cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "get the named release with revision") + f.BoolVarP(&client.AllValues, "all", "a", false, "dump all (computed) values") return cmd } - -// getValues implements 'helm get values' -func (o *getValuesOptions) run(out io.Writer) error { - res, err := o.client.ReleaseContent(o.release, o.version) - if err != nil { - return err - } - - // If the user wants all values, compute the values and return. - if o.allValues { - cfg, err := chartutil.CoalesceValues(res.Chart, res.Config) - if err != nil { - return err - } - cfgStr, err := cfg.YAML() - if err != nil { - return err - } - fmt.Fprintln(out, cfgStr) - return nil - } - - resConfig, err := yaml.Marshal(res.Config) - if err != nil { - return err - } - - fmt.Fprintln(out, string(resConfig)) - return nil -} diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index 2b14bdf4c..56b247032 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -19,8 +19,7 @@ package main import ( "testing" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestGetValuesCmd(t *testing.T) { @@ -28,7 +27,7 @@ func TestGetValuesCmd(t *testing.T) { name: "get values with a release", cmd: "get values thomas-guide", golden: "output/get-values.txt", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, }, { name: "get values requires release name arg", cmd: "get values", diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index ee211443f..b50bd152b 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -27,15 +27,14 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/environment" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" ) var ( - settings environment.EnvSettings + settings cli.EnvSettings config genericclioptions.RESTClientGetter configOnce sync.Once ) @@ -52,45 +51,13 @@ func logf(format string, v ...interface{}) { } func main() { - cmd := newRootCmd(nil, newActionConfig(false), os.Stdout, os.Args[1:]) + cmd := newRootCmd(newActionConfig(false), os.Stdout, os.Args[1:]) if err := cmd.Execute(); err != nil { logf("%+v", err) os.Exit(1) } } -// ensureHelmClient returns a new helm client impl. if h is not nil. -func ensureHelmClient(h helm.Interface, allNamespaces bool) helm.Interface { - if h != nil { - return h - } - return newClient(allNamespaces) -} - -func newClient(allNamespaces bool) helm.Interface { - kc := kube.New(kubeConfig()) - kc.Log = logf - - clientset, err := kc.KubernetesClientSet() - if err != nil { - // TODO return error - log.Fatal(err) - } - var namespace string - if !allNamespaces { - namespace = getNamespace() - } - // TODO add other backends - d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace)) - d.Log = logf - - return helm.NewClient( - helm.KubeClient(kc), - helm.Driver(d), - helm.Discovery(clientset.Discovery()), - ) -} - func newActionConfig(allNamespaces bool) *action.Configuration { kc := kube.New(kubeConfig()) kc.Log = logf diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 86cd7ba79..f5b39cbb6 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -30,13 +30,12 @@ import ( "k8s.io/helm/internal/test" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller/environment" ) func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } @@ -66,11 +65,13 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { t.Run(tt.name, func(t *testing.T) { defer resetEnv()() - c := &helm.FakeClient{ - Rels: tt.rels, - TestRunStatus: tt.testRunStatus, + storage := storageFixture() + for _, rel := range tt.rels { + if err := storage.Create(rel); err != nil { + t.Fatal(err) + } } - out, err := executeCommand(c, tt.cmd) + _, out, err := executeActionCommandC(storage, tt.cmd) if (err != nil) != tt.wantError { t.Errorf("expected error, got '%v'", err) } @@ -115,12 +116,12 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, actionConfig := &action.Configuration{ Releases: store, - KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard}, + KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, Discovery: fake.NewSimpleClientset().Discovery(), Log: func(format string, v ...interface{}) {}, } - root := newRootCmd(nil, actionConfig, buf, args) + root := newRootCmd(actionConfig, buf, args) root.SetOutput(buf) root.SetArgs(args) @@ -136,35 +137,11 @@ type cmdTestCase struct { golden string wantError bool // Rels are the available releases at the start of the test. - rels []*release.Release - testRunStatus map[string]release.TestRunStatus -} - -// deprecated: Switch to executeActionCommandC -func executeCommand(c helm.Interface, cmd string) (string, error) { - _, output, err := executeCommandC(c, cmd) - return output, err + rels []*release.Release } -// deprecated: Switch to executeActionCommandC -func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string, error) { - args, err := shellwords.Parse(cmd) - if err != nil { - return nil, "", err - } - buf := new(bytes.Buffer) - - actionConfig := &action.Configuration{ - Releases: storage.Init(driver.NewMemory()), - } - - root := newRootCmd(client, actionConfig, buf, args) - root.SetOutput(buf) - root.SetArgs(args) - - c, err := root.ExecuteC() - - return c, buf.String(), err +func executeActionCommand(cmd string) (*cobra.Command, string, error) { + return executeActionCommandC(storageFixture(), cmd) } // ensureTestHome creates a home directory like ensureHome, but without remote references. diff --git a/cmd/helm/history.go b/cmd/helm/history.go index 2dd47b17a..b16f6c8d0 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -17,31 +17,15 @@ limitations under the License. package main import ( - "encoding/json" "fmt" "io" - "github.com/ghodss/yaml" - "github.com/gosuri/uitable" - "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) -type releaseInfo struct { - Revision int `json:"revision"` - Updated string `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - Description string `json:"description"` -} - -type releaseHistory []releaseInfo - var historyHelp = ` History prints historical revisions for a given release. @@ -58,18 +42,8 @@ The historical release set is printed as a formatted table, e.g: 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 Upgraded successfully ` -type historyOptions struct { - colWidth uint // --col-width - max int // --max - outputFormat string // --output - - release string - - client helm.Interface -} - -func newHistoryCmd(c helm.Interface, out io.Writer) *cobra.Command { - o := &historyOptions{client: c} +func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewHistory(cfg) cmd := &cobra.Command{ Use: "history RELEASE_NAME", @@ -78,94 +52,18 @@ func newHistoryCmd(c helm.Interface, out io.Writer) *cobra.Command { Aliases: []string{"hist"}, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.client = ensureHelmClient(o.client, false) - o.release = args[0] - return o.run(out) + history, err := client.Run(args[0]) + if err != nil { + return err + } + fmt.Fprintln(out, history) + return nil }, } f := cmd.Flags() - f.IntVar(&o.max, "max", 256, "maximum number of revision to include in history") - f.UintVar(&o.colWidth, "col-width", 60, "specifies the max column width of output") - f.StringVarP(&o.outputFormat, "output", "o", "table", "prints the output in the specified format (json|table|yaml)") + f.StringVarP(&client.OutputFormat, "output", "o", action.Table.String(), "prints the output in the specified format (json|table|yaml)") + f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") return cmd } - -func (o *historyOptions) run(out io.Writer) error { - rels, err := o.client.ReleaseHistory(o.release, o.max) - if err != nil { - return err - } - if len(rels) == 0 { - return nil - } - - releaseHistory := getReleaseHistory(rels) - - var history []byte - var formattingError error - - switch o.outputFormat { - case "yaml": - history, formattingError = yaml.Marshal(releaseHistory) - case "json": - history, formattingError = json.Marshal(releaseHistory) - case "table": - history = formatAsTable(releaseHistory, o.colWidth) - default: - return errors.Errorf("unknown output format %q", o.outputFormat) - } - - if formattingError != nil { - return formattingError - } - - fmt.Fprintln(out, string(history)) - return nil -} - -func getReleaseHistory(rls []*release.Release) (history releaseHistory) { - for i := len(rls) - 1; i >= 0; i-- { - r := rls[i] - c := formatChartname(r.Chart) - s := r.Info.Status.String() - v := r.Version - d := r.Info.Description - - rInfo := releaseInfo{ - Revision: v, - Status: s, - Chart: c, - Description: d, - } - if !r.Info.LastDeployed.IsZero() { - rInfo.Updated = r.Info.LastDeployed.String() - - } - history = append(history, rInfo) - } - - return history -} - -func formatAsTable(releases releaseHistory, colWidth uint) []byte { - tbl := uitable.New() - - tbl.MaxColWidth = colWidth - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION") - for i := 0; i <= len(releases)-1; i++ { - r := releases[i] - tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.Description) - } - return tbl.Bytes() -} - -func formatChartname(c *chart.Chart) string { - if c == nil || c.Metadata == nil { - // This is an edge case that has happened in prod, though we don't - // know how: https://github.com/helm/helm/issues/1347 - return "MISSING" - } - return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) -} diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index 10dc6fb2d..b04b8109f 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -19,13 +19,12 @@ package main import ( "testing" - rpb "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestHistoryCmd(t *testing.T) { - mk := func(name string, vers int, status rpb.Status) *rpb.Release { - return helm.ReleaseMock(&helm.MockReleaseOptions{ + mk := func(name string, vers int, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ Name: name, Version: vers, Status: status, @@ -35,35 +34,35 @@ func TestHistoryCmd(t *testing.T) { tests := []cmdTestCase{{ name: "get history for release", cmd: "history angry-bird", - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), - mk("angry-bird", 2, rpb.StatusSuperseded), - mk("angry-bird", 1, rpb.StatusSuperseded), + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), + mk("angry-bird", 2, release.StatusSuperseded), + mk("angry-bird", 1, release.StatusSuperseded), }, golden: "output/history.txt", }, { name: "get history with max limit set", cmd: "history angry-bird --max 2", - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, golden: "output/history-limit.txt", }, { name: "get history with yaml output format", cmd: "history angry-bird --output yaml", - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, golden: "output/history.yaml", }, { name: "get history with json output format", cmd: "history angry-bird --output json", - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, golden: "output/history.json", }} diff --git a/cmd/helm/init.go b/cmd/helm/init.go index f665bbe42..57f562b74 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -29,7 +29,7 @@ import ( "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin" "k8s.io/helm/pkg/plugin/installer" "k8s.io/helm/pkg/repo" diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index 9aaa9a27c..964846c4f 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) const testPluginsFile = "testdata/plugins.yaml" diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 18be2d299..de20f0f68 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -17,29 +17,19 @@ limitations under the License. package main import ( - "bytes" - "fmt" "io" - "path/filepath" - "regexp" - "strings" - "text/tabwriter" - "text/template" - "time" - - "github.com/Masterminds/sprig" - "github.com/pkg/errors" + + "k8s.io/helm/pkg/release" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" ) const installDesc = ` @@ -104,268 +94,119 @@ To see the list of chart repositories, use 'helm repo list'. To search for charts in a repository, use 'helm search'. ` -type installOptions struct { - name string // arg 0 - dryRun bool // --dry-run - disableHooks bool // --disable-hooks - replace bool // --replace - nameTemplate string // --name-template - timeout int64 // --timeout - wait bool // --wait - devel bool // --devel - depUp bool // --dep-up - chartPath string // arg 1 - generateName bool // --generate-name - - valuesOptions - chartPathOptions - - cfg *action.Configuration - - // LEGACY: Here until we get upgrade converted - client helm.Interface -} - func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - o := &installOptions{cfg: cfg} + client := action.NewInstall(cfg) cmd := &cobra.Command{ Use: "install [NAME] [CHART]", Short: "install a chart", Long: installDesc, Args: require.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - debug("Original chart version: %q", o.version) - if o.version == "" && o.devel { - debug("setting version to >0.0.0-0") - o.version = ">0.0.0-0" - } - - name, chart, err := o.nameAndChart(args) + RunE: func(_ *cobra.Command, args []string) error { + rel, err := runInstall(args, client, out) if err != nil { return err } - o.name = name // FIXME - - cp, err := o.locateChart(chart) - if err != nil { - return err - } - o.chartPath = cp - - return o.run(out) + action.PrintRelease(out, rel) + return nil }, } - f := cmd.Flags() - f.BoolVarP(&o.generateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") - f.BoolVar(&o.dryRun, "dry-run", false, "simulate an install") - f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during install") - f.BoolVar(&o.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") - f.StringVar(&o.nameTemplate, "name-template", "", "specify template used to name the release") - f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&o.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") - f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.BoolVar(&o.depUp, "dep-up", false, "run helm dependency update before installing the chart") - o.valuesOptions.addFlags(f) - o.chartPathOptions.addFlags(f) + addInstallFlags(cmd.Flags(), client) return cmd } -// nameAndChart returns the name of the release and the chart that should be used. -// -// This will read the flags and handle name generation if necessary. -func (o *installOptions) nameAndChart(args []string) (string, string, error) { - flagsNotSet := func() error { - if o.generateName { - return errors.New("cannot set --generate-name and also specify a name") - } - if o.nameTemplate != "" { - return errors.New("cannot set --name-template and also specify a name") - } - return nil - } - if len(args) == 2 { - return args[0], args[1], flagsNotSet() - } +func addInstallFlags(f *pflag.FlagSet, client *action.Install) { + f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") + f.BoolVar(&client.Replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") + f.Int64Var(&client.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") + f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") + 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") + addValueOptionsFlags(f, &client.ValueOptions) + addChartPathOptionsFlags(f, &client.ChartPathOptions) +} - if o.nameTemplate != "" { - newName, err := templateName(o.nameTemplate) - return newName, args[0], err - } +func addValueOptionsFlags(f *pflag.FlagSet, v *action.ValueOptions) { + f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)") + f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") +} - if !o.generateName { - return "", args[0], errors.New("must either provide a name or specify --generate-name") - } +func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { + f.StringVar(&c.Version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") + f.BoolVar(&c.Verify, "verify", false, "verify the package before installing it") + f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") +} - base := filepath.Base(args[0]) - if base == "." || base == "" { - base = "chart" +func runInstall(args []string, client *action.Install, out io.Writer) (*release.Release, error) { + debug("Original chart version: %q", client.Version) + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" } - newName := fmt.Sprintf("%s-%d", base, time.Now().Unix()) - - return newName, args[0], nil -} -func (o *installOptions) run(out io.Writer) error { - debug("CHART PATH: %s\n", o.chartPath) + name, chart, err := client.NameAndChart(args) + if err != nil { + return nil, err + } + client.ReleaseName = name - rawVals, err := o.mergedValues() + cp, err := client.ChartPathOptions.LocateChart(chart, settings) if err != nil { - return err + return nil, err } - // If template is specified, try to run the template. - if o.nameTemplate != "" { - o.name, err = templateName(o.nameTemplate) - if err != nil { - return err - } - // Print the final name so the user knows what the final name of the release is. - fmt.Fprintf(out, "FINAL NAME: %s\n", o.name) + debug("CHART PATH: %s\n", cp) + + if err := client.ValueOptions.MergeValues(settings); err != nil { + return nil, err } // Check chart dependencies to make sure all are present in /charts - chartRequested, err := loader.Load(o.chartPath) + chartRequested, err := loader.Load(cp) if err != nil { - return err + return nil, err } validInstallableChart, err := chartutil.IsChartInstallable(chartRequested) if !validInstallableChart { - return err + return nil, err } if req := chartRequested.Metadata.Dependencies; req != nil { - // If checkDependencies returns an error, we have unfulfilled dependencies. + // If CheckDependencies returns an error, we have unfulfilled dependencies. // As of Helm 2.4.0, this is treated as a stopping condition: // https://github.com/helm/helm/issues/2209 - if err := checkDependencies(chartRequested, req); err != nil { - if o.depUp { + if err := action.CheckDependencies(chartRequested, req); err != nil { + if client.DependencyUpdate { man := &downloader.Manager{ Out: out, - ChartPath: o.chartPath, + ChartPath: cp, HelmHome: settings.Home, - Keyring: o.keyring, + Keyring: client.ChartPathOptions.Keyring, SkipUpdate: false, Getters: getter.All(settings), } if err := man.Update(); err != nil { - return err + return nil, err } } else { - return err - } - - } - } - - inst := action.NewInstall(o.cfg) - inst.DryRun = o.dryRun - inst.DisableHooks = o.disableHooks - inst.Replace = o.replace - inst.Wait = o.wait - inst.Devel = o.devel - inst.Timeout = o.timeout - inst.Namespace = getNamespace() - inst.ReleaseName = o.name - rel, err := inst.Run(chartRequested, rawVals) - if err != nil { - return err - } - - o.printRelease(out, rel) - return nil -} - -// printRelease prints info about a release -func (o *installOptions) printRelease(out io.Writer, rel *release.Release) { - if rel == nil { - return - } - fmt.Fprintf(out, "NAME: %s\n", rel.Name) - if settings.Debug { - printRelease(out, rel) - } - if !rel.Info.LastDeployed.IsZero() { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed) - } - fmt.Fprintf(out, "NAMESPACE: %s\n", rel.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", rel.Info.Status.String()) - fmt.Fprintf(out, "\n") - if len(rel.Info.Resources) > 0 { - re := regexp.MustCompile(" +") - - w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) - fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(rel.Info.Resources, "\t")) - w.Flush() - } - if rel.Info.LastTestSuiteRun != nil { - lastRun := rel.Info.LastTestSuiteRun - fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", - fmt.Sprintf("Last Started: %s", lastRun.StartedAt), - fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt), - formatTestResults(lastRun.Results)) - } - - if len(rel.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", rel.Info.Notes) - } -} - -// Merges source and destination map, preferring values from the source map -func mergeValues(dest, src map[string]interface{}) map[string]interface{} { - for k, v := range src { - // If the key doesn't exist already, then just set the key to that value - if _, exists := dest[k]; !exists { - dest[k] = v - continue - } - nextMap, ok := v.(map[string]interface{}) - // If it isn't another map, overwrite the value - if !ok { - dest[k] = v - continue - } - // Edge case: If the key exists in the destination, but isn't a map - destMap, isMap := dest[k].(map[string]interface{}) - // If the source map has a map for this key, prefer it - if !isMap { - dest[k] = v - continue - } - // If we got to this point, it is a map in both, so merge them - dest[k] = mergeValues(destMap, nextMap) - } - return dest -} - -func templateName(nameTemplate string) (string, error) { - t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) - if err != nil { - return "", err - } - var b bytes.Buffer - err = t.Execute(&b, nil) - return b.String(), err -} - -func checkDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { - var missing []string - -OUTER: - for _, r := range reqs { - for _, d := range ch.Dependencies() { - if d.Name() == r.Name { - continue OUTER + return nil, err } } - missing = append(missing, r.Name) } - if len(missing) > 0 { - return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) - } - return nil + client.Namespace = getNamespace() + return client.Run(chartRequested) } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 1a42c95f1..38464b585 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -17,8 +17,6 @@ limitations under the License. package main import ( - "reflect" - "regexp" "testing" ) @@ -142,133 +140,3 @@ func TestInstall(t *testing.T) { runTestActionCmd(t, tests) } - -type nameTemplateTestCase struct { - tpl string - expected string - expectedErrorStr string -} - -func TestNameTemplate(t *testing.T) { - testCases := []nameTemplateTestCase{ - // Just a straight up nop please - { - tpl: "foobar", - expected: "foobar", - expectedErrorStr: "", - }, - // Random numbers at the end for fun & profit - { - tpl: "foobar-{{randNumeric 6}}", - expected: "foobar-[0-9]{6}$", - expectedErrorStr: "", - }, - // Random numbers in the middle for fun & profit - { - tpl: "foobar-{{randNumeric 4}}-baz", - expected: "foobar-[0-9]{4}-baz$", - expectedErrorStr: "", - }, - // No such function - { - tpl: "foobar-{{randInt}}", - expected: "", - expectedErrorStr: "function \"randInt\" not defined", - }, - // Invalid template - { - tpl: "foobar-{{", - expected: "", - expectedErrorStr: "unexpected unclosed action", - }, - } - - for _, tc := range testCases { - - n, err := templateName(tc.tpl) - if err != nil { - if tc.expectedErrorStr == "" { - t.Errorf("Was not expecting error, but got: %v", err) - continue - } - re, compErr := regexp.Compile(tc.expectedErrorStr) - if compErr != nil { - t.Errorf("Expected error string failed to compile: %v", compErr) - continue - } - if !re.MatchString(err.Error()) { - t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) - continue - } - } - if err == nil && tc.expectedErrorStr != "" { - t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) - } - - if tc.expected != "" { - re, err := regexp.Compile(tc.expected) - if err != nil { - t.Errorf("Expected string failed to compile: %v", err) - continue - } - if !re.MatchString(n) { - t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) - } - } - } -} - -func TestMergeValues(t *testing.T) { - nestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "stuff", - }, - } - anotherNestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", - }, - } - flatMap := map[string]interface{}{ - "foo": "bar", - "baz": "stuff", - } - anotherFlatMap := map[string]interface{}{ - "testing": "fun", - } - - testMap := mergeValues(flatMap, nestedMap) - equal := reflect.DeepEqual(testMap, nestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) - } - - testMap = mergeValues(nestedMap, flatMap) - equal = reflect.DeepEqual(testMap, flatMap) - if !equal { - t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) - } - - testMap = mergeValues(nestedMap, anotherNestedMap) - equal = reflect.DeepEqual(testMap, anotherNestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) - } - - testMap = mergeValues(anotherFlatMap, anotherNestedMap) - expectedMap := map[string]interface{}{ - "testing": "fun", - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", - }, - } - equal = reflect.DeepEqual(testMap, expectedMap) - if !equal { - t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) - } -} diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 756ed4261..eb6c7402a 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -19,19 +19,12 @@ package main import ( "fmt" "io" - "io/ioutil" - "os" - "path/filepath" "strings" - "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint" - "k8s.io/helm/pkg/lint/support" - "k8s.io/helm/pkg/strvals" + "k8s.io/helm/pkg/action" ) var longLintHelp = ` @@ -43,159 +36,43 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio or recommendation, it will emit [WARNING] messages. ` -type lintOptions struct { - strict bool - paths []string - - valuesOptions -} - func newLintCmd(out io.Writer) *cobra.Command { - o := &lintOptions{paths: []string{"."}} + client := action.NewLint() cmd := &cobra.Command{ Use: "lint PATH", Short: "examines a chart for possible issues", Long: longLintHelp, RunE: func(cmd *cobra.Command, args []string) error { + paths := []string{"."} if len(args) > 0 { - o.paths = args + paths = args } - return o.run(out) - }, - } - - fs := cmd.Flags() - fs.BoolVar(&o.strict, "strict", false, "fail on lint warnings") - o.valuesOptions.addFlags(fs) - - return cmd -} - -var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") - -func (o *lintOptions) run(out io.Writer) error { - var lowestTolerance int - if o.strict { - lowestTolerance = support.WarningSev - } else { - lowestTolerance = support.ErrorSev - } - - // Get the raw values - rvals, err := o.vals() - if err != nil { - return err - } - - var total int - var failures int - for _, path := range o.paths { - if linter, err := lintChart(path, rvals, getNamespace(), o.strict); err != nil { - fmt.Println("==> Skipping", path) - fmt.Println(err) - if err == errLintNoChart { - failures = failures + 1 + client.Namespace = getNamespace() + if err := client.ValueOptions.MergeValues(settings); err != nil { + return err } - } else { - fmt.Println("==> Linting", path) - - if len(linter.Messages) == 0 { - fmt.Println("Lint OK") + result := client.Run(paths) + var message strings.Builder + fmt.Fprintf(&message, "%d chart(s) linted, %d chart(s) failed\n", result.TotalChartsLinted, len(result.Errors)) + for _, err := range result.Errors { + fmt.Fprintf(&message, "\t%s\n", err) } - - for _, msg := range linter.Messages { - fmt.Println(msg) + for _, msg := range result.Messages { + fmt.Fprintf(&message, "\t%s\n", msg) } - total = total + 1 - if linter.HighestSeverity >= lowestTolerance { - failures = failures + 1 + if len(result.Errors) > 0 { + return errors.New(message.String()) } - } - fmt.Println("") - } - - msg := fmt.Sprintf("%d chart(s) linted", total) - if failures > 0 { - return errors.Errorf("%s, %d chart(s) failed", msg, failures) - } - - fmt.Fprintf(out, "%s, no failures\n", msg) - - return nil -} - -func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) { - var chartPath string - linter := support.Linter{} - - if strings.HasSuffix(path, ".tgz") { - tempDir, err := ioutil.TempDir("", "helm-lint") - if err != nil { - return linter, err - } - defer os.RemoveAll(tempDir) - - file, err := os.Open(path) - if err != nil { - return linter, err - } - defer file.Close() - - if err = chartutil.Expand(tempDir, file); err != nil { - return linter, err - } - - lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-") - if lastHyphenIndex <= 0 { - return linter, errors.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path)) - } - base := filepath.Base(path)[:lastHyphenIndex] - chartPath = filepath.Join(tempDir, base) - } else { - chartPath = path - } - - // Guard: Error out of this is not a chart. - if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { - return linter, errLintNoChart - } - - return lint.All(chartPath, vals, namespace, strict), nil -} - -func (o *lintOptions) vals() (map[string]interface{}, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range o.valueFiles { - currentMap := map[string]interface{}{} - bytes, err := ioutil.ReadFile(filePath) - if err != nil { - return base, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return base, errors.Wrapf(err, "failed to parse %s", filePath) - } - // Merge with the previous map - base = mergeValues(base, currentMap) + fmt.Fprintf(out, message.String()) + return nil + }, } - // User specified a value via --set - for _, value := range o.values { - if err := strvals.ParseInto(value, base); err != nil { - return base, errors.Wrap(err, "failed parsing --set data") - } - } + f := cmd.Flags() + f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings") + addValueOptionsFlags(f, &client.ValueOptions) - // User specified a value via --set-string - for _, value := range o.stringValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return base, errors.Wrap(err, "failed parsing --set-string data") - } - } - - return base, nil + return cmd } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 280d4585a..2c3a89cfb 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -19,14 +19,11 @@ package main import ( "fmt" "io" - "strings" - "github.com/gosuri/uitable" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/hapi/release" ) var listHelp = ` @@ -39,11 +36,11 @@ By default, it lists only releases that are deployed or failed. Flags like By default, items are sorted alphabetically. Use the '-d' flag to sort by release date. -If an argument is provided, it will be treated as a filter. Filters are +If the --filter flag is provided, it will be treated as a filter. Filters are regular expressions (Perl compatible) that are applied to the list of releases. Only items that match the filter will be returned. - $ helm list 'ara[a-z]+' + $ helm list --filter 'ara[a-z]+' NAME UPDATED CHART maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0 @@ -56,143 +53,51 @@ server's default, which may be much higher than 256. Pairing the '--max' flag with the '--offset' flag allows you to page through results. ` -type listOptions struct { - // flags - all bool // --all - allNamespaces bool // --all-namespaces - byDate bool // --date - colWidth uint // --col-width - uninstalled bool // --uninstalled - uninstalling bool // --uninstalling - deployed bool // --deployed - failed bool // --failed - limit int // --max - offset int // --offset - pending bool // --pending - short bool // --short - sortDesc bool // --reverse - superseded bool // --superseded - - filter string -} - -func newListCmd(actionConfig *action.Configuration, out io.Writer) *cobra.Command { - o := &listOptions{} +func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewList(cfg) cmd := &cobra.Command{ - Use: "list [FILTER]", + Use: "list", Short: "list releases", Long: listHelp, Aliases: []string{"ls"}, - Args: require.MaximumNArgs(1), + Args: require.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - o.filter = strings.Join(args, " ") - } - - if o.allNamespaces { - actionConfig = newActionConfig(true) - } - - lister := action.NewList(actionConfig) - lister.All = o.limit == -1 - lister.AllNamespaces = o.allNamespaces - lister.Limit = o.limit - lister.Offset = o.offset - lister.Filter = o.filter - - // Set StateMask - lister.StateMask = o.setStateMask() - - // Set sorter - lister.Sort = action.ByNameAsc - if o.sortDesc { - lister.Sort = action.ByNameDesc - } - if o.byDate { - lister.Sort = action.ByDate + if client.AllNamespaces { + client.SetConfiguration(newActionConfig(true)) } + client.All = client.Limit == -1 + client.SetStateMask() - results, err := lister.Run() + results, err := client.Run() - if o.short { + if client.Short { for _, res := range results { fmt.Fprintln(out, res.Name) } return err } - fmt.Fprintln(out, formatList(results, 90)) + fmt.Fprintln(out, action.FormatList(results)) return err }, } f := cmd.Flags() - f.BoolVarP(&o.short, "short", "q", false, "output short (quiet) listing format") - f.BoolVarP(&o.byDate, "date", "d", false, "sort by release date") - f.BoolVarP(&o.sortDesc, "reverse", "r", false, "reverse the sort order") - f.IntVarP(&o.limit, "max", "m", 256, "maximum number of releases to fetch") - f.IntVarP(&o.offset, "offset", "o", 0, "next release name in the list, used to offset from start value") - f.BoolVarP(&o.all, "all", "a", false, "show all releases, not just the ones marked deployed") - f.BoolVar(&o.uninstalled, "uninstalled", false, "show uninstalled releases") - f.BoolVar(&o.superseded, "superseded", false, "show superseded releases") - f.BoolVar(&o.uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") - f.BoolVar(&o.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") - f.BoolVar(&o.failed, "failed", false, "show failed releases") - f.BoolVar(&o.pending, "pending", false, "show pending releases") - f.UintVar(&o.colWidth, "col-width", 60, "specifies the max column width of output") - f.BoolVar(&o.allNamespaces, "all-namespaces", false, "list releases across all namespaces") + f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") + f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") + f.BoolVarP(&client.SortDesc, "reverse", "r", false, "reverse the sort order") + f.BoolVarP(&client.All, "all", "a", false, "show all releases, not just the ones marked deployed") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases") + f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") + f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") + f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") + f.BoolVar(&client.Failed, "failed", false, "show failed releases") + f.BoolVar(&client.Pending, "pending", false, "show pending releases") + f.BoolVar(&client.AllNamespaces, "all-namespaces", false, "list releases across all namespaces") + f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") + f.IntVarP(&client.Offset, "offset", "o", 0, "next release name in the list, used to offset from start value") + f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results") return cmd } - -// setStateMask calculates the state mask based on parameters. -func (o *listOptions) setStateMask() action.ListStates { - if o.all { - return action.ListAll - } - - state := action.ListStates(0) - if o.deployed { - state |= action.ListDeployed - } - if o.uninstalled { - state |= action.ListUninstalled - } - if o.uninstalling { - state |= action.ListUninstalling - } - if o.pending { - state |= action.ListPendingInstall | action.ListPendingRollback | action.ListPendingUpgrade - } - if o.failed { - state |= action.ListFailed - } - - // Apply a default - if state == 0 { - return action.ListDeployed | action.ListFailed - } - - return state -} - -func formatList(rels []*release.Release, colWidth uint) string { - table := uitable.New() - - table.MaxColWidth = colWidth - table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE") - for _, r := range rels { - md := r.Chart.Metadata - c := fmt.Sprintf("%s-%s", md.Name, md.Version) - t := "-" - if tspb := r.Info.LastDeployed; !tspb.IsZero() { - t = tspb.String() - } - s := r.Info.Status.String() - v := r.Version - n := r.Namespace - table.AddRow(r.Name, v, t, s, c, n) - } - return table.String() -} diff --git a/cmd/helm/options.go b/cmd/helm/options.go deleted file mode 100644 index 8d68cf214..000000000 --- a/cmd/helm/options.go +++ /dev/null @@ -1,225 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main // import "k8s.io/helm/cmd/helm" - -import ( - "io/ioutil" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/ghodss/yaml" - "github.com/pkg/errors" - "github.com/spf13/pflag" - - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/strvals" -) - -// ----------------------------------------------------------------------------- -// Values Options - -type valuesOptions struct { - valueFiles []string // --values - values []string // --set - stringValues []string // --set-string -} - -func (o *valuesOptions) addFlags(fs *pflag.FlagSet) { - fs.StringSliceVarP(&o.valueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)") - fs.StringArrayVar(&o.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - fs.StringArrayVar(&o.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") -} - -// mergeValues merges values from files specified via -f/--values and -// directly via --set or --set-string, marshaling them to YAML -func (o *valuesOptions) mergedValues() (map[string]interface{}, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range o.valueFiles { - currentMap := map[string]interface{}{} - - bytes, err := readFile(filePath) - if err != nil { - return base, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return base, errors.Wrapf(err, "failed to parse %s", filePath) - } - // Merge with the previous map - base = mergeValues(base, currentMap) - } - - // User specified a value via --set - for _, value := range o.values { - if err := strvals.ParseInto(value, base); err != nil { - return base, errors.Wrap(err, "failed parsing --set data") - } - } - - // User specified a value via --set-string - for _, value := range o.stringValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return base, errors.Wrap(err, "failed parsing --set-string data") - } - } - - return base, nil -} - -// readFile load a file from stdin, the local directory, or a remote file with a url. -func readFile(filePath string) ([]byte, error) { - if strings.TrimSpace(filePath) == "-" { - return ioutil.ReadAll(os.Stdin) - } - u, _ := url.Parse(filePath) - p := getter.All(settings) - - // FIXME: maybe someone handle other protocols like ftp. - getterConstructor, err := p.ByScheme(u.Scheme) - - if err != nil { - return ioutil.ReadFile(filePath) - } - - getter, err := getterConstructor(filePath, "", "", "") - if err != nil { - return []byte{}, err - } - data, err := getter.Get(filePath) - return data.Bytes(), err -} - -// ----------------------------------------------------------------------------- -// Chart Path Options - -type chartPathOptions struct { - caFile string // --ca-file - certFile string // --cert-file - keyFile string // --key-file - keyring string // --keyring - password string // --password - repoURL string // --repo - username string // --username - verify bool // --verify - version string // --version -} - -// defaultKeyring returns the expanded path to the default keyring. -func defaultKeyring() string { - if v, ok := os.LookupEnv("GNUPGHOME"); ok { - return filepath.Join(v, "pubring.gpg") - } - return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") -} - -func (o *chartPathOptions) addFlags(fs *pflag.FlagSet) { - fs.StringVar(&o.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") - fs.BoolVar(&o.verify, "verify", false, "verify the package before installing it") - fs.StringVar(&o.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") - fs.StringVar(&o.repoURL, "repo", "", "chart repository url where to locate the requested chart") - fs.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart") - fs.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart") - fs.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - fs.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - fs.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") -} - -func (o *chartPathOptions) locateChart(name string) (string, error) { - return locateChartPath(o.repoURL, o.username, o.password, name, o.version, o.keyring, o.certFile, o.keyFile, o.caFile, o.verify) -} - -// locateChartPath looks for a chart directory in known places, and returns either the full path or an error. -// -// This does not ensure that the chart is well-formed; only that the requested filename exists. -// -// Order of resolution: -// - relative to current working directory -// - if path is absolute or begins with '.', error out here -// - chart repos in $HELM_HOME -// - URL -// -// If 'verify' is true, this will attempt to also verify the chart. -func locateChartPath(repoURL, username, password, name, version, keyring, - certFile, keyFile, caFile string, verify bool) (string, error) { - name = strings.TrimSpace(name) - version = strings.TrimSpace(version) - - if _, err := os.Stat(name); err == nil { - abs, err := filepath.Abs(name) - if err != nil { - return abs, err - } - if verify { - if _, err := downloader.VerifyChart(abs, keyring); err != nil { - return "", err - } - } - return abs, nil - } - if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { - return name, errors.Errorf("path %q not found", name) - } - - crepo := filepath.Join(settings.Home.Repository(), name) - if _, err := os.Stat(crepo); err == nil { - return filepath.Abs(crepo) - } - - dl := downloader.ChartDownloader{ - HelmHome: settings.Home, - Out: os.Stdout, - Keyring: keyring, - Getters: getter.All(settings), - Username: username, - Password: password, - } - if verify { - dl.Verify = downloader.VerifyAlways - } - if repoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, - certFile, keyFile, caFile, getter.All(settings)) - if err != nil { - return "", err - } - name = chartURL - } - - if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { - os.MkdirAll(settings.Home.Archive(), 0744) - } - - filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) - if err == nil { - lname, err := filepath.Abs(filename) - if err != nil { - return filename, err - } - debug("Fetched %s to %s\n", name, filename) - return lname, nil - } else if settings.Debug { - return filename, err - } - - return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) -} diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 04395607d..efd35b067 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -20,22 +20,15 @@ import ( "fmt" "io" "io/ioutil" - "os" "path/filepath" - "syscall" - "github.com/Masterminds/semver" + "k8s.io/helm/pkg/action" + "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" - "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/provenance" ) const packageDesc = ` @@ -49,186 +42,68 @@ Chart.yaml file, and (if found) build the current directory into a chart. Versioned chart archives are used by Helm package repositories. ` -type packageOptions struct { - appVersion string // --app-version - dependencyUpdate bool // --dependency-update - destination string // --destination - key string // --key - keyring string // --keyring - sign bool // --sign - version string // --version - - valuesOptions - - path string - - home helmpath.Home -} - func newPackageCmd(out io.Writer) *cobra.Command { - o := &packageOptions{} + client := action.NewPackage() cmd := &cobra.Command{ Use: "package [CHART_PATH] [...]", Short: "package a chart directory into a chart archive", Long: packageDesc, RunE: func(cmd *cobra.Command, args []string) error { - o.home = settings.Home if len(args) == 0 { return errors.Errorf("need at least one argument, the path to the chart") } - if o.sign { - if o.key == "" { + if client.Sign { + if client.Key == "" { return errors.New("--key is required for signing a package") } - if o.keyring == "" { + if client.Keyring == "" { return errors.New("--keyring is required for signing a package") } } + if err := client.ValueOptions.MergeValues(settings); err != nil { + return err + } + for i := 0; i < len(args); i++ { - o.path = args[i] - if err := o.run(out); err != nil { + path, err := filepath.Abs(args[i]) + if err != nil { + return err + } + + if client.DependencyUpdate { + downloadManager := &downloader.Manager{ + Out: ioutil.Discard, + ChartPath: path, + HelmHome: settings.Home, + Keyring: client.Keyring, + Getters: getter.All(settings), + Debug: settings.Debug, + } + + if err := downloadManager.Update(); err != nil { + return err + } + } + p, err := client.Run(path) + if err != nil { return err } + fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p) } return nil }, } f := cmd.Flags() - f.BoolVar(&o.sign, "sign", false, "use a PGP private key to sign this package") - f.StringVar(&o.key, "key", "", "name of the key to use when signing. Used if --sign is true") - f.StringVar(&o.keyring, "keyring", defaultKeyring(), "location of a public keyring") - f.StringVar(&o.version, "version", "", "set the version on the chart to this semver version") - f.StringVar(&o.appVersion, "app-version", "", "set the appVersion on the chart to this version") - f.StringVarP(&o.destination, "destination", "d", ".", "location to write the chart.") - f.BoolVarP(&o.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) - o.valuesOptions.addFlags(f) + f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") + f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") + f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") + f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") + f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) + addValueOptionsFlags(f, &client.ValueOptions) return cmd } - -func (o *packageOptions) run(out io.Writer) error { - path, err := filepath.Abs(o.path) - if err != nil { - return err - } - - if o.dependencyUpdate { - downloadManager := &downloader.Manager{ - Out: out, - ChartPath: path, - HelmHome: settings.Home, - Keyring: o.keyring, - Getters: getter.All(settings), - Debug: settings.Debug, - } - - if err := downloadManager.Update(); err != nil { - return err - } - } - - ch, err := loader.LoadDir(path) - if err != nil { - return err - } - - validChartType, err := chartutil.IsValidChartType(ch) - if !validChartType { - return err - } - - overrideVals, err := o.mergedValues() - if err != nil { - return err - } - combinedVals, err := chartutil.CoalesceValues(ch, overrideVals) - if err != nil { - return err - } - ch.Values = combinedVals - - // If version is set, modify the version. - if len(o.version) != 0 { - if err := setVersion(ch, o.version); err != nil { - return err - } - debug("Setting version to %s", o.version) - } - - if o.appVersion != "" { - ch.Metadata.AppVersion = o.appVersion - debug("Setting appVersion to %s", o.appVersion) - } - - if reqs := ch.Metadata.Dependencies; reqs != nil { - if err := checkDependencies(ch, reqs); err != nil { - return err - } - } - - var dest string - if o.destination == "." { - // Save to the current working directory. - dest, err = os.Getwd() - if err != nil { - return err - } - } else { - // Otherwise save to set destination - dest = o.destination - } - - name, err := chartutil.Save(ch, dest) - if err != nil { - return errors.Wrap(err, "failed to save") - } - fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", name) - - if o.sign { - err = o.clearsign(name) - } - - return err -} - -func setVersion(ch *chart.Chart, ver string) error { - // Verify that version is a Version, and error out if it is not. - if _, err := semver.NewVersion(ver); err != nil { - return err - } - - // Set the version field on the chart. - ch.Metadata.Version = ver - return nil -} - -func (o *packageOptions) clearsign(filename string) error { - // Load keyring - signer, err := provenance.NewFromKeyring(o.keyring, o.key) - if err != nil { - return err - } - - if err := signer.DecryptKey(promptUser); err != nil { - return err - } - - sig, err := signer.ClearSign(filename) - if err != nil { - return err - } - - debug(sig) - - return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) -} - -// promptUser implements provenance.PassphraseFetcher -func promptUser(name string) ([]byte, error) { - fmt.Printf("Password for key %q > ", name) - pw, err := terminal.ReadPassword(int(syscall.Stdin)) - fmt.Println() - return pw, err -} diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index c7976ad80..d83b02c05 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -31,30 +31,9 @@ import ( "k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) -func TestSetVersion(t *testing.T) { - c := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "prow", - Version: "0.0.1", - }, - } - expect := "1.2.3-beta.5" - if err := setVersion(c, expect); err != nil { - t.Fatal(err) - } - - if c.Metadata.Version != expect { - t.Errorf("Expected %q, got %q", expect, c.Metadata.Version) - } - - if err := setVersion(c, "monkeyface"); err == nil { - t.Error("Expected bogus version to return an error.") - } -} - func TestPackage(t *testing.T) { statExe := "stat" statFileMsg := "no such file or directory" diff --git a/cmd/helm/plugin_install.go b/cmd/helm/plugin_install.go index 72be003af..ca054b618 100644 --- a/cmd/helm/plugin_install.go +++ b/cmd/helm/plugin_install.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin" "k8s.io/helm/pkg/plugin/installer" ) diff --git a/cmd/helm/plugin_list.go b/cmd/helm/plugin_list.go index a81f59be2..b9f7f545f 100644 --- a/cmd/helm/plugin_list.go +++ b/cmd/helm/plugin_list.go @@ -22,7 +22,7 @@ import ( "github.com/gosuri/uitable" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) type pluginListOptions struct { diff --git a/cmd/helm/plugin_remove.go b/cmd/helm/plugin_remove.go index 30e3c092c..82e863bdc 100644 --- a/cmd/helm/plugin_remove.go +++ b/cmd/helm/plugin_remove.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin" ) diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index e5eba1abb..dd4742449 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin" ) diff --git a/cmd/helm/plugin_update.go b/cmd/helm/plugin_update.go index a84312eb0..13c9bff06 100644 --- a/cmd/helm/plugin_update.go +++ b/cmd/helm/plugin_update.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin" "k8s.io/helm/pkg/plugin/installer" ) diff --git a/cmd/helm/printer.go b/cmd/helm/printer.go index 8cd013075..8f7677930 100644 --- a/cmd/helm/printer.go +++ b/cmd/helm/printer.go @@ -24,7 +24,7 @@ import ( "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) var printReleaseTemplate = `REVISION: {{.Release.Version}} diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go index 570a69ed0..cd1adf8c2 100644 --- a/cmd/helm/pull.go +++ b/cmd/helm/pull.go @@ -19,18 +19,11 @@ package main import ( "fmt" "io" - "io/ioutil" - "os" - "path/filepath" - "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/action" ) const pullDesc = ` @@ -48,20 +41,8 @@ file, and MUST pass the verification process. Failure in any part of this will result in an error, and the chart will not be saved locally. ` -type pullOptions struct { - destdir string // --destination - devel bool // --devel - untar bool // --untar - untardir string // --untardir - verifyLater bool // --prov - - chartRef string - - chartPathOptions -} - func newPullCmd(out io.Writer) *cobra.Command { - o := &pullOptions{} + client := action.NewPull() cmd := &cobra.Command{ Use: "pull [chart URL | repo/chartname] [...]", @@ -70,95 +51,30 @@ func newPullCmd(out io.Writer) *cobra.Command { Long: pullDesc, Args: require.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if o.version == "" && o.devel { + client.Settings = settings + if client.Version == "" && client.Devel { debug("setting version to >0.0.0-0") - o.version = ">0.0.0-0" + client.Version = ">0.0.0-0" } for i := 0; i < len(args); i++ { - o.chartRef = args[i] - if err := o.run(out); err != nil { + output, err := client.Run(args[i]) + if err != nil { return err } + fmt.Fprint(out, output) } return nil }, } f := cmd.Flags() - f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.BoolVar(&o.untar, "untar", false, "if set to true, will untar the chart after downloading it") - f.BoolVar(&o.verifyLater, "prov", false, "fetch the provenance file, but don't perform verification") - f.StringVar(&o.untardir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") - f.StringVarP(&o.destdir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") - - o.chartPathOptions.addFlags(f) + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it") + f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification") + f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") + f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") + addChartPathOptionsFlags(f, &client.ChartPathOptions) return cmd } - -func (o *pullOptions) run(out io.Writer) error { - c := downloader.ChartDownloader{ - HelmHome: settings.Home, - Out: out, - Keyring: o.keyring, - Verify: downloader.VerifyNever, - Getters: getter.All(settings), - Username: o.username, - Password: o.password, - } - - if o.verify { - c.Verify = downloader.VerifyAlways - } else if o.verifyLater { - c.Verify = downloader.VerifyLater - } - - // If untar is set, we fetch to a tempdir, then untar and copy after - // verification. - dest := o.destdir - if o.untar { - var err error - dest, err = ioutil.TempDir("", "helm-") - if err != nil { - return errors.Wrap(err, "failed to untar") - } - defer os.RemoveAll(dest) - } - - if o.repoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(o.repoURL, o.username, o.password, o.chartRef, o.version, o.certFile, o.keyFile, o.caFile, getter.All(settings)) - if err != nil { - return err - } - o.chartRef = chartURL - } - - saved, v, err := c.DownloadTo(o.chartRef, o.version, dest) - if err != nil { - return err - } - - if o.verify { - fmt.Fprintf(out, "Verification: %v\n", v) - } - - // After verification, untar the chart into the requested directory. - if o.untar { - ud := o.untardir - if !filepath.IsAbs(ud) { - ud = filepath.Join(o.destdir, ud) - } - if fi, err := os.Stat(ud); err != nil { - if err := os.MkdirAll(ud, 0755); err != nil { - return errors.Wrap(err, "failed to untar (mkdir)") - } - - } else if !fi.IsDir() { - return errors.Errorf("failed to untar: %s is not a directory", ud) - } - - return chartutil.ExpandFile(ud, saved) - } - return nil -} diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index f837df627..feba9fe5c 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -130,7 +130,7 @@ func TestPullCmd(t *testing.T) { os.Mkdir(outdir, 0755) cmd := strings.Join(append(tt.args, "-d", "'"+outdir+"'", "--home", "'"+hh.String()+"'"), " ") - out, err := executeCommand(nil, "fetch "+cmd) + _, out, err := executeActionCommand("fetch " + cmd) if err != nil { if tt.wantError { continue diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index 08d3b0383..bd27a618d 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -24,8 +24,8 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" + "k8s.io/helm/pkg/release" ) const releaseTestDesc = ` @@ -35,15 +35,8 @@ The argument this command takes is the name of a deployed release. The tests to be run are defined in the chart that was installed. ` -type releaseTestOptions struct { - name string - client helm.Interface - timeout int64 - cleanup bool -} - -func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { - o := &releaseTestOptions{client: c} +func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewReleaseTesting(cfg) cmd := &cobra.Command{ Use: "test [RELEASE]", @@ -51,47 +44,37 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: releaseTestDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.name = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + c, errc := client.Run(args[0]) + testErr := &testErr{} + + for { + select { + case err := <-errc: + if err == nil && testErr.failed > 0 { + return testErr.Error() + } + return err + case res, ok := <-c: + if !ok { + break + } + + if res.Status == release.TestRunFailure { + testErr.failed++ + } + fmt.Fprintf(out, res.Msg+"\n") + } + } }, } f := cmd.Flags() - f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&o.cleanup, "cleanup", false, "delete test pods upon completion") + f.Int64Var(&client.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.Cleanup, "cleanup", false, "delete test pods upon completion") return cmd } -func (o *releaseTestOptions) run(out io.Writer) (err error) { - c, errc := o.client.RunReleaseTest( - o.name, - helm.ReleaseTestTimeout(o.timeout), - helm.ReleaseTestCleanup(o.cleanup), - ) - testErr := &testErr{} - - for { - select { - case err := <-errc: - if err == nil && testErr.failed > 0 { - return testErr.Error() - } - return err - case res, ok := <-c: - if !ok { - break - } - - if res.Status == release.TestRunFailure { - testErr.failed++ - } - fmt.Fprintf(out, res.Msg+"\n") - } - } -} - type testErr struct { failed int } diff --git a/cmd/helm/release_testing_test.go b/cmd/helm/release_testing_test.go index 28be860b4..d228f175b 100644 --- a/cmd/helm/release_testing_test.go +++ b/cmd/helm/release_testing_test.go @@ -18,49 +18,79 @@ package main import ( "testing" + "time" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) func TestReleaseTesting(t *testing.T) { + timestamp := time.Unix(1452902400, 0).UTC() + tests := []cmdTestCase{{ - name: "basic test", - cmd: "test example-release", - testRunStatus: map[string]release.TestRunStatus{"PASSED: green lights everywhere": release.TestRunSuccess}, - golden: "output/test.txt", - }, { - name: "test failure", - cmd: "test example-fail", - testRunStatus: map[string]release.TestRunStatus{"FAILURE: red lights everywhere": release.TestRunFailure}, - wantError: true, - golden: "output/test-failure.txt", + name: "successful test", + cmd: "status test-success", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{ + Name: "test-success", + TestSuiteResults: []*release.TestRun{ + { + Name: "test-success", + Status: release.TestRunSuccess, + StartedAt: timestamp, + CompletedAt: timestamp, + }, + }, + })}, + golden: "output/test-success.txt", }, { - name: "test unknown", - cmd: "test example-unknown", - testRunStatus: map[string]release.TestRunStatus{"UNKNOWN: yellow lights everywhere": release.TestRunUnknown}, - golden: "output/test-unknown.txt", + name: "test failure", + cmd: "status test-failure", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{ + Name: "test-failure", + TestSuiteResults: []*release.TestRun{ + { + Name: "test-failure", + Status: release.TestRunFailure, + StartedAt: timestamp, + CompletedAt: timestamp, + }, + }, + })}, + golden: "output/test-failure.txt", }, { - name: "test error", - cmd: "test example-error", - testRunStatus: map[string]release.TestRunStatus{"ERROR: yellow lights everywhere": release.TestRunFailure}, - wantError: true, - golden: "output/test-error.txt", + name: "test unknown", + cmd: "status test-unknown", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{ + Name: "test-unknown", + TestSuiteResults: []*release.TestRun{ + { + Name: "test-unknown", + Status: release.TestRunUnknown, + StartedAt: timestamp, + CompletedAt: timestamp, + }, + }, + })}, + golden: "output/test-unknown.txt", }, { - name: "test running", - cmd: "test example-running", - testRunStatus: map[string]release.TestRunStatus{"RUNNING: things are happpeningggg": release.TestRunRunning}, - golden: "output/test-running.txt", + name: "test running", + cmd: "status test-running", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{ + Name: "test-running", + TestSuiteResults: []*release.TestRun{ + { + Name: "test-running", + Status: release.TestRunRunning, + StartedAt: timestamp, + CompletedAt: timestamp, + }, + }, + })}, + golden: "output/test-running.txt", }, { - name: "multiple tests example", - cmd: "test example-suite", - testRunStatus: map[string]release.TestRunStatus{ - "RUNNING: things are happpeningggg": release.TestRunRunning, - "PASSED: party time": release.TestRunSuccess, - "RUNNING: things are happening again": release.TestRunRunning, - "FAILURE: good thing u checked :)": release.TestRunFailure, - "RUNNING: things are happpeningggg yet again": release.TestRunRunning, - "PASSED: feel free to party again": release.TestRunSuccess}, - wantError: true, + name: "test with no tests", + cmd: "test no-tests", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "no-tests"})}, + golden: "output/test-no-tests.txt", }} runTestCmd(t, tests) } diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index e249c0cea..ad43f3430 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -25,7 +25,7 @@ import ( "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 775d80ea6..9eecef166 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index 1622dd086..4755a7224 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go index c805851ff..b704e4c84 100644 --- a/cmd/helm/repo_update.go +++ b/cmd/helm/repo_update.go @@ -26,7 +26,7 @@ import ( "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index d7001f31c..de62d03bb 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -24,7 +24,7 @@ import ( "testing" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo/repotest" ) diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index 521141e11..f691fbfb9 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -19,13 +19,11 @@ package main import ( "fmt" "io" - "strconv" - "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) const rollbackDesc = ` @@ -36,20 +34,8 @@ second is a revision (version) number. To see revision numbers, run 'helm history RELEASE'. ` -type rollbackOptions struct { - name string - revision int - dryRun bool - recreate bool - force bool - disableHooks bool - client helm.Interface - timeout int64 - wait bool -} - -func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { - o := &rollbackOptions{client: c} +func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewRollback(cfg) cmd := &cobra.Command{ Use: "rollback [RELEASE] [REVISION]", @@ -57,45 +43,25 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: rollbackDesc, Args: require.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - o.name = args[0] - - v64, err := strconv.ParseInt(args[1], 10, 32) + _, err := client.Run(args[0]) if err != nil { - return errors.Wrapf(err, "invalid revision number '%q'", args[1]) + return err } - o.revision = int(v64) - o.client = ensureHelmClient(o.client, false) - return o.run(out) + fmt.Fprintf(out, "Rollback was a success! Happy Helming!\n") + + return nil }, } f := cmd.Flags() - f.BoolVar(&o.dryRun, "dry-run", false, "simulate a rollback") - f.BoolVar(&o.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") - f.BoolVar(&o.force, "force", false, "force resource update through delete/recreate if needed") - f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") - f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&o.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.IntVarP(&client.Version, "version", "v", 0, "revision number to rollback to (default: rollback to previous release)") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback") + f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback") + f.Int64Var(&client.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") return cmd } - -func (o *rollbackOptions) run(out io.Writer) error { - _, err := o.client.RollbackRelease( - o.name, - helm.RollbackDryRun(o.dryRun), - helm.RollbackRecreate(o.recreate), - helm.RollbackForce(o.force), - helm.RollbackDisableHooks(o.disableHooks), - helm.RollbackVersion(o.revision), - helm.RollbackTimeout(o.timeout), - helm.RollbackWait(o.wait)) - if err != nil { - return err - } - - fmt.Fprintf(out, "Rollback was a success! Happy Helming!\n") - - return nil -} diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index 862f7521b..3898ce6a9 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -18,25 +18,47 @@ package main import ( "testing" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/release" ) func TestRollbackCmd(t *testing.T) { + rels := []*release.Release{ + { + Name: "funny-honey", + Info: &release.Info{Status: release.StatusSuperseded}, + Chart: &chart.Chart{}, + Version: 1, + }, + { + Name: "funny-honey", + Info: &release.Info{Status: release.StatusDeployed}, + Chart: &chart.Chart{}, + Version: 2, + }, + } + tests := []cmdTestCase{{ name: "rollback a release", cmd: "rollback funny-honey 1", golden: "output/rollback.txt", + rels: rels, }, { name: "rollback a release with timeout", cmd: "rollback funny-honey 1 --timeout 120", golden: "output/rollback-timeout.txt", + rels: rels, }, { name: "rollback a release with wait", cmd: "rollback funny-honey 1 --wait", golden: "output/rollback-wait.txt", + rels: rels, }, { name: "rollback a release without revision", cmd: "rollback funny-honey", golden: "output/rollback-no-args.txt", + rels: rels, wantError: true, }} runTestCmd(t, tests) diff --git a/cmd/helm/root.go b/cmd/helm/root.go index ec47071d7..d1ef58a05 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -24,7 +24,6 @@ import ( "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/action" - "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/registry" ) @@ -50,8 +49,7 @@ Environment: $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") ` -// TODO: 'c helm.Interface' is deprecated in favor of actionConfig -func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command { +func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command { cmd := &cobra.Command{ Use: "helm", Short: "The Helm package manager for Kubernetes.", @@ -92,15 +90,15 @@ func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Wri newChartCmd(actionConfig, out), // release commands - newGetCmd(c, out), - newHistoryCmd(c, out), + newGetCmd(actionConfig, out), + newHistoryCmd(actionConfig, out), newInstallCmd(actionConfig, out), newListCmd(actionConfig, out), - newReleaseTestCmd(c, out), - newRollbackCmd(c, out), - newStatusCmd(c, out), - newUninstallCmd(c, out), - newUpgradeCmd(c, out), + newReleaseTestCmd(actionConfig, out), + newRollbackCmd(actionConfig, out), + newStatusCmd(actionConfig, out), + newUninstallCmd(actionConfig, out), + newUpgradeCmd(actionConfig, out), newCompletionCmd(out), newHomeCmd(out), diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go index 163b24bb6..a8bec6354 100644 --- a/cmd/helm/root_test.go +++ b/cmd/helm/root_test.go @@ -77,7 +77,7 @@ func TestRootCmd(t *testing.T) { os.Setenv(k, v) } - cmd, _, err := executeCommandC(nil, tt.args) + cmd, _, err := executeActionCommand(tt.args) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 784df42a2..3ef9ed1d7 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -27,7 +27,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/search" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/cmd/helm/show.go b/cmd/helm/show.go index 491477922..d8970f077 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -19,14 +19,11 @@ package main import ( "fmt" "io" - "strings" - "github.com/ghodss/yaml" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" + "k8s.io/helm/pkg/action" ) const showDesc = ` @@ -51,24 +48,8 @@ This command inspects a chart (directory, file, or URL) and displays the content of the README file ` -type showOptions struct { - chartpath string - output string - - chartPathOptions -} - -const ( - chartOnly = "chart" - valuesOnly = "values" - readmeOnly = "readme" - all = "all" -) - -var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} - func newShowCmd(out io.Writer) *cobra.Command { - o := &showOptions{output: all} + client := action.NewShow(action.ShowAll) showCommand := &cobra.Command{ Use: "show [CHART]", @@ -77,12 +58,16 @@ func newShowCmd(out io.Writer) *cobra.Command { Long: showDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cp, err := o.locateChart(args[0]) + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) if err != nil { return err } - o.chartpath = cp - return o.run(out) + output, err := client.Run(cp) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil }, } @@ -92,13 +77,17 @@ func newShowCmd(out io.Writer) *cobra.Command { Long: showValuesDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.output = valuesOnly - cp, err := o.locateChart(args[0]) + client.OutputFormat = action.ShowValues + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) if err != nil { return err } - o.chartpath = cp - return o.run(out) + output, err := client.Run(cp) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil }, } @@ -108,13 +97,17 @@ func newShowCmd(out io.Writer) *cobra.Command { Long: showChartDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.output = chartOnly - cp, err := o.locateChart(args[0]) + client.OutputFormat = action.ShowChart + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) if err != nil { return err } - o.chartpath = cp - return o.run(out) + output, err := client.Run(cp) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil }, } @@ -124,19 +117,23 @@ func newShowCmd(out io.Writer) *cobra.Command { Long: readmeChartDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.output = readmeOnly - cp, err := o.locateChart(args[0]) + client.OutputFormat = action.ShowReadme + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) if err != nil { return err } - o.chartpath = cp - return o.run(out) + output, err := client.Run(cp) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil }, } cmds := []*cobra.Command{showCommand, readmeSubCmd, valuesSubCmd, chartSubCmd} for _, subCmd := range cmds { - o.chartPathOptions.addFlags(subCmd.Flags()) + addChartPathOptionsFlags(subCmd.Flags(), &client.ChartPathOptions) } for _, subCmd := range cmds[1:] { @@ -145,52 +142,3 @@ func newShowCmd(out io.Writer) *cobra.Command { return showCommand } - -func (i *showOptions) run(out io.Writer) error { - chrt, err := loader.Load(i.chartpath) - if err != nil { - return err - } - cf, err := yaml.Marshal(chrt.Metadata) - if err != nil { - return err - } - - if i.output == chartOnly || i.output == all { - fmt.Fprintln(out, string(cf)) - } - - if (i.output == valuesOnly || i.output == all) && chrt.Values != nil { - if i.output == all { - fmt.Fprintln(out, "---") - } - b, err := yaml.Marshal(chrt.Values) - if err != nil { - return err - } - fmt.Fprintln(out, string(b)) - } - - if i.output == readmeOnly || i.output == all { - if i.output == all { - fmt.Fprintln(out, "---") - } - readme := findReadme(chrt.Files) - if readme == nil { - return nil - } - fmt.Fprintln(out, string(readme.Data)) - } - return nil -} - -func findReadme(files []*chart.File) (file *chart.File) { - for _, file := range files { - for _, n := range readmeFileNames { - if strings.EqualFold(file.Name, n) { - return file - } - } - } - return nil -} diff --git a/cmd/helm/status.go b/cmd/helm/status.go index a1cea897b..0c6bee728 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -18,20 +18,14 @@ package main import ( "encoding/json" - "fmt" "io" - "regexp" - "text/tabwriter" "github.com/ghodss/yaml" - "github.com/gosuri/uitable" - "github.com/gosuri/uitable/util/strutil" "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) var statusHelp = ` @@ -45,15 +39,8 @@ The status consists of: - additional notes provided by the chart ` -type statusOptions struct { - release string - client helm.Interface - version int - outfmt string -} - -func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &statusOptions{client: client} +func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewStatus(cfg) cmd := &cobra.Command{ Use: "status RELEASE_NAME", @@ -61,88 +48,44 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: statusHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.release = args[0] - o.client = ensureHelmClient(o.client, false) - return o.run(out) + rel, err := client.Run(args[0]) + if err != nil { + return err + } + + outfmt, err := action.ParseOutputFormat(client.OutputFormat) + // We treat an invalid format type as the default + if err != nil && err != action.ErrInvalidFormatType { + return err + } + + switch outfmt { + case "": + action.PrintRelease(out, rel) + return nil + case action.JSON: + data, err := json.Marshal(rel) + if err != nil { + return errors.Wrap(err, "failed to Marshal JSON output") + } + out.Write(data) + return nil + case action.YAML: + data, err := yaml.Marshal(rel) + if err != nil { + return errors.Wrap(err, "failed to Marshal YAML output") + } + out.Write(data) + return nil + default: + return errors.Errorf("unknown output format %q", outfmt) + } }, } - cmd.PersistentFlags().IntVar(&o.version, "revision", 0, "if set, display the status of the named release with revision") - cmd.PersistentFlags().StringVarP(&o.outfmt, "output", "o", "", "output the status in the specified format (json or yaml)") + f := cmd.PersistentFlags() + f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision") + f.StringVarP(&client.OutputFormat, "output", "o", "", "output the status in the specified format (json or yaml)") return cmd } - -func (o *statusOptions) run(out io.Writer) error { - res, err := o.client.ReleaseContent(o.release, o.version) - if err != nil { - return err - } - - switch o.outfmt { - case "": - PrintStatus(out, res) - return nil - case "json": - data, err := json.Marshal(res) - if err != nil { - return errors.Wrap(err, "failed to Marshal JSON output") - } - out.Write(data) - return nil - case "yaml": - data, err := yaml.Marshal(res) - if err != nil { - return errors.Wrap(err, "failed to Marshal YAML output") - } - out.Write(data) - return nil - } - - return errors.Errorf("unknown output format %q", o.outfmt) -} - -// PrintStatus prints out the status of a release. Shared because also used by -// install / upgrade -func PrintStatus(out io.Writer, res *release.Release) { - if !res.Info.LastDeployed.IsZero() { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", res.Info.LastDeployed) - } - fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.String()) - fmt.Fprintf(out, "\n") - if len(res.Info.Resources) > 0 { - re := regexp.MustCompile(" +") - - w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) - fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Resources, "\t")) - w.Flush() - } - if res.Info.LastTestSuiteRun != nil { - lastRun := res.Info.LastTestSuiteRun - fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", - fmt.Sprintf("Last Started: %s", lastRun.StartedAt), - fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt), - formatTestResults(lastRun.Results)) - } - - if len(res.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Notes) - } -} - -func formatTestResults(results []*release.TestRun) string { - tbl := uitable.New() - tbl.MaxColWidth = 50 - tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED") - for i := 0; i < len(results); i++ { - r := results[i] - n := r.Name - s := strutil.PadRight(r.Status.String(), 10, ' ') - i := r.Info - ts := r.StartedAt - tc := r.CompletedAt - tbl.AddRow(n, s, i, ts, tc) - } - return tbl.String() -} diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 8b7f41cb6..fe4f460a7 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -20,15 +20,18 @@ import ( "testing" "time" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/chart" + + "k8s.io/helm/pkg/release" ) func TestStatusCmd(t *testing.T) { releasesMockWithStatus := func(info *release.Info) []*release.Release { info.LastDeployed = time.Unix(1452902400, 0).UTC() return []*release.Release{{ - Name: "flummoxed-chickadee", - Info: info, + Name: "flummoxed-chickadee", + Info: info, + Chart: &chart.Chart{}, }} } diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 06a7f355c..f33237dea 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -19,39 +19,23 @@ package main import ( "fmt" "io" - "os" - "path" - "path/filepath" - "regexp" + "io/ioutil" "strings" - "time" - "github.com/Masterminds/semver" - "github.com/pkg/errors" "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes/fake" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chart/loader" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/hapi/release" - util "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/tiller" -) - -const defaultDirectoryPermission = 0755 - -var ( - whitespaceRegex = regexp.MustCompile(`^\s*$`) - - // defaultKubeVersion is the default value of --kube-version flag - defaultKubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor) + "k8s.io/helm/pkg/action" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/storage" + "k8s.io/helm/pkg/storage/driver" ) const templateDesc = ` Render chart templates locally and display the output. -This does not require Tiller. However, any values that would normally be +This does not require Helm. However, any values that would normally be looked up or retrieved in-cluster will be faked locally. Additionally, none of the server-side testing of chart validity (e.g. whether an API is supported) is done. @@ -61,232 +45,38 @@ To render just one template in a chart, use '-x': $ helm template mychart -x templates/deployment.yaml ` -type templateOptions struct { - nameTemplate string // --name-template - showNotes bool // --notes - releaseName string // --name - renderFiles []string // --execute - kubeVersion string // --kube-version - outputDir string // --output-dir - - valuesOptions - - chartPath string -} - func newTemplateCmd(out io.Writer) *cobra.Command { - o := &templateOptions{} + customConfig := &action.Configuration{ + // Add mock objects in here so it doesn't use Kube API server + Releases: storage.Init(driver.NewMemory()), + KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, + Discovery: fake.NewSimpleClientset().Discovery(), + Log: func(format string, v ...interface{}) { + fmt.Fprintf(out, format, v...) + }, + } + + client := action.NewInstall(customConfig) cmd := &cobra.Command{ Use: "template CHART", Short: fmt.Sprintf("locally render templates"), Long: templateDesc, Args: require.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // verify chart path exists - if _, err := os.Stat(args[0]); err == nil { - if o.chartPath, err = filepath.Abs(args[0]); err != nil { - return err - } - } else { + RunE: func(_ *cobra.Command, args []string) error { + client.DryRun = true + client.ReleaseName = "RELEASE-NAME" + client.Replace = true // Skip the name check + rel, err := runInstall(args, client, out) + if err != nil { return err } - return o.run(out) + fmt.Fprintln(out, strings.TrimSpace(rel.Manifest)) + return nil }, } - f := cmd.Flags() - f.BoolVar(&o.showNotes, "notes", false, "show the computed NOTES.txt file as well") - f.StringVarP(&o.releaseName, "name", "", "RELEASE-NAME", "release name") - f.StringArrayVarP(&o.renderFiles, "execute", "x", []string{}, "only execute the given templates") - f.StringVar(&o.nameTemplate, "name-template", "", "specify template used to name the release") - f.StringVar(&o.kubeVersion, "kube-version", defaultKubeVersion, "kubernetes version used as Capabilities.KubeVersion.Major/Minor") - f.StringVar(&o.outputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") - o.valuesOptions.addFlags(f) + addInstallFlags(cmd.Flags(), client) return cmd } - -func (o *templateOptions) run(out io.Writer) error { - // verify specified templates exist relative to chart - rf := []string{} - var af string - var err error - if len(o.renderFiles) > 0 { - for _, f := range o.renderFiles { - if !filepath.IsAbs(f) { - af, err = filepath.Abs(filepath.Join(o.chartPath, f)) - if err != nil { - return errors.Wrap(err, "could not resolve template path") - } - } else { - af = f - } - rf = append(rf, af) - - if _, err := os.Stat(af); err != nil { - return errors.Wrap(err, "could not resolve template path") - } - } - } - - // verify that output-dir exists if provided - if o.outputDir != "" { - if _, err := os.Stat(o.outputDir); os.IsNotExist(err) { - return errors.Errorf("output-dir '%s' does not exist", o.outputDir) - } - } - - // get combined values and create config - config, err := o.mergedValues() - if err != nil { - return err - } - - // If template is specified, try to run the template. - if o.nameTemplate != "" { - o.releaseName, err = templateName(o.nameTemplate) - if err != nil { - return err - } - } - - // Check chart dependencies to make sure all are present in /charts - c, err := loader.Load(o.chartPath) - if err != nil { - return err - } - - validInstallableChart, err := chartutil.IsChartInstallable(c) - if !validInstallableChart { - return err - } - - if req := c.Metadata.Dependencies; req != nil { - if err := checkDependencies(c, req); err != nil { - return err - } - } - options := chartutil.ReleaseOptions{ - Name: o.releaseName, - } - - if err := chartutil.ProcessDependencies(c, config); err != nil { - return err - } - - // kubernetes version - kv, err := semver.NewVersion(o.kubeVersion) - if err != nil { - return errors.Wrap(err, "could not parse a kubernetes version") - } - - caps := chartutil.DefaultCapabilities - caps.KubeVersion.Major = fmt.Sprint(kv.Major()) - caps.KubeVersion.Minor = fmt.Sprint(kv.Minor()) - caps.KubeVersion.GitVersion = fmt.Sprintf("v%d.%d.0", kv.Major(), kv.Minor()) - - vals, err := chartutil.ToRenderValues(c, config, options, caps) - if err != nil { - return err - } - - rendered, err := engine.Render(c, vals) - if err != nil { - return err - } - - listManifests := []tiller.Manifest{} - // extract kind and name - re := regexp.MustCompile("kind:(.*)\n") - for k, v := range rendered { - match := re.FindStringSubmatch(v) - h := "Unknown" - if len(match) == 2 { - h = strings.TrimSpace(match[1]) - } - m := tiller.Manifest{Name: k, Content: v, Head: &util.SimpleHead{Kind: h}} - listManifests = append(listManifests, m) - } - in := func(needle string, haystack []string) bool { - // make needle path absolute - // NOTE: chart manifest names always use backslashes as path separators, even on Windows - d := strings.Split(needle, "/") - dd := d[1:] - an := filepath.Join(o.chartPath, strings.Join(dd, string(os.PathSeparator))) - - for _, h := range haystack { - if h == an { - return true - } - } - return false - } - - if settings.Debug { - rel := &release.Release{ - Name: o.releaseName, - Chart: c, - Config: config, - Version: 1, - Info: &release.Info{LastDeployed: time.Now()}, - } - printRelease(out, rel) - } - - for _, m := range tiller.SortByKind(listManifests) { - b := filepath.Base(m.Name) - switch { - case len(o.renderFiles) > 0 && !in(m.Name, rf): - continue - case !o.showNotes && b == "NOTES.txt": - continue - case strings.HasPrefix(b, "_"): - continue - case whitespaceRegex.MatchString(m.Content): - // blank template after execution - continue - case o.outputDir != "": - if err := writeToFile(out, o.outputDir, m.Name, m.Content); err != nil { - return err - } - default: - fmt.Fprintf(out, "---\n# Source: %s\n", m.Name) - fmt.Fprintln(out, m.Content) - } - } - return nil -} - -// write the to / -func writeToFile(out io.Writer, outputDir, name, data string) error { - outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) - - if err := ensureDirectoryForFile(outfileName); err != nil { - return err - } - - f, err := os.Create(outfileName) - if err != nil { - return err - } - - defer f.Close() - - if _, err = f.WriteString(fmt.Sprintf("##---\n# Source: %s\n%s", name, data)); err != nil { - return err - } - - fmt.Fprintf(out, "wrote %s\n", outfileName) - return nil -} - -// check if the directory exists to create file. creates if don't exists -func ensureDirectoryForFile(file string) error { - baseDir := path.Dir(file) - if _, err := os.Stat(baseDir); err != nil && !os.IsNotExist(err) { - return err - } - - return os.MkdirAll(baseDir, defaultDirectoryPermission) -} diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index d34409e44..65586c394 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -25,10 +25,6 @@ import ( var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1" func TestTemplateCmd(t *testing.T) { - absChartPath, err := filepath.Abs(chartPath) - if err != nil { - t.Fatal(err) - } tests := []cmdTestCase{ { name: "check name", @@ -37,24 +33,9 @@ func TestTemplateCmd(t *testing.T) { }, { name: "check set name", - cmd: fmt.Sprintf("template '%s' -x '%s' --set service.name=apache", chartPath, filepath.Join("templates", "service.yaml")), + cmd: fmt.Sprintf("template '%s' --set service.name=apache", chartPath), golden: "output/template-set.txt", }, - { - name: "check execute absolute", - cmd: fmt.Sprintf("template '%s' -x '%s' --set service.name=apache", chartPath, filepath.Join(absChartPath, "templates", "service.yaml")), - golden: "output/template-absolute.txt", - }, - { - name: "check release name", - cmd: fmt.Sprintf("template '%s' --name test", chartPath), - golden: "output/template-name.txt", - }, - { - name: "check notes", - cmd: fmt.Sprintf("template '%s' --notes", chartPath), - golden: "output/template-notes.txt", - }, { name: "check values files", cmd: fmt.Sprintf("template '%s' --values '%s'", chartPath, filepath.Join(chartPath, "/charts/subchartA/values.yaml")), @@ -65,11 +46,6 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" }}-baz'`, chartPath), golden: "output/template-name-template.txt", }, - { - name: "check kube version", - cmd: fmt.Sprintf("template '%s' --kube-version 1.6", chartPath), - golden: "output/template-kube-version.txt", - }, { name: "check no args", cmd: "template", diff --git a/cmd/helm/testdata/output/dependency-list-no-chart-windows.txt b/cmd/helm/testdata/output/dependency-list-no-chart-windows.txt deleted file mode 100644 index 8127e347d..000000000 --- a/cmd/helm/testdata/output/dependency-list-no-chart-windows.txt +++ /dev/null @@ -1 +0,0 @@ -Error: FindFirstFile \no\such\chart: The system cannot find the path specified. diff --git a/cmd/helm/testdata/output/dependency-list-no-requirements-windows.txt b/cmd/helm/testdata/output/dependency-list-no-requirements-windows.txt deleted file mode 100644 index c1ba49d24..000000000 --- a/cmd/helm/testdata/output/dependency-list-no-requirements-windows.txt +++ /dev/null @@ -1 +0,0 @@ -WARNING: no dependencies at testdata\testcharts\alpine\charts diff --git a/cmd/helm/testdata/output/install-and-replace.txt b/cmd/helm/testdata/output/install-and-replace.txt index 4df13b70e..21fc9db6e 100644 --- a/cmd/helm/testdata/output/install-and-replace.txt +++ b/cmd/helm/testdata/output/install-and-replace.txt @@ -1,4 +1,4 @@ -NAME: aeneas +NAME: aeneas LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-name-template.txt b/cmd/helm/testdata/output/install-name-template.txt index d52fe60c5..63ea1d4c3 100644 --- a/cmd/helm/testdata/output/install-name-template.txt +++ b/cmd/helm/testdata/output/install-name-template.txt @@ -1,5 +1,4 @@ -FINAL NAME: FOOBAR -NAME: FOOBAR +NAME: FOOBAR LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-no-hooks.txt b/cmd/helm/testdata/output/install-no-hooks.txt index 4df13b70e..21fc9db6e 100644 --- a/cmd/helm/testdata/output/install-no-hooks.txt +++ b/cmd/helm/testdata/output/install-no-hooks.txt @@ -1,4 +1,4 @@ -NAME: aeneas +NAME: aeneas LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-multiple-values-files.txt b/cmd/helm/testdata/output/install-with-multiple-values-files.txt index 4f3c82375..05c879353 100644 --- a/cmd/helm/testdata/output/install-with-multiple-values-files.txt +++ b/cmd/helm/testdata/output/install-with-multiple-values-files.txt @@ -1,4 +1,4 @@ -NAME: virgil +NAME: virgil LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-multiple-values.txt b/cmd/helm/testdata/output/install-with-multiple-values.txt index 4f3c82375..05c879353 100644 --- a/cmd/helm/testdata/output/install-with-multiple-values.txt +++ b/cmd/helm/testdata/output/install-with-multiple-values.txt @@ -1,4 +1,4 @@ -NAME: virgil +NAME: virgil LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-timeout.txt b/cmd/helm/testdata/output/install-with-timeout.txt index dfa30eed0..ac2bb5550 100644 --- a/cmd/helm/testdata/output/install-with-timeout.txt +++ b/cmd/helm/testdata/output/install-with-timeout.txt @@ -1,4 +1,4 @@ -NAME: foobar +NAME: foobar LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-values-file.txt b/cmd/helm/testdata/output/install-with-values-file.txt index 4f3c82375..05c879353 100644 --- a/cmd/helm/testdata/output/install-with-values-file.txt +++ b/cmd/helm/testdata/output/install-with-values-file.txt @@ -1,4 +1,4 @@ -NAME: virgil +NAME: virgil LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-values.txt b/cmd/helm/testdata/output/install-with-values.txt index 4f3c82375..05c879353 100644 --- a/cmd/helm/testdata/output/install-with-values.txt +++ b/cmd/helm/testdata/output/install-with-values.txt @@ -1,4 +1,4 @@ -NAME: virgil +NAME: virgil LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install-with-wait.txt b/cmd/helm/testdata/output/install-with-wait.txt index 1955d4714..0ad49af8c 100644 --- a/cmd/helm/testdata/output/install-with-wait.txt +++ b/cmd/helm/testdata/output/install-with-wait.txt @@ -1,4 +1,4 @@ -NAME: apollo +NAME: apollo LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/install.txt b/cmd/helm/testdata/output/install.txt index 4df13b70e..21fc9db6e 100644 --- a/cmd/helm/testdata/output/install.txt +++ b/cmd/helm/testdata/output/install.txt @@ -1,4 +1,4 @@ -NAME: aeneas +NAME: aeneas LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/list-with-failed.txt b/cmd/helm/testdata/output/list-with-failed.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-failed.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-deleting.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-namespaced.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt b/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-mulitple-flags-pending.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags.txt b/cmd/helm/testdata/output/list-with-mulitple-flags.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-mulitple-flags.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-mulitple-flags2.txt b/cmd/helm/testdata/output/list-with-mulitple-flags2.txt deleted file mode 100644 index 6cacd15a0..000000000 --- a/cmd/helm/testdata/output/list-with-mulitple-flags2.txt +++ /dev/null @@ -1,2 +0,0 @@ -atlas-guide -thomas-guide diff --git a/cmd/helm/testdata/output/list-with-old-releases.txt b/cmd/helm/testdata/output/list-with-old-releases.txt deleted file mode 100644 index 0a0d11d3f..000000000 --- a/cmd/helm/testdata/output/list-with-old-releases.txt +++ /dev/null @@ -1,3 +0,0 @@ -NAME REVISION UPDATED STATUS CHART NAMESPACE -thomas-guide 1 1977-09-02 22:04:05 +0000 UTC deployed foo-0.1.0-beta.1 default -thomas-guide 2 1977-09-02 22:04:05 +0000 UTC failed foo-0.1.0-beta.1 default diff --git a/cmd/helm/testdata/output/list-with-pending.txt b/cmd/helm/testdata/output/list-with-pending.txt deleted file mode 100644 index c90e0e93d..000000000 --- a/cmd/helm/testdata/output/list-with-pending.txt +++ /dev/null @@ -1,4 +0,0 @@ -atlas-guide -crazy-maps -thomas-guide -wild-idea diff --git a/cmd/helm/testdata/output/list-with-release.txt b/cmd/helm/testdata/output/list-with-release.txt deleted file mode 100644 index 11b3397b5..000000000 --- a/cmd/helm/testdata/output/list-with-release.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME REVISION UPDATED STATUS CHART NAMESPACE -thomas-guide 1 1977-09-02 22:04:05 +0000 UTC deployed foo-0.1.0-beta.1 default diff --git a/cmd/helm/testdata/output/list.txt b/cmd/helm/testdata/output/list.txt deleted file mode 100644 index 819f60f6d..000000000 --- a/cmd/helm/testdata/output/list.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME REVISION UPDATED STATUS CHART NAMESPACE -atlas 1 1977-09-02 22:04:05 +0000 UTC deployed foo-0.1.0-beta.1 default diff --git a/cmd/helm/testdata/output/status-with-notes.txt b/cmd/helm/testdata/output/status-with-notes.txt index 280855bc7..22a34368b 100644 --- a/cmd/helm/testdata/output/status-with-notes.txt +++ b/cmd/helm/testdata/output/status-with-notes.txt @@ -1,3 +1,4 @@ +NAME: flummoxed-chickadee LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC NAMESPACE: STATUS: deployed diff --git a/cmd/helm/testdata/output/status-with-resource.txt b/cmd/helm/testdata/output/status-with-resource.txt index ec2d9f8e6..b6aa27c66 100644 --- a/cmd/helm/testdata/output/status-with-resource.txt +++ b/cmd/helm/testdata/output/status-with-resource.txt @@ -1,3 +1,4 @@ +NAME: flummoxed-chickadee LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC NAMESPACE: STATUS: deployed diff --git a/cmd/helm/testdata/output/status-with-test-suite.txt b/cmd/helm/testdata/output/status-with-test-suite.txt index 63012498a..057cab434 100644 --- a/cmd/helm/testdata/output/status-with-test-suite.txt +++ b/cmd/helm/testdata/output/status-with-test-suite.txt @@ -1,3 +1,4 @@ +NAME: flummoxed-chickadee LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC NAMESPACE: STATUS: deployed diff --git a/cmd/helm/testdata/output/status.txt b/cmd/helm/testdata/output/status.txt index 7ac60bede..cbd76fba2 100644 --- a/cmd/helm/testdata/output/status.txt +++ b/cmd/helm/testdata/output/status.txt @@ -1,3 +1,4 @@ +NAME: flummoxed-chickadee LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC NAMESPACE: STATUS: deployed diff --git a/cmd/helm/testdata/output/template-absolute.txt b/cmd/helm/testdata/output/template-absolute.txt deleted file mode 100644 index eede3b0b2..000000000 --- a/cmd/helm/testdata/output/template-absolute.txt +++ /dev/null @@ -1,22 +0,0 @@ ---- -# Source: subchart1/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart1 - labels: - chart: "subchart1-0.1.0" - release-name: "RELEASE-NAME" - kube-version/major: "1" - kube-version/minor: "9" - kube-version/gitversion: "v1.9.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app: subchart1 - diff --git a/cmd/helm/testdata/output/template-kube-version.txt b/cmd/helm/testdata/output/template-kube-version.txt deleted file mode 100644 index 21c9b7aa7..000000000 --- a/cmd/helm/testdata/output/template-kube-version.txt +++ /dev/null @@ -1,58 +0,0 @@ ---- -# Source: subchart1/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app: subcharta - ---- -# Source: subchart1/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchartb - ---- -# Source: subchart1/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart1 - labels: - chart: "subchart1-0.1.0" - release-name: "RELEASE-NAME" - kube-version/major: "1" - kube-version/minor: "6" - kube-version/gitversion: "v1.6.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchart1 - diff --git a/cmd/helm/testdata/output/template-name-template.txt b/cmd/helm/testdata/output/template-name-template.txt index 27e9afa72..56105bb75 100644 --- a/cmd/helm/testdata/output/template-name-template.txt +++ b/cmd/helm/testdata/output/template-name-template.txt @@ -15,7 +15,6 @@ spec: name: apache selector: app: subcharta - --- # Source: subchart1/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -33,7 +32,6 @@ spec: name: nginx selector: app: subchartb - --- # Source: subchart1/templates/service.yaml apiVersion: v1 @@ -55,4 +53,3 @@ spec: name: nginx selector: app: subchart1 - diff --git a/cmd/helm/testdata/output/template-name.txt b/cmd/helm/testdata/output/template-name.txt deleted file mode 100644 index a7fc5c286..000000000 --- a/cmd/helm/testdata/output/template-name.txt +++ /dev/null @@ -1,58 +0,0 @@ ---- -# Source: subchart1/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app: subcharta - ---- -# Source: subchart1/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchartb - ---- -# Source: subchart1/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart1 - labels: - chart: "subchart1-0.1.0" - release-name: "test" - kube-version/major: "1" - kube-version/minor: "9" - kube-version/gitversion: "v1.9.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchart1 - diff --git a/cmd/helm/testdata/output/template-notes.txt b/cmd/helm/testdata/output/template-notes.txt deleted file mode 100644 index 812e36802..000000000 --- a/cmd/helm/testdata/output/template-notes.txt +++ /dev/null @@ -1,61 +0,0 @@ ---- -# Source: subchart1/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app: subcharta - ---- -# Source: subchart1/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchartb - ---- -# Source: subchart1/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart1 - labels: - chart: "subchart1-0.1.0" - release-name: "RELEASE-NAME" - kube-version/major: "1" - kube-version/minor: "9" - kube-version/gitversion: "v1.9.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app: subchart1 - ---- -# Source: subchart1/templates/NOTES.txt -Sample notes for subchart1 diff --git a/cmd/helm/testdata/output/template-set.txt b/cmd/helm/testdata/output/template-set.txt index eede3b0b2..4e4643976 100644 --- a/cmd/helm/testdata/output/template-set.txt +++ b/cmd/helm/testdata/output/template-set.txt @@ -1,4 +1,38 @@ --- +# Source: subchart1/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app: subcharta +--- +# Source: subchart1/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app: subchartb +--- # Source: subchart1/templates/service.yaml apiVersion: v1 kind: Service @@ -19,4 +53,3 @@ spec: name: apache selector: app: subchart1 - diff --git a/cmd/helm/testdata/output/template-values-files.txt b/cmd/helm/testdata/output/template-values-files.txt index bcb93148a..4e4643976 100644 --- a/cmd/helm/testdata/output/template-values-files.txt +++ b/cmd/helm/testdata/output/template-values-files.txt @@ -15,7 +15,6 @@ spec: name: apache selector: app: subcharta - --- # Source: subchart1/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -33,7 +32,6 @@ spec: name: nginx selector: app: subchartb - --- # Source: subchart1/templates/service.yaml apiVersion: v1 @@ -55,4 +53,3 @@ spec: name: apache selector: app: subchart1 - diff --git a/cmd/helm/testdata/output/template.txt b/cmd/helm/testdata/output/template.txt index c9fb07138..40b05a1f6 100644 --- a/cmd/helm/testdata/output/template.txt +++ b/cmd/helm/testdata/output/template.txt @@ -15,7 +15,6 @@ spec: name: apache selector: app: subcharta - --- # Source: subchart1/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -33,7 +32,6 @@ spec: name: nginx selector: app: subchartb - --- # Source: subchart1/templates/service.yaml apiVersion: v1 @@ -55,4 +53,3 @@ spec: name: nginx selector: app: subchart1 - diff --git a/cmd/helm/testdata/output/test-error.txt b/cmd/helm/testdata/output/test-error.txt deleted file mode 100644 index 8a80a0492..000000000 --- a/cmd/helm/testdata/output/test-error.txt +++ /dev/null @@ -1,2 +0,0 @@ -ERROR: yellow lights everywhere -Error: 1 test(s) failed diff --git a/cmd/helm/testdata/output/test-failure.txt b/cmd/helm/testdata/output/test-failure.txt index aed8bd8c3..93ae567be 100644 --- a/cmd/helm/testdata/output/test-failure.txt +++ b/cmd/helm/testdata/output/test-failure.txt @@ -1,2 +1,11 @@ -FAILURE: red lights everywhere -Error: 1 test(s) failed +NAME: test-failure +LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC +NAMESPACE: default +STATUS: deployed + +TEST SUITE: +Last Started: 1977-09-02 22:04:05 +0000 UTC +Last Completed: 1977-09-02 22:04:05 +0000 UTC + +TEST STATUS INFO STARTED COMPLETED +test-failure failure 2016-01-16 00:00:00 +0000 UTC 2016-01-16 00:00:00 +0000 UTC diff --git a/cmd/helm/testdata/output/test-no-tests.txt b/cmd/helm/testdata/output/test-no-tests.txt new file mode 100644 index 000000000..fe5e07c3c --- /dev/null +++ b/cmd/helm/testdata/output/test-no-tests.txt @@ -0,0 +1 @@ +No Tests Found diff --git a/cmd/helm/testdata/output/test-running.txt b/cmd/helm/testdata/output/test-running.txt index da4e5e285..0061033df 100644 --- a/cmd/helm/testdata/output/test-running.txt +++ b/cmd/helm/testdata/output/test-running.txt @@ -1 +1,11 @@ -RUNNING: things are happpeningggg +NAME: test-running +LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC +NAMESPACE: default +STATUS: deployed + +TEST SUITE: +Last Started: 1977-09-02 22:04:05 +0000 UTC +Last Completed: 1977-09-02 22:04:05 +0000 UTC + +TEST STATUS INFO STARTED COMPLETED +test-running running 2016-01-16 00:00:00 +0000 UTC 2016-01-16 00:00:00 +0000 UTC diff --git a/cmd/helm/testdata/output/test-success.txt b/cmd/helm/testdata/output/test-success.txt new file mode 100644 index 000000000..99b6afdc5 --- /dev/null +++ b/cmd/helm/testdata/output/test-success.txt @@ -0,0 +1,11 @@ +NAME: test-success +LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC +NAMESPACE: default +STATUS: deployed + +TEST SUITE: +Last Started: 1977-09-02 22:04:05 +0000 UTC +Last Completed: 1977-09-02 22:04:05 +0000 UTC + +TEST STATUS INFO STARTED COMPLETED +test-success success 2016-01-16 00:00:00 +0000 UTC 2016-01-16 00:00:00 +0000 UTC diff --git a/cmd/helm/testdata/output/test-unknown.txt b/cmd/helm/testdata/output/test-unknown.txt index 0003f3d25..4b0cde7eb 100644 --- a/cmd/helm/testdata/output/test-unknown.txt +++ b/cmd/helm/testdata/output/test-unknown.txt @@ -1 +1,11 @@ -UNKNOWN: yellow lights everywhere +NAME: test-unknown +LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC +NAMESPACE: default +STATUS: deployed + +TEST SUITE: +Last Started: 1977-09-02 22:04:05 +0000 UTC +Last Completed: 1977-09-02 22:04:05 +0000 UTC + +TEST STATUS INFO STARTED COMPLETED +test-unknown unknown 2016-01-16 00:00:00 +0000 UTC 2016-01-16 00:00:00 +0000 UTC diff --git a/cmd/helm/testdata/output/test.txt b/cmd/helm/testdata/output/test.txt deleted file mode 100644 index 26876ac88..000000000 --- a/cmd/helm/testdata/output/test.txt +++ /dev/null @@ -1 +0,0 @@ -PASSED: green lights everywhere diff --git a/cmd/helm/testdata/output/upgrade-with-install-timeout.txt b/cmd/helm/testdata/output/upgrade-with-install-timeout.txt index 11d66d8d3..fb64f1f86 100644 --- a/cmd/helm/testdata/output/upgrade-with-install-timeout.txt +++ b/cmd/helm/testdata/output/upgrade-with-install-timeout.txt @@ -1,5 +1,11 @@ Release "crazy-bunny" has been upgraded. Happy Helming! +NAME: crazy-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=crazy-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade-with-install.txt b/cmd/helm/testdata/output/upgrade-with-install.txt index 95cc1f625..8aba923a8 100644 --- a/cmd/helm/testdata/output/upgrade-with-install.txt +++ b/cmd/helm/testdata/output/upgrade-with-install.txt @@ -1,5 +1,11 @@ Release "zany-bunny" has been upgraded. Happy Helming! +NAME: zany-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=zany-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade-with-reset-values.txt b/cmd/helm/testdata/output/upgrade-with-reset-values.txt index 53227b192..4b0f9c309 100644 --- a/cmd/helm/testdata/output/upgrade-with-reset-values.txt +++ b/cmd/helm/testdata/output/upgrade-with-reset-values.txt @@ -1,5 +1,11 @@ Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade-with-reset-values2.txt b/cmd/helm/testdata/output/upgrade-with-reset-values2.txt index 53227b192..4b0f9c309 100644 --- a/cmd/helm/testdata/output/upgrade-with-reset-values2.txt +++ b/cmd/helm/testdata/output/upgrade-with-reset-values2.txt @@ -1,5 +1,11 @@ Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade-with-timeout.txt b/cmd/helm/testdata/output/upgrade-with-timeout.txt index 53227b192..4b0f9c309 100644 --- a/cmd/helm/testdata/output/upgrade-with-timeout.txt +++ b/cmd/helm/testdata/output/upgrade-with-timeout.txt @@ -1,5 +1,11 @@ Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade-with-wait.txt b/cmd/helm/testdata/output/upgrade-with-wait.txt index 11d66d8d3..fb64f1f86 100644 --- a/cmd/helm/testdata/output/upgrade-with-wait.txt +++ b/cmd/helm/testdata/output/upgrade-with-wait.txt @@ -1,5 +1,11 @@ Release "crazy-bunny" has been upgraded. Happy Helming! +NAME: crazy-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=crazy-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/testdata/output/upgrade.txt b/cmd/helm/testdata/output/upgrade.txt index 53227b192..4b0f9c309 100644 --- a/cmd/helm/testdata/output/upgrade.txt +++ b/cmd/helm/testdata/output/upgrade.txt @@ -1,5 +1,11 @@ Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default STATUS: deployed +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 diff --git a/cmd/helm/uninstall.go b/cmd/helm/uninstall.go index 863a62fbb..a52d151e4 100644 --- a/cmd/helm/uninstall.go +++ b/cmd/helm/uninstall.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/action" ) const uninstallDesc = ` @@ -34,20 +34,8 @@ Use the '--dry-run' flag to see which releases will be uninstalled without actua uninstalling them. ` -type uninstallOptions struct { - disableHooks bool // --no-hooks - dryRun bool // --dry-run - purge bool // --purge - timeout int64 // --timeout - - // args - name string - - client helm.Interface -} - -func newUninstallCmd(c helm.Interface, out io.Writer) *cobra.Command { - o := &uninstallOptions{client: c} +func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewUninstall(cfg) cmd := &cobra.Command{ Use: "uninstall RELEASE_NAME [...]", @@ -57,40 +45,27 @@ func newUninstallCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: uninstallDesc, Args: require.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.client = ensureHelmClient(o.client, false) - for i := 0; i < len(args); i++ { - o.name = args[i] - if err := o.run(out); err != nil { + + res, err := client.Run(args[0]) + if err != nil { return err } + if res != nil && res.Info != "" { + fmt.Fprintln(out, res.Info) + } - fmt.Fprintf(out, "release \"%s\" uninstalled\n", o.name) + fmt.Fprintf(out, "release \"%s\" uninstalled\n", args[i]) } return nil }, } f := cmd.Flags() - f.BoolVar(&o.dryRun, "dry-run", false, "simulate a uninstall") - f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") - f.BoolVar(&o.purge, "purge", false, "remove the release from the store and make its name free for later use") - f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") + f.BoolVar(&client.Purge, "purge", false, "remove the release from the store and make its name free for later use") + f.Int64Var(&client.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") return cmd } - -func (o *uninstallOptions) run(out io.Writer) error { - opts := []helm.UninstallOption{ - helm.UninstallDryRun(o.dryRun), - helm.UninstallDisableHooks(o.disableHooks), - helm.UninstallPurge(o.purge), - helm.UninstallTimeout(o.timeout), - } - res, err := o.client.UninstallRelease(o.name, opts...) - if res != nil && res.Info != "" { - fmt.Fprintln(out, res.Info) - } - - return err -} diff --git a/cmd/helm/uninstall_test.go b/cmd/helm/uninstall_test.go index d83174479..8012a8d92 100644 --- a/cmd/helm/uninstall_test.go +++ b/cmd/helm/uninstall_test.go @@ -19,38 +19,34 @@ package main import ( "testing" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestUninstall(t *testing.T) { - - rels := []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})} - tests := []cmdTestCase{ { name: "basic uninstall", cmd: "uninstall aeneas", golden: "output/uninstall.txt", - rels: rels, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, { name: "uninstall with timeout", cmd: "uninstall aeneas --timeout 120", golden: "output/uninstall-timeout.txt", - rels: rels, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, { name: "uninstall without hooks", cmd: "uninstall aeneas --no-hooks", golden: "output/uninstall-no-hooks.txt", - rels: rels, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, { name: "purge", cmd: "uninstall aeneas --purge", golden: "output/uninstall-purge.txt", - rels: rels, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, { name: "uninstall without release", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 975bc8d99..a5079e91e 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -24,8 +24,8 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" + "k8s.io/helm/pkg/action" "k8s.io/helm/pkg/chart/loader" - "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/storage/driver" ) @@ -54,30 +54,8 @@ set for a key called 'foo', the 'newbar' value would take precedence: $ helm upgrade --set foo=bar --set foo=newbar redis ./redis ` -type upgradeOptions struct { - devel bool // --devel - disableHooks bool // --disable-hooks - dryRun bool // --dry-run - force bool // --force - install bool // --install - recreate bool // --recreate-pods - resetValues bool // --reset-values - reuseValues bool // --reuse-values - timeout int64 // --timeout - wait bool // --wait - maxHistory int // --max-history - - valuesOptions - chartPathOptions - - release string - chart string - - client helm.Interface -} - -func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { - o := &upgradeOptions{client: client} +func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewUpgrade(cfg) cmd := &cobra.Command{ Use: "upgrade [RELEASE] [CHART]", @@ -85,102 +63,92 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { Long: upgradeDesc, Args: require.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - if o.version == "" && o.devel { + client.Namespace = getNamespace() + + if client.Version == "" && client.Devel { debug("setting version to >0.0.0-0") - o.version = ">0.0.0-0" + client.Version = ">0.0.0-0" } - o.release = args[0] - o.chart = args[1] - o.client = ensureHelmClient(o.client, false) + if err := client.ValueOptions.MergeValues(settings); err != nil { + return err + } - return o.run(out) - }, - } + chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings) + if err != nil { + return err + } - f := cmd.Flags() - f.BoolVar(&o.dryRun, "dry-run", false, "simulate an upgrade") - f.BoolVar(&o.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") - f.BoolVar(&o.force, "force", false, "force resource update through delete/recreate if needed") - f.BoolVar(&o.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks") - f.BoolVar(&o.disableHooks, "no-hooks", false, "disable pre/post upgrade hooks") - f.BoolVarP(&o.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") - f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&o.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") - f.BoolVar(&o.reuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored.") - f.BoolVar(&o.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") - f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.IntVar(&o.maxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") - o.valuesOptions.addFlags(f) - o.chartPathOptions.addFlags(f) + if client.Install { + // If a release does not exist, install it. If another error occurs during + // the check, ignore the error and continue with the upgrade. + histClient := action.NewHistory(cfg) + histClient.Max = 1 + if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound { + fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) + instClient := action.NewInstall(cfg) + instClient.ChartPathOptions = client.ChartPathOptions + instClient.ValueOptions = client.ValueOptions + instClient.DryRun = client.DryRun + instClient.DisableHooks = client.DisableHooks + instClient.Timeout = client.Timeout + instClient.Wait = client.Wait + instClient.Devel = client.Devel + instClient.Namespace = client.Namespace + + _, err := runInstall(args, instClient, out) + return err + } + } - return cmd -} + // Check chart dependencies to make sure all are present in /charts + ch, err := loader.Load(chartPath) + if err != nil { + return err + } + if req := ch.Metadata.Dependencies; req != nil { + if err := action.CheckDependencies(ch, req); err != nil { + return err + } + } -func (o *upgradeOptions) run(out io.Writer) error { - chartPath, err := o.locateChart(o.chart) - if err != nil { - return err - } + resp, err := client.Run(args[0], ch) + if err != nil { + return errors.Wrap(err, "UPGRADE FAILED") + } - if o.install { - // If a release does not exist, install it. If another error occurs during - // the check, ignore the error and continue with the upgrade. - if _, err := o.client.ReleaseHistory(o.release, 1); err == driver.ErrReleaseNotFound { - fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", o.release) - io := &installOptions{ - chartPath: chartPath, - client: o.client, - name: o.release, - dryRun: o.dryRun, - disableHooks: o.disableHooks, - timeout: o.timeout, - wait: o.wait, - valuesOptions: o.valuesOptions, - chartPathOptions: o.chartPathOptions, + if settings.Debug { + action.PrintRelease(out, resp) } - return io.run(out) - } - } - rawVals, err := o.mergedValues() - if err != nil { - return err - } + fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) - // Check chart dependencies to make sure all are present in /charts - ch, err := loader.Load(chartPath) - if err != nil { - return err - } - if req := ch.Metadata.Dependencies; req != nil { - if err := checkDependencies(ch, req); err != nil { - return err - } - } + // Print the status like status command does + statusClient := action.NewStatus(cfg) + rel, err := statusClient.Run(args[0]) + if err != nil { + return err + } + action.PrintRelease(out, rel) - resp, err := o.client.UpdateRelease( - o.release, - chartPath, - helm.UpdateValueOverrides(rawVals), - helm.UpgradeDryRun(o.dryRun), - helm.UpgradeRecreate(o.recreate), - helm.UpgradeForce(o.force), - helm.UpgradeDisableHooks(o.disableHooks), - helm.UpgradeTimeout(o.timeout), - helm.ResetValues(o.resetValues), - helm.ReuseValues(o.reuseValues), - helm.UpgradeWait(o.wait), - helm.MaxHistory(o.maxHistory)) - if err != nil { - return errors.Wrap(err, "UPGRADE FAILED") + return nil + }, } - if settings.Debug { - printRelease(out, resp) - } + f := cmd.Flags() + f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade") + f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") + f.Int64Var(&client.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.ResetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") + f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored.") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.IntVar(&client.MaxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") + addChartPathOptionsFlags(f, &client.ChartPathOptions) + addValueOptionsFlags(f, &client.ValueOptions) - fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", o.release) - PrintStatus(out, resp) - return nil + return cmd } diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index c45f77026..4cc44e2f1 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -23,8 +23,7 @@ import ( "k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/release" ) func TestUpgradeCmd(t *testing.T) { @@ -42,7 +41,7 @@ func TestUpgradeCmd(t *testing.T) { if err != nil { t.Fatalf("Error loading chart: %v", err) } - _ = helm.ReleaseMock(&helm.MockReleaseOptions{ + _ = release.Mock(&release.MockReleaseOptions{ Name: "funny-bunny", Chart: ch, }) @@ -84,7 +83,7 @@ func TestUpgradeCmd(t *testing.T) { badDepsPath := "testdata/testcharts/chart-bad-requirements" relMock := func(n string, v int, ch *chart.Chart) *release.Release { - return helm.ReleaseMock(&helm.MockReleaseOptions{Name: n, Version: v, Chart: ch}) + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) } tests := []cmdTestCase{ diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go index f46e1570f..d9069312a 100644 --- a/cmd/helm/verify.go +++ b/cmd/helm/verify.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/action" ) const verifyDesc = ` @@ -35,13 +35,8 @@ This command can be used to verify a local chart. Several other commands provide the 'helm package --sign' command. ` -type verifyOptions struct { - keyring string - chartfile string -} - func newVerifyCmd(out io.Writer) *cobra.Command { - o := &verifyOptions{} + client := action.NewVerify() cmd := &cobra.Command{ Use: "verify PATH", @@ -49,18 +44,11 @@ func newVerifyCmd(out io.Writer) *cobra.Command { Long: verifyDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - o.chartfile = args[0] - return o.run(out) + return client.Run(args[0]) }, } - f := cmd.Flags() - f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + cmd.Flags().StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") return cmd } - -func (o *verifyOptions) run(out io.Writer) error { - _, err := downloader.VerifyChart(o.chartfile, o.keyring) - return err -} diff --git a/cmd/helm/verify_test.go b/cmd/helm/verify_test.go index 4d01c61b9..a70051ff6 100644 --- a/cmd/helm/verify_test.go +++ b/cmd/helm/verify_test.go @@ -72,7 +72,7 @@ func TestVerifyCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out, err := executeCommand(nil, tt.cmd) + _, out, err := executeActionCommand(tt.cmd) if tt.wantError { if err == nil { t.Errorf("Expected error, but got none: %q", out) diff --git a/docs/install.md b/docs/install.md index ad7b1b111..3bd5beeb5 100755 --- a/docs/install.md +++ b/docs/install.md @@ -10,11 +10,11 @@ Helm can be installed either from source, or from pre-built binary releases. ### From the Binary Releases -Every [release](https://github.com/helm/helm/releases) of Helm +Every [release](https://github.com/helm/releases) of Helm provides binary releases for a variety of OSes. These binary versions can be manually downloaded and installed. -1. Download your [desired version](https://github.com/helm/helm/releases) +1. Download your [desired version](https://github.com/helm/releases) 2. Unpack it (`tar -zxvf helm-v2.0.0-linux-amd64.tgz`) 3. Find the `helm` binary in the unpacked directory, and move it to its desired destination (`mv linux-amd64/helm /usr/local/bin/helm`) diff --git a/docs/quickstart.md b/docs/quickstart.md index 61f9f5f73..1206ab8c6 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -12,7 +12,7 @@ The following prerequisites are required for a successful and properly secured u ### Install Kubernetes or have access to a cluster -- You must have Kubernetes installed. For the latest release of Helm, we recommend the latest stable release of Kubernetes, which in most cases is the second-latest minor release. +- You must have Kubernetes installed. For the latest release of Helm, we recommend the latest stable release of Kubernetes, which in most cases is the second-latest minor release. - You should also have a local configured copy of `kubectl`. NOTE: Kubernetes versions prior to 1.6 have limited or no support for role-based access controls (RBAC). diff --git a/internal/test/test.go b/internal/test/test.go index aa1791a07..319ec1557 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -75,7 +75,7 @@ func compare(actual []byte, filename string) error { return errors.Wrapf(err, "unable to read testdata %s", filename) } if !bytes.Equal(expected, actual) { - return errors.Errorf("does not match golden file %s\n\nWANT:\n%s\n\nGOT:\n%s\n", filename, expected, actual) + return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'\n", filename, expected, actual) } return nil } diff --git a/pkg/action/action.go b/pkg/action/action.go index 9d097788e..24bab62fa 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -17,15 +17,18 @@ limitations under the License. package action import ( + "regexp" "time" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/registry" + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/tiller/environment" ) // Timestamper is a function capable of producing a timestamp.Timestamper. @@ -34,6 +37,28 @@ import ( // though, so that timestamps are predictable. var Timestamper = time.Now +var ( + // errMissingChart indicates that a chart was not provided. + errMissingChart = errors.New("no chart provided") + // errMissingRelease indicates that a release (name) was not provided. + errMissingRelease = errors.New("no release provided") + // errInvalidRevision indicates that an invalid release revision number was provided. + errInvalidRevision = errors.New("invalid release revision") + //errInvalidName indicates that an invalid release name was provided + errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53") +) + +// ValidName is a regular expression for 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])? +// +// 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])+$") + // Configuration injects the dependencies that all actions share. type Configuration struct { // Discovery contains a discovery client @@ -43,7 +68,7 @@ type Configuration struct { Releases *storage.Storage // KubeClient is a Kubernetes API client. - KubeClient environment.KubeClient + KubeClient kube.KubernetesClient // RegistryClient is a client for working with registries RegistryClient *registry.Client @@ -69,6 +94,18 @@ func (c *Configuration) Now() time.Time { return Timestamper() } +func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) { + if err := validateReleaseName(name); err != nil { + return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) + } + + if version <= 0 { + return c.Releases.Last(name) + } + + return c.Releases.Get(name, version) +} + // GetVersionSet retrieves a set of available k8s API versions func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { groups, err := client.ServerGroups() @@ -87,3 +124,14 @@ func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet versions := metav1.ExtractGroupVersions(groups) return chartutil.NewVersionSet(versions...), nil } + +// recordRelease with an update operation in case reuse has been set. +func (c *Configuration) recordRelease(r *release.Release, reuse bool) { + if reuse { + if err := c.Releases.Update(r); err != nil { + c.Log("warning: Failed to update release %s: %s", r.Name, err) + } + } else if err := c.Releases.Create(r); err != nil { + c.Log("warning: Failed to record release %s: %s", r.Name, err) + } +} diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index b1378b378..acf0c2d19 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -26,10 +26,10 @@ import ( "k8s.io/client-go/kubernetes/fake" "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller/environment" ) var verbose = flag.Bool("test.log", false, "enable test logging") @@ -39,7 +39,7 @@ func actionConfigFixture(t *testing.T) *Configuration { return &Configuration{ Releases: storage.Init(driver.NewMemory()), - KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard}, + KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, Discovery: fake.NewSimpleClientset().Discovery(), Log: func(format string, v ...interface{}) { t.Helper() @@ -176,12 +176,12 @@ func namedReleaseStub(name string, status release.Status) *release.Release { func newHookFailingKubeClient() *hookFailingKubeClient { return &hookFailingKubeClient{ - PrintingKubeClient: environment.PrintingKubeClient{Out: ioutil.Discard}, + PrintingKubeClient: kube.PrintingKubeClient{Out: ioutil.Discard}, } } type hookFailingKubeClient struct { - environment.PrintingKubeClient + kube.PrintingKubeClient } func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go new file mode 100644 index 000000000..b17a09aa4 --- /dev/null +++ b/pkg/action/dependency.go @@ -0,0 +1,184 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Masterminds/semver" + "github.com/gosuri/uitable" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/chart/loader" +) + +// Dependency is the action for building a given chart's dependency tree. +// +// It provides the implementation of 'helm dependency' and its respective subcommands. +type Dependency struct { + Verify bool + Keyring string + SkipRefresh bool +} + +// NewDependency creates a new Dependency object with the given configuration. +func NewDependency() *Dependency { + return &Dependency{} +} + +// List executes 'helm dependency list'. +func (d *Dependency) List(chartpath string, out io.Writer) error { + c, err := loader.Load(chartpath) + if err != nil { + return err + } + + if c.Metadata.Dependencies == nil { + fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(chartpath, "charts")) + return nil + } + + d.printDependencies(chartpath, out, c.Metadata.Dependencies) + fmt.Fprintln(out) + d.printMissing(chartpath, out, c.Metadata.Dependencies) + return nil +} + +func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency) string { + filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") + archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)) + if err != nil { + return "bad pattern" + } else if len(archives) > 1 { + return "too many matches" + } else if len(archives) == 1 { + archive := archives[0] + if _, err := os.Stat(archive); err == nil { + c, err := loader.Load(archive) + if err != nil { + return "corrupt" + } + if c.Name() != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "ok" + } + return "wrong version" + } + return "ok" + } + } + + folder := filepath.Join(chartpath, "charts", dep.Name) + if fi, err := os.Stat(folder); err != nil { + return "missing" + } else if !fi.IsDir() { + return "mispackaged" + } + + c, err := loader.Load(folder) + if err != nil { + return "corrupt" + } + + if c.Name() != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "unpacked" + } + return "wrong version" + } + + return "unpacked" +} + +// printDependencies prints all of the dependencies in the yaml file. +func (d *Dependency) printDependencies(chartpath string, out io.Writer, reqs []*chart.Dependency) { + table := uitable.New() + table.MaxColWidth = 80 + table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + for _, row := range reqs { + table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row)) + } + fmt.Fprintln(out, table) +} + +// printMissing prints warnings about charts that are present on disk, but are +// not in Charts.yaml. +func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) { + folder := filepath.Join(chartpath, "charts/*") + files, err := filepath.Glob(folder) + if err != nil { + fmt.Fprintln(out, err) + return + } + + for _, f := range files { + fi, err := os.Stat(f) + if err != nil { + fmt.Fprintf(out, "Warning: %s\n", err) + } + // Skip anything that is not a directory and not a tgz file. + if !fi.IsDir() && filepath.Ext(f) != ".tgz" { + continue + } + c, err := loader.Load(f) + if err != nil { + fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) + continue + } + found := false + for _, d := range reqs { + if d.Name == c.Name() { + found = true + break + } + } + if !found { + fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) + } + } +} diff --git a/pkg/action/get.go b/pkg/action/get.go new file mode 100644 index 000000000..f4726c48c --- /dev/null +++ b/pkg/action/get.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 action + +import ( + "k8s.io/helm/pkg/release" +) + +// Get is the action for checking a given release's information. +// +// It provides the implementation of 'helm get' and its respective subcommands (except `helm get values`). +type Get struct { + cfg *Configuration + + Version int +} + +// NewGet creates a new Get object with the given configuration. +func NewGet(cfg *Configuration) *Get { + return &Get{ + cfg: cfg, + } +} + +// Run executes 'helm get' against the given release. +func (g *Get) Run(name string) (*release.Release, error) { + return g.cfg.releaseContent(name, g.Version) +} diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go new file mode 100644 index 000000000..eaea54929 --- /dev/null +++ b/pkg/action/get_values.go @@ -0,0 +1,68 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/chartutil" +) + +// GetValues is the action for checking a given release's values. +// +// It provides the implementation of 'helm get values'. +type GetValues struct { + cfg *Configuration + + Version int + AllValues bool +} + +// NewGetValues creates a new GetValues object with the given configuration. +func NewGetValues(cfg *Configuration) *GetValues { + return &GetValues{ + cfg: cfg, + } +} + +// Run executes 'helm get values' against the given release. +func (g *GetValues) Run(name string) (string, error) { + res, err := g.cfg.releaseContent(name, g.Version) + if err != nil { + return "", err + } + + // If the user wants all values, compute the values and return. + if g.AllValues { + cfg, err := chartutil.CoalesceValues(res.Chart, res.Config) + if err != nil { + return "", err + } + cfgStr, err := cfg.YAML() + if err != nil { + return "", err + } + return cfgStr, nil + } + + resConfig, err := yaml.Marshal(res.Config) + if err != nil { + return "", err + } + + return string(resConfig), nil +} diff --git a/pkg/action/history.go b/pkg/action/history.go new file mode 100644 index 000000000..d90350708 --- /dev/null +++ b/pkg/action/history.go @@ -0,0 +1,180 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "encoding/json" + "fmt" + + "github.com/ghodss/yaml" + "github.com/gosuri/uitable" + "github.com/pkg/errors" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/release" + "k8s.io/helm/pkg/releaseutil" +) + +type releaseInfo struct { + Revision int `json:"revision"` + Updated string `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + Description string `json:"description"` +} + +type releaseHistory []releaseInfo + +type OutputFormat string + +const ( + Table OutputFormat = "table" + JSON OutputFormat = "json" + YAML OutputFormat = "yaml" +) + +var ErrInvalidFormatType = errors.New("invalid format type") + +func (o OutputFormat) String() string { + return string(o) +} + +func ParseOutputFormat(s string) (out OutputFormat, err error) { + switch s { + case Table.String(): + out, err = Table, nil + case JSON.String(): + out, err = JSON, nil + case YAML.String(): + out, err = YAML, nil + default: + out, err = "", ErrInvalidFormatType + } + return +} + +func (o OutputFormat) MarshalHistory(hist releaseHistory) (byt []byte, err error) { + switch o { + case YAML: + byt, err = yaml.Marshal(hist) + case JSON: + byt, err = json.Marshal(hist) + case Table: + byt = formatAsTable(hist) + default: + err = ErrInvalidFormatType + } + return +} + +// History is the action for checking the release's ledger. +// +// It provides the implementation of 'helm history'. +type History struct { + cfg *Configuration + + Max int + OutputFormat string +} + +// NewHistory creates a new History object with the given configuration. +func NewHistory(cfg *Configuration) *History { + return &History{ + cfg: cfg, + } +} + +// Run executes 'helm history' against the given release. +func (h *History) Run(name string) (string, error) { + if err := validateReleaseName(name); err != nil { + return "", errors.Errorf("getHistory: Release name is invalid: %s", name) + } + + h.cfg.Log("getting history for release %s", name) + hist, err := h.cfg.Releases.History(name) + if err != nil { + return "", err + } + + releaseutil.Reverse(hist, releaseutil.SortByRevision) + + var rels []*release.Release + for i := 0; i < min(len(hist), h.Max); i++ { + rels = append(rels, hist[i]) + } + + if len(rels) == 0 { + return "", nil + } + + releaseHistory := getReleaseHistory(rels) + + outputFormat, err := ParseOutputFormat(h.OutputFormat) + if err != nil { + return "", err + } + history, formattingError := outputFormat.MarshalHistory(releaseHistory) + if formattingError != nil { + return "", formattingError + } + + return string(history), nil +} + +func getReleaseHistory(rls []*release.Release) (history releaseHistory) { + for i := len(rls) - 1; i >= 0; i-- { + r := rls[i] + c := formatChartname(r.Chart) + s := r.Info.Status.String() + v := r.Version + d := r.Info.Description + + rInfo := releaseInfo{ + Revision: v, + Status: s, + Chart: c, + Description: d, + } + if !r.Info.LastDeployed.IsZero() { + rInfo.Updated = r.Info.LastDeployed.String() + + } + history = append(history, rInfo) + } + + return history +} + +func formatAsTable(releases releaseHistory) []byte { + tbl := uitable.New() + + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION") + for i := 0; i <= len(releases)-1; i++ { + r := releases[i] + tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.Description) + } + return tbl.Bytes() +} + +func formatChartname(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/helm/helm/issues/1347 + return "MISSING" + } + return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) +} diff --git a/pkg/action/install.go b/pkg/action/install.go index dd0acdf31..b3cb92d04 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -20,19 +20,31 @@ import ( "bytes" "fmt" "io" + "io/ioutil" + "net/url" + "os" "path" + "path/filepath" "sort" "strings" + "text/template" "time" + "github.com/Masterminds/sprig" + "github.com/ghodss/yaml" "github.com/pkg/errors" "k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/cli" + "k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/releaseutil" + "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/strvals" "k8s.io/helm/pkg/version" ) @@ -53,15 +65,39 @@ const notesFileSuffix = "NOTES.txt" type Install struct { cfg *Configuration - DryRun bool - DisableHooks bool - Replace bool - Wait bool - Devel bool - DepUp bool - Timeout int64 - Namespace string - ReleaseName string + ChartPathOptions + ValueOptions + + DryRun bool + DisableHooks bool + Replace bool + Wait bool + Devel bool + DependencyUpdate bool + Timeout int64 + Namespace string + ReleaseName string + GenerateName bool + NameTemplate string +} + +type ValueOptions struct { + ValueFiles []string + StringValues []string + Values []string + rawValues map[string]interface{} +} + +type ChartPathOptions struct { + CaFile string // --ca-file + CertFile string // --cert-file + KeyFile string // --key-file + Keyring string // --keyring + Password string // --password + RepoURL string // --repo + Username string // --username + Verify bool // --verify + Version string // --version } // NewInstall creates a new Install object with the given configuration. @@ -74,7 +110,7 @@ func NewInstall(cfg *Configuration) *Install { // Run executes the installation // // If DryRun is set to true, this will prepare the release, but not install it -func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*release.Release, error) { +func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { if err := i.availableName(); err != nil { return nil, err } @@ -85,12 +121,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel Name: i.ReleaseName, IsInstall: true, } - valuesToRender, err := chartutil.ToRenderValues(chrt, rawValues, options, caps) + valuesToRender, err := chartutil.ToRenderValues(chrt, i.rawValues, options, caps) if err != nil { return nil, err } - rel := i.createRelease(chrt, rawValues) + rel := i.createRelease(chrt, i.rawValues) var manifestDoc *bytes.Buffer rel.Hooks, manifestDoc, rel.Info.Notes, err = i.renderResources(chrt, valuesToRender, caps.APIVersions) // Even for errors, attach this if available @@ -100,12 +136,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel // Check error from render if err != nil { rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) - rel.Version = 0 // Why do we do this? + // Return a release with partial data so that the client can show debugging information. return rel, err } // Mark this release as in-progress - rel.SetStatus(release.StatusPendingInstall, "Intiial install underway") + rel.SetStatus(release.StatusPendingInstall, "Initial install underway") if err := i.validateManifest(manifestDoc); err != nil { return rel, err } @@ -257,20 +293,20 @@ func (i *Install) replaceRelease(rel *release.Release) error { // renderResources renders the templates in a chart func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { hooks := []*release.Hook{} - buf := bytes.NewBuffer(nil) + b := bytes.NewBuffer(nil) if ch.Metadata.KubeVersion != "" { cap, _ := values["Capabilities"].(*chartutil.Capabilities) gitVersion := cap.KubeVersion.String() k8sVersion := strings.Split(gitVersion, "+")[0] if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { - return hooks, buf, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) + return hooks, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) } } files, err := engine.Render(ch, values) if err != nil { - return hooks, buf, "", err + return hooks, b, "", err } // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, @@ -301,22 +337,18 @@ func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs c // // We return the files as a big blob of data to help the user debug parser // errors. - b := bytes.NewBuffer(nil) for name, content := range files { if len(strings.TrimSpace(content)) == 0 { continue } - b.WriteString("\n---\n# Source: " + name + "\n") - b.WriteString(content) + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) } return hooks, b, "", err } // Aggregate all valid manifests into one big doc. - b := bytes.NewBuffer(nil) for _, m := range manifests { - b.WriteString("\n---\n# Source: " + m.Name + "\n") - b.WriteString(m.Content) + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) } return hooks, b, notes, nil @@ -346,7 +378,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error { sort.Sort(hookByWeight(executingHooks)) for _, h := range executingHooks { - if err := i.deleteHookByPolicy(h, hooks.BeforeHookCreation, hook); err != nil { + if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.BeforeHookCreation, hook); err != nil { return err } @@ -360,7 +392,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error { if err := i.cfg.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil { // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted // under failed condition. If so, then clear the corresponding resource object in the hook - if err := i.deleteHookByPolicy(h, hooks.HookFailed, hook); err != nil { + if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.HookFailed, hook); err != nil { return err } return err @@ -370,7 +402,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error { // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted // under succeeded condition. If so, then clear the corresponding resource object in each hook for _, h := range executingHooks { - if err := i.deleteHookByPolicy(h, hooks.HookSucceeded, hook); err != nil { + if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.HookSucceeded, hook); err != nil { return err } h.LastRun = time.Now() @@ -379,17 +411,6 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error { return nil } -// deleteHookByPolicy deletes a hook if the hook policy instructs it to -func (i *Install) deleteHookByPolicy(h *release.Hook, policy, hook string) error { - b := bytes.NewBufferString(h.Manifest) - if hookHasDeletePolicy(h, policy) { - if errHookDelete := i.cfg.KubeClient.Delete(i.Namespace, b); errHookDelete != nil { - return errHookDelete - } - } - return nil -} - // deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning // FIXME: Can we refactor this out? var deletePolices = map[string]release.HookDeletePolicy{ @@ -424,3 +445,241 @@ func (x hookByWeight) Less(i, j int) bool { } return x[i].Weight < x[j].Weight } + +// NameAndChart returns the name and chart that should be used. +// +// This will read the flags and handle name generation if necessary. +func (i *Install) NameAndChart(args []string) (string, string, error) { + flagsNotSet := func() error { + if i.GenerateName { + return errors.New("cannot set --generate-name and also specify a name") + } + if i.NameTemplate != "" { + return errors.New("cannot set --name-template and also specify a name") + } + return nil + } + + if len(args) == 2 { + return args[0], args[1], flagsNotSet() + } + + if i.NameTemplate != "" { + name, err := TemplateName(i.NameTemplate) + return name, args[0], err + } + + if i.ReleaseName != "" { + return i.ReleaseName, args[0], nil + } + + if !i.GenerateName { + return "", args[0], errors.New("must either provide a name or specify --generate-name") + } + + base := filepath.Base(args[0]) + if base == "." || base == "" { + base = "chart" + } + + return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil +} + +func TemplateName(nameTemplate string) (string, error) { + if nameTemplate == "" { + return "", nil + } + + t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) + if err != nil { + return "", err + } + var b bytes.Buffer + if err := t.Execute(&b, nil); err != nil { + return "", err + } + + return b.String(), nil +} + +func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { + var missing []string + +OUTER: + for _, r := range reqs { + for _, d := range ch.Dependencies() { + if d.Name() == r.Name { + continue OUTER + } + } + missing = append(missing, r.Name) + } + + if len(missing) > 0 { + return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) + } + return nil +} + +// LocateChart looks for a chart directory in known places, and returns either the full path or an error. +// +// This does not ensure that the chart is well-formed; only that the requested filename exists. +// +// Order of resolution: +// - relative to current working directory +// - if path is absolute or begins with '.', error out here +// - chart repos in $HELM_HOME +// - URL +// +// If 'verify' is true, this will attempt to also verify the chart. +func (c *ChartPathOptions) LocateChart(name string, settings cli.EnvSettings) (string, error) { + name = strings.TrimSpace(name) + version := strings.TrimSpace(c.Version) + + if _, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err + } + if c.Verify { + if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { + return "", err + } + } + return abs, nil + } + if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { + return name, errors.Errorf("path %q not found", name) + } + + crepo := filepath.Join(settings.Home.Repository(), name) + if _, err := os.Stat(crepo); err == nil { + return filepath.Abs(crepo) + } + + dl := downloader.ChartDownloader{ + HelmHome: settings.Home, + Out: os.Stdout, + Keyring: c.Keyring, + Getters: getter.All(settings), + Username: c.Username, + Password: c.Password, + } + if c.Verify { + dl.Verify = downloader.VerifyAlways + } + if c.RepoURL != "" { + chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version, + c.CertFile, c.KeyFile, c.CaFile, getter.All(settings)) + if err != nil { + return "", err + } + name = chartURL + } + + if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { + os.MkdirAll(settings.Home.Archive(), 0744) + } + + filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) + if err == nil { + lname, err := filepath.Abs(filename) + if err != nil { + return filename, err + } + return lname, nil + } else if settings.Debug { + return filename, err + } + + return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) +} + +// MergeValues merges values from files specified via -f/--values and +// directly via --set or --set-string, marshaling them to YAML +func (v *ValueOptions) MergeValues(settings cli.EnvSettings) error { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range v.ValueFiles { + currentMap := map[string]interface{}{} + + bytes, err := readFile(filePath, settings) + if err != nil { + return err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return errors.Wrapf(err, "failed to parse %s", filePath) + } + // Merge with the previous map + base = MergeValues(base, currentMap) + } + + // User specified a value via --set + for _, value := range v.Values { + if err := strvals.ParseInto(value, base); err != nil { + return errors.Wrap(err, "failed parsing --set data") + } + } + + // User specified a value via --set-string + for _, value := range v.StringValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return errors.Wrap(err, "failed parsing --set-string data") + } + } + + v.rawValues = base + return nil +} + +// MergeValues merges source and destination map, preferring values from the source map +func MergeValues(dest, src map[string]interface{}) map[string]interface{} { + for k, v := range src { + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = v + continue + } + nextMap, ok := v.(map[string]interface{}) + // If it isn't another map, overwrite the value + if !ok { + dest[k] = v + continue + } + // Edge case: If the key exists in the destination, but isn't a map + destMap, isMap := dest[k].(map[string]interface{}) + // If the source map has a map for this key, prefer it + if !isMap { + dest[k] = v + continue + } + // If we got to this point, it is a map in both, so merge them + dest[k] = MergeValues(destMap, nextMap) + } + return dest +} + +// readFile load a file from stdin, the local directory, or a remote file with a url. +func readFile(filePath string, settings cli.EnvSettings) ([]byte, error) { + if strings.TrimSpace(filePath) == "-" { + return ioutil.ReadAll(os.Stdin) + } + u, _ := url.Parse(filePath) + p := getter.All(settings) + + // FIXME: maybe someone handle other protocols like ftp. + getterConstructor, err := p.ByScheme(u.Scheme) + + if err != nil { + return ioutil.ReadFile(filePath) + } + + getter, err := getterConstructor(filePath, "", "", "") + if err != nil { + return []byte{}, err + } + data, err := getter.Get(filePath) + return data.Bytes(), err +} diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 91ad53d8f..4d338deb6 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -18,13 +18,21 @@ package action import ( "fmt" + "reflect" + "regexp" "testing" "github.com/stretchr/testify/assert" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) +type nameTemplateTestCase struct { + tpl string + expected string + expectedErrorStr string +} + func installAction(t *testing.T) *Install { config := actionConfigFixture(t) instAction := NewInstall(config) @@ -34,12 +42,11 @@ func installAction(t *testing.T) *Install { return instAction } -var mockEmptyVals = func() map[string]interface{} { return map[string]interface{}{} } - func TestInstallRelease(t *testing.T) { is := assert.New(t) instAction := installAction(t) - res, err := instAction.Run(buildChart(), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart()) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -63,7 +70,8 @@ func TestInstallRelease(t *testing.T) { func TestInstallRelease_NoName(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "" - _, err := instAction.Run(buildChart(), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + _, err := instAction.Run(buildChart()) if err == nil { t.Fatal("expected failure when no name is specified") } @@ -74,7 +82,8 @@ func TestInstallRelease_WithNotes(t *testing.T) { is := assert.New(t) instAction := installAction(t) instAction.ReleaseName = "with-notes" - res, err := instAction.Run(buildChart(withNotes("note here")), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("note here"))) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -100,7 +109,8 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) { is := assert.New(t) instAction := installAction(t) instAction.ReleaseName = "with-notes" - res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}"))) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -118,11 +128,8 @@ func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) { is := assert.New(t) instAction := installAction(t) instAction.ReleaseName = "with-notes" - res, err := instAction.Run(buildChart( - withNotes("parent"), - withDependency(withNotes("child"))), - mockEmptyVals(), - ) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child")))) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -138,7 +145,8 @@ func TestInstallRelease_DryRun(t *testing.T) { is := assert.New(t) instAction := installAction(t) instAction.DryRun = true - res, err := instAction.Run(buildChart(withSampleTemplates()), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart(withSampleTemplates())) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -163,7 +171,8 @@ func TestInstallRelease_NoHooks(t *testing.T) { instAction.ReleaseName = "no-hooks" instAction.cfg.Releases.Create(releaseStub()) - res, err := instAction.Run(buildChart(), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart()) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -177,7 +186,8 @@ func TestInstallRelease_FailedHooks(t *testing.T) { instAction.ReleaseName = "failed-hooks" instAction.cfg.KubeClient = newHookFailingKubeClient() - res, err := instAction.Run(buildChart(), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart()) is.Error(err) is.Contains(res.Info.Description, "failed post-install") is.Equal(res.Info.Status, release.StatusFailed) @@ -193,7 +203,8 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) { instAction.cfg.Releases.Create(rel) instAction.ReleaseName = rel.Name - res, err := instAction.Run(buildChart(), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + res, err := instAction.Run(buildChart()) is.NoError(err) // This should have been auto-incremented @@ -208,12 +219,138 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) { func TestInstallRelease_KubeVersion(t *testing.T) { is := assert.New(t) instAction := installAction(t) - _, err := instAction.Run(buildChart(withKube(">=0.0.0")), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + _, err := instAction.Run(buildChart(withKube(">=0.0.0"))) is.NoError(err) // This should fail for a few hundred years instAction.ReleaseName = "should-fail" - _, err = instAction.Run(buildChart(withKube(">=99.0.0")), mockEmptyVals()) + instAction.rawValues = map[string]interface{}{} + _, err = instAction.Run(buildChart(withKube(">=99.0.0"))) is.Error(err) is.Contains(err.Error(), "chart requires kubernetesVersion") } + +func TestNameTemplate(t *testing.T) { + testCases := []nameTemplateTestCase{ + // Just a straight up nop please + { + tpl: "foobar", + expected: "foobar", + expectedErrorStr: "", + }, + // Random numbers at the end for fun & profit + { + tpl: "foobar-{{randNumeric 6}}", + expected: "foobar-[0-9]{6}$", + expectedErrorStr: "", + }, + // Random numbers in the middle for fun & profit + { + tpl: "foobar-{{randNumeric 4}}-baz", + expected: "foobar-[0-9]{4}-baz$", + expectedErrorStr: "", + }, + // No such function + { + tpl: "foobar-{{randInt}}", + expected: "", + expectedErrorStr: "function \"randInt\" not defined", + }, + // Invalid template + { + tpl: "foobar-{{", + expected: "", + expectedErrorStr: "unexpected unclosed action", + }, + } + + for _, tc := range testCases { + + n, err := TemplateName(tc.tpl) + if err != nil { + if tc.expectedErrorStr == "" { + t.Errorf("Was not expecting error, but got: %v", err) + continue + } + re, compErr := regexp.Compile(tc.expectedErrorStr) + if compErr != nil { + t.Errorf("Expected error string failed to compile: %v", compErr) + continue + } + if !re.MatchString(err.Error()) { + t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) + continue + } + } + if err == nil && tc.expectedErrorStr != "" { + t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) + } + + if tc.expected != "" { + re, err := regexp.Compile(tc.expected) + if err != nil { + t.Errorf("Expected string failed to compile: %v", err) + continue + } + if !re.MatchString(n) { + t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) + } + } + } +} + +func TestMergeValues(t *testing.T) { + nestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "stuff", + }, + } + anotherNestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + flatMap := map[string]interface{}{ + "foo": "bar", + "baz": "stuff", + } + anotherFlatMap := map[string]interface{}{ + "testing": "fun", + } + + testMap := MergeValues(flatMap, nestedMap) + equal := reflect.DeepEqual(testMap, nestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) + } + + testMap = MergeValues(nestedMap, flatMap) + equal = reflect.DeepEqual(testMap, flatMap) + if !equal { + t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) + } + + testMap = MergeValues(nestedMap, anotherNestedMap) + equal = reflect.DeepEqual(testMap, anotherNestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + } + + testMap = MergeValues(anotherFlatMap, anotherNestedMap) + expectedMap := map[string]interface{}{ + "testing": "fun", + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + equal = reflect.DeepEqual(testMap, expectedMap) + if !equal { + t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + } +} diff --git a/pkg/action/lint.go b/pkg/action/lint.go new file mode 100644 index 000000000..e7e464e92 --- /dev/null +++ b/pkg/action/lint.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/lint" + "k8s.io/helm/pkg/lint/support" +) + +var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") + +// Lint is the action for checking that the semantics of a chart are well-formed. +// +// It provides the implementation of 'helm lint'. +type Lint struct { + ValueOptions + + Strict bool + Namespace string +} + +type LintResult struct { + TotalChartsLinted int + Messages []support.Message + Errors []error +} + +// NewLint creates a new Lint object with the given configuration. +func NewLint() *Lint { + return &Lint{} +} + +// Run executes 'helm Lint' against the given chart. +func (l *Lint) Run(paths []string) *LintResult { + lowestTolerance := support.ErrorSev + if l.Strict { + lowestTolerance = support.WarningSev + } + + result := &LintResult{} + for _, path := range paths { + if linter, err := lintChart(path, l.ValueOptions.rawValues, l.Namespace, l.Strict); err != nil { + if err == errLintNoChart { + result.Errors = append(result.Errors, err) + } + } else { + result.Messages = append(result.Messages, linter.Messages...) + result.TotalChartsLinted++ + if linter.HighestSeverity >= lowestTolerance { + result.Errors = append(result.Errors, err) + } + } + } + return result +} + +func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) { + var chartPath string + linter := support.Linter{} + + if strings.HasSuffix(path, ".tgz") { + tempDir, err := ioutil.TempDir("", "helm-lint") + if err != nil { + return linter, err + } + defer os.RemoveAll(tempDir) + + file, err := os.Open(path) + if err != nil { + return linter, err + } + defer file.Close() + + if err = chartutil.Expand(tempDir, file); err != nil { + return linter, err + } + + lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-") + if lastHyphenIndex <= 0 { + return linter, errors.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path)) + } + base := filepath.Base(path)[:lastHyphenIndex] + chartPath = filepath.Join(tempDir, base) + } else { + chartPath = path + } + + // Guard: Error out of this is not a chart. + if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { + return linter, errLintNoChart + } + + return lint.All(chartPath, vals, namespace, strict), nil +} diff --git a/cmd/helm/lint_test.go b/pkg/action/lint_test.go similarity index 73% rename from cmd/helm/lint_test.go rename to pkg/action/lint_test.go index da7bdb70a..c442be344 100644 --- a/cmd/helm/lint_test.go +++ b/pkg/action/lint_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package action import ( "testing" @@ -24,11 +24,11 @@ var ( values = make(map[string]interface{}) namespace = "testNamespace" strict = false - archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz" - archivedChartPathWithHyphens = "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz" - invalidArchivedChartPath = "testdata/testcharts/invalidcompressedchart0.1.0.tgz" - chartDirPath = "testdata/testcharts/decompressedchart/" - chartMissingManifest = "testdata/testcharts/chart-missing-manifest" + archivedChartPath = "../../cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz" + archivedChartPathWithHyphens = "../../cmd/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz" + invalidArchivedChartPath = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz" + chartDirPath = "../../cmd/helm/testdata/testcharts/decompressedchart/" + chartMissingManifest = "../../cmd/helm/testdata/testcharts/chart-missing-manifest" ) func TestLintChart(t *testing.T) { diff --git a/pkg/action/list.go b/pkg/action/list.go index db8ac3541..27e6577d9 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -17,9 +17,12 @@ limitations under the License. package action import ( + "fmt" "regexp" - "k8s.io/helm/pkg/hapi/release" + "github.com/gosuri/uitable" + + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/releaseutil" ) @@ -96,6 +99,8 @@ const ( // // It provides, for example, the implementation of 'helm list'. type List struct { + cfg *Configuration + // All ignores the limit/offset All bool // AllNamespaces searches across namespaces @@ -112,9 +117,16 @@ type List struct { // Offset is the starting index for the Run() call Offset int // Filter is a filter that is applied to the results - Filter string - - cfg *Configuration + Filter string + Short bool + ByDate bool + SortDesc bool + Uninstalled bool + Superseded bool + Uninstalling bool + Deployed bool + Failed bool + Pending bool } // NewList constructs a new *List @@ -125,21 +137,25 @@ func NewList(cfg *Configuration) *List { } } +func (l *List) SetConfiguration(cfg *Configuration) { + l.cfg = cfg +} + // Run executes the list command, returning a set of matches. -func (a *List) Run() ([]*release.Release, error) { +func (l *List) Run() ([]*release.Release, error) { var filter *regexp.Regexp - if a.Filter != "" { + if l.Filter != "" { var err error - filter, err = regexp.Compile(a.Filter) + filter, err = regexp.Compile(l.Filter) if err != nil { return nil, err } } - results, err := a.cfg.Releases.List(func(rel *release.Release) bool { + results, err := l.cfg.Releases.List(func(rel *release.Release) bool { // Skip anything that the mask doesn't cover - currentStatus := a.StateMask.FromName(rel.Info.Status.String()) - if a.StateMask¤tStatus == 0 { + currentStatus := l.StateMask.FromName(rel.Info.Status.String()) + if l.StateMask¤tStatus == 0 { return false } @@ -155,30 +171,30 @@ func (a *List) Run() ([]*release.Release, error) { } // Unfortunately, we have to sort before truncating, which can incur substantial overhead - a.sort(results) + l.sort(results) // Guard on offset - if a.Offset >= len(results) { + if l.Offset >= len(results) { return []*release.Release{}, nil } // Calculate the limit and offset, and then truncate results if necessary. limit := len(results) - if a.Limit > 0 && a.Limit < limit { - limit = a.Limit + if l.Limit > 0 && l.Limit < limit { + limit = l.Limit } - last := a.Offset + limit + last := l.Offset + limit if l := len(results); l < last { last = l } - results = results[a.Offset:last] + results = results[l.Offset:last] return results, err } // sort is an in-place sort where order is based on the value of a.Sort -func (a *List) sort(rels []*release.Release) { - switch a.Sort { +func (l *List) sort(rels []*release.Release) { + switch l.Sort { case ByDate: releaseutil.SortByDate(rels) case ByNameDesc: @@ -187,3 +203,53 @@ func (a *List) sort(rels []*release.Release) { releaseutil.SortByName(rels) } } + +// setStateMask calculates the state mask based on parameters. +func (l *List) SetStateMask() { + if l.All { + l.StateMask = ListAll + return + } + + state := ListStates(0) + if l.Deployed { + state |= ListDeployed + } + if l.Uninstalled { + state |= ListUninstalled + } + if l.Uninstalling { + state |= ListUninstalling + } + if l.Pending { + state |= ListPendingInstall | ListPendingRollback | ListPendingUpgrade + } + if l.Failed { + state |= ListFailed + } + + // Apply a default + if state == 0 { + state = ListDeployed | ListFailed + } + + l.StateMask = state +} + +func FormatList(rels []*release.Release) string { + table := uitable.New() + table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE") + for _, r := range rels { + md := r.Chart.Metadata + c := fmt.Sprintf("%s-%s", md.Name, md.Version) + t := "-" + if tspb := r.Info.LastDeployed; !tspb.IsZero() { + t = tspb.String() + } + s := r.Info.Status.String() + v := r.Version + n := r.Namespace + table.AddRow(r.Name, v, t, s, c, n) + } + return table.String() +} diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index fad94d67d..3a1bf6a72 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/storage" ) diff --git a/pkg/action/package.go b/pkg/action/package.go new file mode 100644 index 000000000..8624118fc --- /dev/null +++ b/pkg/action/package.go @@ -0,0 +1,150 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "io/ioutil" + "os" + "syscall" + + "github.com/Masterminds/semver" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/chart/loader" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/provenance" +) + +// Package is the action for packaging a chart. +// +// It provides the implementation of 'helm package'. +type Package struct { + ValueOptions + + Sign bool + Key string + Keyring string + Version string + AppVersion string + Destination string + DependencyUpdate bool +} + +// NewPackage creates a new Package object with the given configuration. +func NewPackage() *Package { + return &Package{} +} + +// Run executes 'helm package' against the given chart and returns the path to the packaged chart. +func (p *Package) Run(path string) (string, error) { + ch, err := loader.LoadDir(path) + if err != nil { + return "", err + } + + validChartType, err := chartutil.IsValidChartType(ch) + if !validChartType { + return "", err + } + + combinedVals, err := chartutil.CoalesceValues(ch, p.ValueOptions.rawValues) + if err != nil { + return "", err + } + ch.Values = combinedVals + + // If version is set, modify the version. + if len(p.Version) != 0 { + if err := setVersion(ch, p.Version); err != nil { + return "", err + } + } + + if p.AppVersion != "" { + ch.Metadata.AppVersion = p.AppVersion + } + + if reqs := ch.Metadata.Dependencies; reqs != nil { + if err := CheckDependencies(ch, reqs); err != nil { + return "", err + } + } + + var dest string + if p.Destination == "." { + // Save to the current working directory. + dest, err = os.Getwd() + if err != nil { + return "", err + } + } else { + // Otherwise save to set destination + dest = p.Destination + } + + name, err := chartutil.Save(ch, dest) + if err != nil { + return "", errors.Wrap(err, "failed to save") + } + + if p.Sign { + err = p.Clearsign(name) + } + + return "", err +} + +func setVersion(ch *chart.Chart, ver string) error { + // Verify that version is a Version, and error out if it is not. + if _, err := semver.NewVersion(ver); err != nil { + return err + } + + // Set the version field on the chart. + ch.Metadata.Version = ver + return nil +} + +func (p *Package) Clearsign(filename string) error { + // Load keyring + signer, err := provenance.NewFromKeyring(p.Keyring, p.Key) + if err != nil { + return err + } + + if err := signer.DecryptKey(promptUser); err != nil { + return err + } + + sig, err := signer.ClearSign(filename) + if err != nil { + return err + } + + return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) +} + +// promptUser implements provenance.PassphraseFetcher +func promptUser(name string) ([]byte, error) { + fmt.Printf("Password for key %q > ", name) + pw, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + return pw, err +} diff --git a/pkg/tiller/release_content_test.go b/pkg/action/package_test.go similarity index 54% rename from pkg/tiller/release_content_test.go rename to pkg/action/package_test.go index b2ec33edc..7bb84756d 100644 --- a/pkg/tiller/release_content_test.go +++ b/pkg/action/package_test.go @@ -14,27 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package action import ( "testing" - "k8s.io/helm/pkg/hapi" + "k8s.io/helm/pkg/chart" ) -func TestGetReleaseContent(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) +func TestSetVersion(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "prow", + Version: "0.0.1", + }, + } + expect := "1.2.3-beta.5" + if err := setVersion(c, expect); err != nil { + t.Fatal(err) } - res, err := rs.GetReleaseContent(&hapi.GetReleaseContentRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Errorf("Error getting release content: %s", err) + if c.Metadata.Version != expect { + t.Errorf("Expected %q, got %q", expect, c.Metadata.Version) } - if res.Chart.Name() != rel.Chart.Name() { - t.Errorf("Expected %q, got %q", rel.Chart.Name(), res.Chart.Name()) + if err := setVersion(c, "monkeyface"); err == nil { + t.Error("Expected bogus version to return an error.") } } diff --git a/pkg/action/printer.go b/pkg/action/printer.go new file mode 100644 index 000000000..4ed0d39b8 --- /dev/null +++ b/pkg/action/printer.go @@ -0,0 +1,78 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "io" + "regexp" + "strings" + "text/tabwriter" + + "github.com/gosuri/uitable" + "github.com/gosuri/uitable/util/strutil" + + "k8s.io/helm/pkg/release" +) + +// PrintRelease prints info about a release +func PrintRelease(out io.Writer, rel *release.Release) { + if rel == nil { + return + } + fmt.Fprintf(out, "NAME: %s\n", rel.Name) + if !rel.Info.LastDeployed.IsZero() { + fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed) + } + fmt.Fprintf(out, "NAMESPACE: %s\n", rel.Namespace) + fmt.Fprintf(out, "STATUS: %s\n", rel.Info.Status.String()) + fmt.Fprintf(out, "\n") + if len(rel.Info.Resources) > 0 { + re := regexp.MustCompile(" +") + + w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) + fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(rel.Info.Resources, "\t")) + w.Flush() + } + if rel.Info.LastTestSuiteRun != nil { + lastRun := rel.Info.LastTestSuiteRun + fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", + fmt.Sprintf("Last Started: %s", lastRun.StartedAt), + fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt), + formatTestResults(lastRun.Results)) + } + + if len(rel.Info.Notes) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes)) + } +} + +func formatTestResults(results []*release.TestRun) string { + tbl := uitable.New() + tbl.MaxColWidth = 50 + tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED") + for i := 0; i < len(results); i++ { + r := results[i] + n := r.Name + s := strutil.PadRight(r.Status.String(), 10, ' ') + i := r.Info + ts := r.StartedAt + tc := r.CompletedAt + tbl.AddRow(n, s, i, ts, tc) + } + return tbl.String() +} diff --git a/pkg/action/pull.go b/pkg/action/pull.go new file mode 100644 index 000000000..0c23941ca --- /dev/null +++ b/pkg/action/pull.go @@ -0,0 +1,122 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/cli" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/repo" +) + +// Pull is the action for checking a given release's information. +// +// It provides the implementation of 'helm pull'. +type Pull struct { + ChartPathOptions + + Settings cli.EnvSettings // TODO: refactor this out of pkg/action + + Devel bool + Untar bool + VerifyLater bool + UntarDir string + DestDir string +} + +// NewPull creates a new Pull object with the given configuration. +func NewPull() *Pull { + return &Pull{} +} + +// Run executes 'helm pull' against the given release. +func (p *Pull) Run(chartRef string) (string, error) { + var out strings.Builder + + c := downloader.ChartDownloader{ + HelmHome: p.Settings.Home, + Out: &out, + Keyring: p.Keyring, + Verify: downloader.VerifyNever, + Getters: getter.All(p.Settings), + Username: p.Username, + Password: p.Password, + } + + if p.Verify { + c.Verify = downloader.VerifyAlways + } else if p.VerifyLater { + c.Verify = downloader.VerifyLater + } + + // If untar is set, we fetch to a tempdir, then untar and copy after + // verification. + dest := p.DestDir + if p.Untar { + var err error + dest, err = ioutil.TempDir("", "helm-") + if err != nil { + return out.String(), errors.Wrap(err, "failed to untar") + } + defer os.RemoveAll(dest) + } + + if p.RepoURL != "" { + chartURL, err := repo.FindChartInAuthRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, getter.All(p.Settings)) + if err != nil { + return out.String(), err + } + chartRef = chartURL + } + + saved, v, err := c.DownloadTo(chartRef, p.Version, dest) + if err != nil { + return out.String(), err + } + + if p.Verify { + fmt.Fprintf(&out, "Verification: %v\n", v) + } + + // After verification, untar the chart into the requested directory. + if p.Untar { + ud := p.UntarDir + if !filepath.IsAbs(ud) { + ud = filepath.Join(p.DestDir, ud) + } + if fi, err := os.Stat(ud); err != nil { + if err := os.MkdirAll(ud, 0755); err != nil { + return out.String(), errors.Wrap(err, "failed to untar (mkdir)") + } + + } else if !fi.IsDir() { + return out.String(), errors.Errorf("failed to untar: %s is not a directory", ud) + } + + return out.String(), chartutil.ExpandFile(ud, saved) + } + return out.String(), nil +} diff --git a/pkg/tiller/release_testing.go b/pkg/action/release_testing.go similarity index 58% rename from pkg/tiller/release_testing.go rename to pkg/action/release_testing.go index 0f7ffbeb3..f98ecbe3f 100644 --- a/pkg/tiller/release_testing.go +++ b/pkg/action/release_testing.go @@ -14,39 +14,55 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package action import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" reltesting "k8s.io/helm/pkg/releasetesting" ) -// RunReleaseTest runs pre-defined tests stored as hooks on a given release -func (s *ReleaseServer) RunReleaseTest(req *hapi.TestReleaseRequest) (<-chan *hapi.TestReleaseResponse, <-chan error) { +// ReleaseTesting is the action for testing a release. +// +// It provides the implementation of 'helm test'. +type ReleaseTesting struct { + cfg *Configuration + + Timeout int64 + Cleanup bool +} + +// NewReleaseTesting creates a new ReleaseTesting object with the given configuration. +func NewReleaseTesting(cfg *Configuration) *ReleaseTesting { + return &ReleaseTesting{ + cfg: cfg, + } +} + +// Run executes 'helm test' against the given release. +func (r *ReleaseTesting) Run(name string) (<-chan *release.TestReleaseResponse, <-chan error) { errc := make(chan error, 1) - if err := validateReleaseName(req.Name); err != nil { - errc <- errors.Errorf("releaseTest: Release name is invalid: %s", req.Name) + if err := validateReleaseName(name); err != nil { + errc <- errors.Errorf("releaseTest: Release name is invalid: %s", name) return nil, errc } // finds the non-deleted release with the given name - rel, err := s.Releases.Last(req.Name) + rel, err := r.cfg.Releases.Last(name) if err != nil { errc <- err return nil, errc } - ch := make(chan *hapi.TestReleaseResponse, 1) + ch := make(chan *release.TestReleaseResponse, 1) testEnv := &reltesting.Environment{ Namespace: rel.Namespace, - KubeClient: s.KubeClient, - Timeout: req.Timeout, - Mesages: ch, + KubeClient: r.cfg.KubeClient, + Timeout: r.Timeout, + Messages: ch, } - s.Log("running tests for release %s", rel.Name) + r.cfg.Log("running tests for release %s", rel.Name) tSuite := reltesting.NewTestSuite(rel) go func() { @@ -64,12 +80,12 @@ func (s *ReleaseServer) RunReleaseTest(req *hapi.TestReleaseRequest) (<-chan *ha Results: tSuite.Results, } - if req.Cleanup { + if r.Cleanup { testEnv.DeleteTestPods(tSuite.TestManifests) } - if err := s.Releases.Update(rel); err != nil { - s.Log("test: Failed to store updated release: %s", err) + if err := r.cfg.Releases.Update(rel); err != nil { + r.cfg.Log("test: Failed to store updated release: %s", err) } }() return ch, errc diff --git a/pkg/tiller/resource_policy.go b/pkg/action/resource_policy.go similarity index 84% rename from pkg/tiller/resource_policy.go rename to pkg/action/resource_policy.go index cca2391d8..53da7a002 100644 --- a/pkg/tiller/resource_policy.go +++ b/pkg/action/resource_policy.go @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package action import ( "bytes" "strings" "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/tiller/environment" + "k8s.io/helm/pkg/releaseutil" ) // resourcePolicyAnno is the annotation name for a resource policy @@ -33,9 +33,9 @@ const resourcePolicyAnno = "helm.sh/resource-policy" // during an uninstallRelease action. const keepPolicy = "keep" -func filterManifestsToKeep(manifests []Manifest) ([]Manifest, []Manifest) { - remaining := []Manifest{} - keep := []Manifest{} +func filterManifestsToKeep(manifests []releaseutil.Manifest) ([]releaseutil.Manifest, []releaseutil.Manifest) { + remaining := []releaseutil.Manifest{} + keep := []releaseutil.Manifest{} for _, m := range manifests { if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 { @@ -58,7 +58,7 @@ func filterManifestsToKeep(manifests []Manifest) ([]Manifest, []Manifest) { return keep, remaining } -func summarizeKeptManifests(manifests []Manifest, kubeClient environment.KubeClient, namespace string) string { +func summarizeKeptManifests(manifests []releaseutil.Manifest, kubeClient kube.KubernetesClient, namespace string) string { var message string for _, m := range manifests { // check if m is in fact present from k8s client's POV. diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go new file mode 100644 index 000000000..48c4dfda2 --- /dev/null +++ b/pkg/action/rollback.go @@ -0,0 +1,244 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + "fmt" + "sort" + "time" + + "github.com/pkg/errors" + + "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/release" +) + +// Rollback is the action for rolling back to a given release. +// +// It provides the implementation of 'helm rollback'. +type Rollback struct { + cfg *Configuration + + Version int + Timeout int64 + Wait bool + DisableHooks bool + DryRun bool + Recreate bool // will (if true) recreate pods after a rollback. + Force bool // will (if true) force resource upgrade through uninstall/recreate if needed +} + +// NewRollback creates a new Rollback object with the given configuration. +func NewRollback(cfg *Configuration) *Rollback { + return &Rollback{ + cfg: cfg, + } +} + +// Run executes 'helm rollback' against the given release. +func (r *Rollback) Run(name string) (*release.Release, error) { + r.cfg.Log("preparing rollback of %s", name) + currentRelease, targetRelease, err := r.prepareRollback(name) + if err != nil { + return nil, err + } + + if !r.DryRun { + r.cfg.Log("creating rolled back release for %s", name) + if err := r.cfg.Releases.Create(targetRelease); err != nil { + return nil, err + } + } + r.cfg.Log("performing rollback of %s", name) + res, err := r.performRollback(currentRelease, targetRelease) + if err != nil { + return res, err + } + + if !r.DryRun { + r.cfg.Log("updating status for rolled back release for %s", name) + if err := r.cfg.Releases.Update(targetRelease); err != nil { + return res, err + } + } + + return res, nil +} + +// prepareRollback finds the previous release and prepares a new release object with +// the previous release's configuration +func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) { + if err := validateReleaseName(name); err != nil { + return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name) + } + + if r.Version < 0 { + return nil, nil, errInvalidRevision + } + + currentRelease, err := r.cfg.Releases.Last(name) + if err != nil { + return nil, nil, err + } + + previousVersion := r.Version + if r.Version == 0 { + previousVersion = currentRelease.Version - 1 + } + + r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion) + + previousRelease, err := r.cfg.Releases.Get(name, previousVersion) + if err != nil { + return nil, nil, err + } + + // Store a new release object with previous release's configuration + targetRelease := &release.Release{ + Name: name, + Namespace: currentRelease.Namespace, + Chart: previousRelease.Chart, + Config: previousRelease.Config, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: time.Now(), + Status: release.StatusPendingRollback, + Notes: previousRelease.Info.Notes, + // Because we lose the reference to previous version elsewhere, we set the + // message here, and only override it later if we experience failure. + Description: fmt.Sprintf("Rollback to %d", previousVersion), + }, + Version: currentRelease.Version + 1, + Manifest: previousRelease.Manifest, + Hooks: previousRelease.Hooks, + } + + return currentRelease, targetRelease, nil +} + +func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) { + + if r.DryRun { + r.cfg.Log("dry run for %s", targetRelease.Name) + return targetRelease, nil + } + + // pre-rollback hooks + if !r.DisableHooks { + if err := r.execHook(targetRelease.Hooks, targetRelease.Namespace, hooks.PreRollback); err != nil { + return targetRelease, err + } + } else { + r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name) + } + + cr := bytes.NewBufferString(currentRelease.Manifest) + tr := bytes.NewBufferString(targetRelease.Manifest) + if err := r.cfg.KubeClient.Update(targetRelease.Namespace, cr, tr, r.Force, r.Recreate, r.Timeout, r.Wait); err != nil { + msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) + r.cfg.Log("warning: %s", msg) + currentRelease.Info.Status = release.StatusSuperseded + targetRelease.Info.Status = release.StatusFailed + targetRelease.Info.Description = msg + r.cfg.recordRelease(currentRelease, true) + r.cfg.recordRelease(targetRelease, true) + return targetRelease, err + } + + // post-rollback hooks + if !r.DisableHooks { + if err := r.execHook(targetRelease.Hooks, targetRelease.Namespace, hooks.PostRollback); err != nil { + return targetRelease, err + } + } + + deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name) + if err != nil { + return nil, err + } + // Supersede all previous deployments, see issue #2941. + for _, rel := range deployed { + r.cfg.Log("superseding previous deployment %d", rel.Version) + rel.Info.Status = release.StatusSuperseded + r.cfg.recordRelease(rel, true) + } + + targetRelease.Info.Status = release.StatusDeployed + + return targetRelease, nil +} + +// execHook executes all of the hooks for the given hook event. +func (r *Rollback) execHook(hs []*release.Hook, namespace, hook string) error { + timeout := r.Timeout + executingHooks := []*release.Hook{} + + for _, h := range hs { + for _, e := range h.Events { + if string(e) == hook { + executingHooks = append(executingHooks, h) + } + } + } + + sort.Sort(hookByWeight(executingHooks)) + + for _, h := range executingHooks { + if err := deleteHookByPolicy(r.cfg, namespace, h, hooks.BeforeHookCreation, hook); err != nil { + return err + } + + b := bytes.NewBufferString(h.Manifest) + if err := r.cfg.KubeClient.Create(namespace, b, timeout, false); err != nil { + return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) + } + b.Reset() + b.WriteString(h.Manifest) + + if err := r.cfg.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil { + // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted + // under failed condition. If so, then clear the corresponding resource object in the hook + if err := deleteHookByPolicy(r.cfg, namespace, h, hooks.HookFailed, hook); err != nil { + return err + } + return err + } + } + + // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted + // under succeeded condition. If so, then clear the corresponding resource object in each hook + for _, h := range executingHooks { + if err := deleteHookByPolicy(r.cfg, namespace, h, hooks.HookSucceeded, hook); err != nil { + return err + } + h.LastRun = time.Now() + } + + return nil +} + +// deleteHookByPolicy deletes a hook if the hook policy instructs it to +func deleteHookByPolicy(cfg *Configuration, namespace string, h *release.Hook, policy, hook string) error { + b := bytes.NewBufferString(h.Manifest) + if hookHasDeletePolicy(h, policy) { + if errHookDelete := cfg.KubeClient.Delete(namespace, b); errHookDelete != nil { + return errHookDelete + } + } + return nil +} diff --git a/pkg/action/show.go b/pkg/action/show.go new file mode 100644 index 000000000..d12cabc71 --- /dev/null +++ b/pkg/action/show.go @@ -0,0 +1,108 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "strings" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/chart/loader" +) + +type ShowOutputFormat string + +const ( + ShowAll ShowOutputFormat = "all" + ShowChart ShowOutputFormat = "chart" + ShowValues ShowOutputFormat = "values" + ShowReadme ShowOutputFormat = "readme" +) + +var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} + +func (o ShowOutputFormat) String() string { + return string(o) +} + +// Show is the action for checking a given release's information. +// +// It provides the implementation of 'helm show' and its respective subcommands. +type Show struct { + OutputFormat ShowOutputFormat + ChartPathOptions +} + +// NewShow creates a new Show object with the given configuration. +func NewShow(output ShowOutputFormat) *Show { + return &Show{ + OutputFormat: output, + } +} + +// Run executes 'helm show' against the given release. +func (s *Show) Run(chartpath string) (string, error) { + var out strings.Builder + chrt, err := loader.Load(chartpath) + if err != nil { + return "", err + } + cf, err := yaml.Marshal(chrt.Metadata) + if err != nil { + return "", err + } + + if s.OutputFormat == ShowChart || s.OutputFormat == ShowAll { + fmt.Fprintf(&out, "%s\n", cf) + } + + if (s.OutputFormat == ShowValues || s.OutputFormat == ShowAll) && chrt.Values != nil { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + b, err := yaml.Marshal(chrt.Values) + if err != nil { + return "", err + } + fmt.Fprintf(&out, "%s\n", b) + } + + if s.OutputFormat == ShowReadme || s.OutputFormat == ShowAll { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + readme := findReadme(chrt.Files) + if readme == nil { + return out.String(), nil + } + fmt.Fprintf(&out, "%s\n", readme.Data) + } + return out.String(), nil +} + +func findReadme(files []*chart.File) (file *chart.File) { + for _, file := range files { + for _, n := range readmeFileNames { + if strings.EqualFold(file.Name, n) { + return file + } + } + } + return nil +} diff --git a/cmd/helm/show_test.go b/pkg/action/show_test.go similarity index 68% rename from cmd/helm/show_test.go rename to pkg/action/show_test.go index 1da5c630d..02fb5d042 100644 --- a/cmd/helm/show_test.go +++ b/pkg/action/show_test.go @@ -14,38 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package action import ( - "bytes" "io/ioutil" "strings" "testing" ) func TestShow(t *testing.T) { - b := bytes.NewBuffer(nil) + client := NewShow(ShowAll) - o := &showOptions{ - chartpath: "testdata/testcharts/alpine", - output: all, + output, err := client.Run("../../cmd/helm/testdata/testcharts/alpine") + if err != nil { + t.Fatal(err) } - o.run(b) // Load the data from the textfixture directly. - cdata, err := ioutil.ReadFile("testdata/testcharts/alpine/Chart.yaml") + cdata, err := ioutil.ReadFile("../../cmd/helm/testdata/testcharts/alpine/Chart.yaml") if err != nil { t.Fatal(err) } - data, err := ioutil.ReadFile("testdata/testcharts/alpine/values.yaml") + data, err := ioutil.ReadFile("../../cmd/helm/testdata/testcharts/alpine/values.yaml") if err != nil { t.Fatal(err) } - readmeData, err := ioutil.ReadFile("testdata/testcharts/alpine/README.md") + readmeData, err := ioutil.ReadFile("../../cmd/helm/testdata/testcharts/alpine/README.md") if err != nil { t.Fatal(err) } - parts := strings.SplitN(b.String(), "---", 3) + parts := strings.SplitN(output, "---", 3) if len(parts) != 3 { t.Fatalf("Expected 2 parts, got %d", len(parts)) } @@ -66,13 +64,13 @@ func TestShow(t *testing.T) { } // Regression tests for missing values. See issue #1024. - b.Reset() - o = &showOptions{ - chartpath: "testdata/testcharts/novals", - output: "values", + client.OutputFormat = ShowValues + output, err = client.Run("../../cmd/helm/testdata/testcharts/novals") + if err != nil { + t.Fatal(err) } - o.run(b) - if b.Len() != 0 { - t.Errorf("expected empty values buffer, got %q", b.String()) + + if len(output) != 0 { + t.Errorf("expected empty values buffer, got %s", output) } } diff --git a/pkg/action/status.go b/pkg/action/status.go new file mode 100644 index 000000000..3f7f684a3 --- /dev/null +++ b/pkg/action/status.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "k8s.io/helm/pkg/release" +) + +// Status is the action for checking the deployment status of releases. +// +// It provides the implementation of 'helm status'. +type Status struct { + cfg *Configuration + + Version int + OutputFormat string +} + +// NewStatus creates a new Status object with the given configuration. +func NewStatus(cfg *Configuration) *Status { + return &Status{ + cfg: cfg, + } +} + +// Run executes 'helm status' against the given release. +func (s *Status) Run(name string) (*release.Release, error) { + return s.cfg.releaseContent(name, s.Version) +} diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go new file mode 100644 index 000000000..37df65921 --- /dev/null +++ b/pkg/action/uninstall.go @@ -0,0 +1,241 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + + "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" + "k8s.io/helm/pkg/releaseutil" +) + +// Uninstall is the action for uninstalling releases. +// +// It provides the implementation of 'helm uninstall'. +type Uninstall struct { + cfg *Configuration + + DisableHooks bool + DryRun bool + Purge bool + Timeout int64 +} + +// NewUninstall creates a new Uninstall object with the given configuration. +func NewUninstall(cfg *Configuration) *Uninstall { + return &Uninstall{ + cfg: cfg, + } +} + +// Run uninstalls the given release. +func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) { + if u.DryRun { + // In the dry run case, just see if the release exists + r, err := u.cfg.releaseContent(name, 0) + if err != nil { + return &release.UninstallReleaseResponse{}, err + } + return &release.UninstallReleaseResponse{Release: r}, nil + } + + if err := validateReleaseName(name); err != nil { + return nil, errors.Errorf("uninstall: Release name is invalid: %s", name) + } + + rels, err := u.cfg.Releases.History(name) + if err != nil { + return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name) + } + if len(rels) < 1 { + return nil, errMissingRelease + } + + releaseutil.SortByRevision(rels) + rel := rels[len(rels)-1] + + // TODO: Are there any cases where we want to force a delete even if it's + // already marked deleted? + if rel.Info.Status == release.StatusUninstalled { + if u.Purge { + if err := u.purgeReleases(rels...); err != nil { + return nil, errors.Wrap(err, "uninstall: Failed to purge the release") + } + return &release.UninstallReleaseResponse{Release: rel}, nil + } + return nil, errors.Errorf("the release named %q is already deleted", name) + } + + u.cfg.Log("uninstall: Deleting %s", name) + rel.Info.Status = release.StatusUninstalling + rel.Info.Deleted = time.Now() + rel.Info.Description = "Deletion in progress (or silently failed)" + res := &release.UninstallReleaseResponse{Release: rel} + + if !u.DisableHooks { + if err := u.execHook(rel.Hooks, rel.Namespace, hooks.PreDelete); err != nil { + return res, err + } + } else { + u.cfg.Log("delete hooks disabled for %s", name) + } + + // From here on out, the release is currently considered to be in StatusUninstalling + // state. + if err := u.cfg.Releases.Update(rel); err != nil { + u.cfg.Log("uninstall: Failed to store updated release: %s", err) + } + + kept, errs := u.deleteRelease(rel) + res.Info = kept + + if !u.DisableHooks { + if err := u.execHook(rel.Hooks, rel.Namespace, hooks.PostDelete); err != nil { + errs = append(errs, err) + } + } + + rel.Info.Status = release.StatusUninstalled + rel.Info.Description = "Uninstallation complete" + + if u.Purge { + u.cfg.Log("purge requested for %s", name) + err := u.purgeReleases(rels...) + return res, errors.Wrap(err, "uninstall: Failed to purge the release") + } + + if err := u.cfg.Releases.Update(rel); err != nil { + u.cfg.Log("uninstall: Failed to store updated release: %s", err) + } + + if len(errs) > 0 { + return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) + } + return res, nil +} + +func (u *Uninstall) purgeReleases(rels ...*release.Release) error { + for _, rel := range rels { + if _, err := u.cfg.Releases.Delete(rel.Name, rel.Version); err != nil { + return err + } + } + return nil +} + +func joinErrors(errs []error) string { + es := make([]string, 0, len(errs)) + for _, e := range errs { + es = append(es, e.Error()) + } + return strings.Join(es, "; ") +} + +// execHook executes all of the hooks for the given hook event. +func (u *Uninstall) execHook(hs []*release.Hook, namespace, hook string) error { + timeout := u.Timeout + executingHooks := []*release.Hook{} + + for _, h := range hs { + for _, e := range h.Events { + if string(e) == hook { + executingHooks = append(executingHooks, h) + } + } + } + + sort.Sort(hookByWeight(executingHooks)) + + for _, h := range executingHooks { + if err := deleteHookByPolicy(u.cfg, namespace, h, hooks.BeforeHookCreation, hook); err != nil { + return err + } + + b := bytes.NewBufferString(h.Manifest) + if err := u.cfg.KubeClient.Create(namespace, b, timeout, false); err != nil { + return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) + } + b.Reset() + b.WriteString(h.Manifest) + + if err := u.cfg.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil { + // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted + // under failed condition. If so, then clear the corresponding resource object in the hook + if err := deleteHookByPolicy(u.cfg, namespace, h, hooks.HookFailed, hook); err != nil { + return err + } + return err + } + } + + // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted + // under succeeded condition. If so, then clear the corresponding resource object in each hook + for _, h := range executingHooks { + if err := deleteHookByPolicy(u.cfg, namespace, h, hooks.HookSucceeded, hook); err != nil { + return err + } + h.LastRun = time.Now() + } + + return nil +} + +// deleteRelease deletes the release and returns manifests that were kept in the deletion process +func (u *Uninstall) deleteRelease(rel *release.Release) (kept string, errs []error) { + caps, err := newCapabilities(u.cfg.Discovery) + if err != nil { + return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} + } + + manifests := releaseutil.SplitManifests(rel.Manifest) + _, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder) + if err != nil { + // We could instead just delete everything in no particular order. + // FIXME: One way to delete at this point would be to try a label-based + // deletion. The problem with this is that we could get a false positive + // and delete something that was not legitimately part of this release. + return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} + } + + filesToKeep, filesToDelete := filterManifestsToKeep(files) + if len(filesToKeep) > 0 { + kept = summarizeKeptManifests(filesToKeep, u.cfg.KubeClient, rel.Namespace) + } + + for _, file := range filesToDelete { + b := bytes.NewBufferString(strings.TrimSpace(file.Content)) + if b.Len() == 0 { + continue + } + if err := u.cfg.KubeClient.Delete(rel.Namespace, b); err != nil { + u.cfg.Log("uninstall: Failed deletion of %q: %s", rel.Name, err) + if err == kube.ErrNoObjectsVisited { + // Rewrite the message from "no objects visited" + err = errors.New("object not found, skipping delete") + } + errs = append(errs, err) + } + } + return kept, errs +} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go new file mode 100644 index 000000000..8e8dce162 --- /dev/null +++ b/pkg/action/upgrade.go @@ -0,0 +1,417 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + "fmt" + "path" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + "k8s.io/client-go/discovery" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/engine" + "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" + "k8s.io/helm/pkg/releaseutil" + "k8s.io/helm/pkg/version" +) + +// Upgrade is the action for upgrading releases. +// +// It provides the implementation of 'helm upgrade'. +type Upgrade struct { + cfg *Configuration + + ChartPathOptions + ValueOptions + + Install bool + Devel bool + Namespace string + Timeout int64 + Wait bool + // Values is a string containing (unparsed) YAML values. + Values map[string]interface{} + DisableHooks bool + DryRun bool + Force bool + ResetValues bool + 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 +} + +// NewUpgrade creates a new Upgrade object with the given configuration. +func NewUpgrade(cfg *Configuration) *Upgrade { + return &Upgrade{ + cfg: cfg, + } +} + +// Run executes the upgrade on the given release. +func (u *Upgrade) Run(name string, chart *chart.Chart) (*release.Release, error) { + if err := chartutil.ProcessDependencies(chart, u.Values); err != nil { + return nil, err + } + + if err := validateReleaseName(name); err != nil { + return nil, errors.Errorf("upgradeRelease: Release name is invalid: %s", name) + } + u.cfg.Log("preparing upgrade for %s", name) + currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart) + if err != nil { + return nil, err + } + + u.cfg.Releases.MaxHistory = u.MaxHistory + + if !u.DryRun { + u.cfg.Log("creating upgraded release for %s", name) + if err := u.cfg.Releases.Create(upgradedRelease); err != nil { + return nil, err + } + } + + u.cfg.Log("performing update for %s", name) + res, err := u.performUpgrade(currentRelease, upgradedRelease) + if err != nil { + return res, err + } + + if !u.DryRun { + u.cfg.Log("updating status for upgraded release for %s", name) + if err := u.cfg.Releases.Update(upgradedRelease); err != nil { + return res, err + } + } + + return res, nil +} + +func validateReleaseName(releaseName string) error { + if releaseName == "" { + return errMissingRelease + } + + if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { + return errInvalidName + } + + return nil +} + +// prepareUpgrade builds an upgraded release for an upgrade operation. +func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart) (*release.Release, *release.Release, error) { + if chart == nil { + return nil, nil, errMissingChart + } + + // finds the deployed release with the given name + currentRelease, err := u.cfg.Releases.Deployed(name) + if err != nil { + return nil, nil, err + } + + // determine if values will be reused + if err := u.reuseValues(chart, currentRelease); err != nil { + 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 + + options := chartutil.ReleaseOptions{ + Name: name, + IsUpgrade: true, + } + + caps, err := newCapabilities(u.cfg.Discovery) + if err != nil { + return nil, nil, err + } + valuesToRender, err := chartutil.ToRenderValues(chart, u.Values, options, caps) + if err != nil { + return nil, nil, err + } + + hooks, manifestDoc, notesTxt, err := u.renderResources(chart, valuesToRender, caps.APIVersions) + if err != nil { + return nil, nil, err + } + + // Store an upgraded release. + upgradedRelease := &release.Release{ + Name: name, + Namespace: currentRelease.Namespace, + Chart: chart, + Config: u.Values, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: Timestamper(), + Status: release.StatusPendingUpgrade, + Description: "Preparing upgrade", // This should be overwritten later. + }, + Version: revision, + Manifest: manifestDoc.String(), + Hooks: hooks, + } + + if len(notesTxt) > 0 { + upgradedRelease.Info.Notes = notesTxt + } + err = validateManifest(u.cfg.KubeClient, currentRelease.Namespace, manifestDoc.Bytes()) + return currentRelease, upgradedRelease, err +} + +func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) { + if u.DryRun { + u.cfg.Log("dry run for %s", upgradedRelease.Name) + upgradedRelease.Info.Description = "Dry run complete" + return upgradedRelease, nil + } + + // pre-upgrade hooks + if !u.DisableHooks { + if err := u.execHook(upgradedRelease.Hooks, hooks.PreUpgrade); err != nil { + return upgradedRelease, err + } + } else { + u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) + } + if err := u.upgradeRelease(originalRelease, upgradedRelease); err != nil { + msg := fmt.Sprintf("Upgrade %q failed: %s", upgradedRelease.Name, err) + u.cfg.Log("warning: %s", msg) + upgradedRelease.Info.Status = release.StatusFailed + upgradedRelease.Info.Description = msg + u.cfg.recordRelease(originalRelease, true) + u.cfg.recordRelease(upgradedRelease, true) + return upgradedRelease, err + } + + // post-upgrade hooks + if !u.DisableHooks { + if err := u.execHook(upgradedRelease.Hooks, hooks.PostUpgrade); err != nil { + return upgradedRelease, err + } + } + + originalRelease.Info.Status = release.StatusSuperseded + u.cfg.recordRelease(originalRelease, true) + + upgradedRelease.Info.Status = release.StatusDeployed + upgradedRelease.Info.Description = "Upgrade complete" + + return upgradedRelease, nil +} + +// upgradeRelease performs an upgrade from current to target release +func (u *Upgrade) upgradeRelease(current, target *release.Release) error { + cm := bytes.NewBufferString(current.Manifest) + tm := bytes.NewBufferString(target.Manifest) + return u.cfg.KubeClient.Update(target.Namespace, cm, tm, u.Force, u.Recreate, u.Timeout, u.Wait) +} + +// reuseValues copies values from the current release to a new release if the +// new release does not have any values. +// +// If the request already has values, or if there are no values in the current +// release, this does nothing. +// +// This is skipped if the u.ResetValues flag is set, in which case the +// request values are not altered. +func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release) error { + if u.ResetValues { + // If ResetValues is set, we comletely ignore current.Config. + u.cfg.Log("resetting values to the chart's original version") + return nil + } + + // If the ReuseValues flag is set, we always copy the old values over the new config's values. + if u.ReuseValues { + u.cfg.Log("reusing the old release's values") + + // We have to regenerate the old coalesced values: + oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) + if err != nil { + return errors.Wrap(err, "failed to rebuild old values") + } + + u.Values = chartutil.CoalesceTables(current.Config, u.Values) + + chart.Values = oldVals + + return nil + } + + if len(u.Values) == 0 && len(current.Config) > 0 { + u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) + u.Values = current.Config + } + return nil +} + +func newCapabilities(dc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) { + kubeVersion, err := dc.ServerVersion() + if err != nil { + return nil, err + } + + apiVersions, err := GetVersionSet(dc) + if err != nil { + return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") + } + + return &chartutil.Capabilities{ + KubeVersion: kubeVersion, + APIVersions: apiVersions, + }, nil +} + +func (u *Upgrade) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { + if ch.Metadata.KubeVersion != "" { + cap, _ := values["Capabilities"].(*chartutil.Capabilities) + gitVersion := cap.KubeVersion.String() + k8sVersion := strings.Split(gitVersion, "+")[0] + if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { + return nil, nil, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) + } + } + + u.cfg.Log("rendering %s chart using values", ch.Name()) + files, err := engine.Render(ch, values) + if err != nil { + return nil, nil, "", err + } + + // 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. + notes := "" + for k, v := range files { + if strings.HasSuffix(k, notesFileSuffix) { + // Only apply the notes if it belongs to the parent chart + // Note: Do not use filePath.Join since it creates a path with \ which is not expected + if k == path.Join(ch.Name(), "templates", notesFileSuffix) { + notes = v + } + delete(files, k) + } + } + + // 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. + hooks, manifests, err := releaseutil.SortManifests(files, vs, 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. + b := bytes.NewBuffer(nil) + for name, content := range files { + if len(strings.TrimSpace(content)) == 0 { + continue + } + b.WriteString("\n---\n# Source: " + name + "\n") + b.WriteString(content) + } + return nil, b, "", err + } + + // Aggregate all valid manifests into one big doc. + b := bytes.NewBuffer(nil) + for _, m := range manifests { + b.WriteString("\n---\n# Source: " + m.Name + "\n") + b.WriteString(m.Content) + } + + return hooks, b, notes, nil +} + +func validateManifest(c kube.KubernetesClient, ns string, manifest []byte) error { + r := bytes.NewReader(manifest) + _, err := c.BuildUnstructured(ns, r) + return err +} + +// execHook executes all of the hooks for the given hook event. +func (u *Upgrade) execHook(hs []*release.Hook, hook string) error { + timeout := u.Timeout + executingHooks := []*release.Hook{} + + for _, h := range hs { + for _, e := range h.Events { + if string(e) == hook { + executingHooks = append(executingHooks, h) + } + } + } + + sort.Sort(hookByWeight(executingHooks)) + + for _, h := range executingHooks { + if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.BeforeHookCreation, hook); err != nil { + return err + } + + b := bytes.NewBufferString(h.Manifest) + if err := u.cfg.KubeClient.Create(u.Namespace, b, timeout, false); err != nil { + return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) + } + b.Reset() + b.WriteString(h.Manifest) + + if err := u.cfg.KubeClient.WatchUntilReady(u.Namespace, b, timeout, false); err != nil { + // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted + // under failed condition. If so, then clear the corresponding resource object in the hook + if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.HookFailed, hook); err != nil { + return err + } + return err + } + } + + // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted + // under succeeded condition. If so, then clear the corresponding resource object in each hook + for _, h := range executingHooks { + if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.HookSucceeded, hook); err != nil { + return err + } + h.LastRun = time.Now() + } + + return nil +} diff --git a/pkg/tiller/hook_sorter.go b/pkg/action/util.go similarity index 61% rename from pkg/tiller/hook_sorter.go rename to pkg/action/util.go index 35bff9bed..f80002c99 100644 --- a/pkg/tiller/hook_sorter.go +++ b/pkg/action/util.go @@ -14,19 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package action -import ( - "k8s.io/helm/pkg/hapi/release" -) - -type hookByWeight []*release.Hook - -func (x hookByWeight) Len() int { return len(x) } -func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] } -func (x hookByWeight) Less(i, j int) bool { - if x[i].Weight == x[j].Weight { - return x[i].Name < x[j].Name +func min(x, y int) int { + if x < y { + return x } - return x[i].Weight < x[j].Weight + return y } diff --git a/pkg/tiller/release_content.go b/pkg/action/verify.go similarity index 52% rename from pkg/tiller/release_content.go rename to pkg/action/verify.go index a97a7c3a3..533ef8813 100644 --- a/pkg/tiller/release_content.go +++ b/pkg/action/verify.go @@ -14,24 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package action import ( - "github.com/pkg/errors" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/downloader" ) -// GetReleaseContent gets all of the stored information for the given release. -func (s *ReleaseServer) GetReleaseContent(req *hapi.GetReleaseContentRequest) (*release.Release, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, errors.Errorf("releaseContent: Release name is invalid: %s", req.Name) - } +// Verify is the action for building a given chart's Verify tree. +// +// It provides the implementation of 'helm verify'. +type Verify struct { + Keyring string +} - if req.Version <= 0 { - return s.Releases.Last(req.Name) - } +// NewVerify creates a new Verify object with the given configuration. +func NewVerify() *Verify { + return &Verify{} +} - return s.Releases.Get(req.Name, req.Version) +// Run executes 'helm verify'. +func (v *Verify) Run(chartfile string) error { + _, err := downloader.VerifyChart(chartfile, v.Keyring) + return err } diff --git a/pkg/helm/environment/environment.go b/pkg/cli/environment.go similarity index 92% rename from pkg/helm/environment/environment.go rename to pkg/cli/environment.go index 5bc6f70ca..a3f65643c 100644 --- a/pkg/helm/environment/environment.go +++ b/pkg/cli/environment.go @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package environment describes the operating environment for Tiller. +/*Package cli describes the operating environment for the Helm CLI. -Tiller's environment encapsulates all of the service dependencies Tiller has. +Helm's environment encapsulates all of the service dependencies Helm has. These dependencies are expressed as interfaces so that alternate implementations (mocks, etc.) can be easily generated. */ -package environment +package cli import ( "os" @@ -29,7 +29,7 @@ import ( "github.com/spf13/pflag" "k8s.io/client-go/util/homedir" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) // defaultHelmHome is the default HELM_HOME. diff --git a/pkg/helm/environment/environment_test.go b/pkg/cli/environment_test.go similarity index 98% rename from pkg/helm/environment/environment_test.go rename to pkg/cli/environment_test.go index f54127c6d..e5411406f 100644 --- a/pkg/helm/environment/environment_test.go +++ b/pkg/cli/environment_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package environment +package cli import ( "os" @@ -23,7 +23,7 @@ import ( "github.com/spf13/pflag" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) func TestEnvSettings(t *testing.T) { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index c390a157e..6d67c984e 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -27,7 +27,7 @@ import ( "github.com/pkg/errors" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/urlutil" diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 86ed2c76b..f8b47c57c 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -25,9 +25,9 @@ import ( "path/filepath" "testing" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo/repotest" ) @@ -57,7 +57,7 @@ func TestResolveChartRef(t *testing.T) { c := ChartDownloader{ HelmHome: helmpath.Home("testdata/helmhome"), Out: os.Stderr, - Getters: getter.All(environment.EnvSettings{}), + Getters: getter.All(cli.EnvSettings{}), } for _, tt := range tests { @@ -94,7 +94,7 @@ func TestDownload(t *testing.T) { })) defer srv.Close() - provider, err := getter.ByScheme("http", environment.EnvSettings{}) + provider, err := getter.ByScheme("http", cli.EnvSettings{}) if err != nil { t.Fatal("No http provider found") } @@ -197,7 +197,7 @@ func TestDownloadTo(t *testing.T) { Out: os.Stderr, Verify: VerifyAlways, Keyring: "testdata/helm-test-key.pub", - Getters: getter.All(environment.EnvSettings{}), + Getters: getter.All(cli.EnvSettings{}), } cname := "/signtest-0.1.0.tgz" where, v, err := c.DownloadTo(srv.URL()+cname, "", dest) @@ -260,7 +260,7 @@ func TestDownloadTo_VerifyLater(t *testing.T) { HelmHome: hh, Out: os.Stderr, Verify: VerifyLater, - Getters: getter.All(environment.EnvSettings{}), + Getters: getter.All(cli.EnvSettings{}), } cname := "/signtest-0.1.0.tgz" where, _, err := c.DownloadTo(srv.URL()+cname, "", dest) @@ -289,7 +289,7 @@ func TestScanReposForURL(t *testing.T) { HelmHome: hh, Out: os.Stderr, Verify: VerifyLater, - Getters: getter.All(environment.EnvSettings{}), + Getters: getter.All(cli.EnvSettings{}), } u := "http://example.com/alpine-0.2.0.tgz" diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 17d04ffdd..1800be48e 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -34,7 +34,7 @@ import ( "k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/resolver" "k8s.io/helm/pkg/urlutil" diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index d87582dfe..ad230ba74 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -21,7 +21,7 @@ import ( "testing" "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) func TestVersionEquals(t *testing.T) { diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 75fcc9e77..087cc0805 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -21,7 +21,7 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/environment" + "k8s.io/helm/pkg/cli" ) // Getter is an interface to support GET to the specified URL. @@ -71,7 +71,7 @@ func (p Providers) ByScheme(scheme string) (Constructor, error) { // All finds all of the registered getters as a list of Provider instances. // Currently the build-in http/https getter and the discovered // plugins with downloader notations are collected. -func All(settings environment.EnvSettings) Providers { +func All(settings cli.EnvSettings) Providers { result := Providers{ { Schemes: []string{"http", "https"}, @@ -86,7 +86,7 @@ func All(settings environment.EnvSettings) Providers { // ByScheme returns a getter for the given scheme. // // If the scheme is not supported, this will return an error. -func ByScheme(scheme string, settings environment.EnvSettings) (Provider, error) { +func ByScheme(scheme string, settings cli.EnvSettings) (Provider, error) { // Q: What do you call a scheme string who's the boss? // A: Bruce Schemestring, of course. a := All(settings) diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index 6f5b6969e..61c1eda90 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -23,13 +23,13 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/environment" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/plugin" ) // collectPlugins scans for getter plugins. -// This will load plugins according to the environment. -func collectPlugins(settings environment.EnvSettings) (Providers, error) { +// This will load plugins according to the cli. +func collectPlugins(settings cli.EnvSettings) (Providers, error) { plugins, err := plugin.FindPlugins(settings.PluginDirs()) if err != nil { return nil, err @@ -56,7 +56,7 @@ func collectPlugins(settings environment.EnvSettings) (Providers, error) { type pluginGetter struct { command string certFile, keyFile, cAFile string - settings environment.EnvSettings + settings cli.EnvSettings name string base string } @@ -81,7 +81,7 @@ func (p *pluginGetter) Get(href string) (*bytes.Buffer, error) { } // newPluginGetter constructs a valid plugin getter -func newPluginGetter(command string, settings environment.EnvSettings, name, base string) Constructor { +func newPluginGetter(command string, settings cli.EnvSettings, name, base string) Constructor { return func(URL, CertFile, KeyFile, CAFile string) (Getter, error) { result := &pluginGetter{ command: command, diff --git a/pkg/getter/plugingetter_test.go b/pkg/getter/plugingetter_test.go index 7c0bd6c1e..a147928b3 100644 --- a/pkg/getter/plugingetter_test.go +++ b/pkg/getter/plugingetter_test.go @@ -22,17 +22,17 @@ import ( "strings" "testing" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/cli" + "k8s.io/helm/pkg/helmpath" ) -func hh(debug bool) environment.EnvSettings { +func hh(debug bool) cli.EnvSettings { apath, err := filepath.Abs("./testdata") if err != nil { panic(err) } hp := helmpath.Home(apath) - return environment.EnvSettings{ + return cli.EnvSettings{ Home: hp, Debug: debug, } diff --git a/pkg/hapi/tiller.go b/pkg/hapi/tiller.go deleted file mode 100644 index 544fb6a96..000000000 --- a/pkg/hapi/tiller.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -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 hapi - -import ( - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi/release" -) - -// SortBy defines sort operations. -type SortBy string - -const ( - // SortByName requests releases sorted by name. - SortByName SortBy = "name" - // SortByLastReleased requests releases sorted by last released. - SortByLastReleased SortBy = "last-released" -) - -// SortOrder defines sort orders to augment sorting operations. -type SortOrder string - -const ( - //SortAsc defines ascending sorting. - SortAsc SortOrder = "ascending" - //SortDesc defines descending sorting. - SortDesc SortOrder = "descending" -) - -// ListReleasesRequest requests a list of releases. -// -// Releases can be retrieved in chunks by setting limit and offset. -// -// Releases can be sorted according to a few pre-determined sort stategies. -type ListReleasesRequest struct { - // Limit is the maximum number of releases to be returned. - Limit int64 `json:"limit,omitempty"` - // Offset is the last release name that was seen. The next listing - // operation will start with the name after this one. - // Example: If list one returns albert, bernie, carl, and sets 'next: dennis'. - // dennis is the offset. Supplying 'dennis' for the next request should - // cause the next batch to return a set of results starting with 'dennis'. - Offset string `json:"offset,omitempty"` - // SortBy is the sort field that the ListReleases server should sort data before returning. - SortBy SortBy `json:"sort_by,omitempty"` - // Filter is a regular expression used to filter which releases should be listed. - // - // Anything that matches the regexp will be included in the results. - Filter string `json:"filter,omitempty"` - // SortOrder is the ordering directive used for sorting. - SortOrder SortOrder `json:"sort_order,omitempty"` - StatusCodes []release.Status `json:"status_codes,omitempty"` -} - -// GetReleaseStatusRequest is a request to get the status of a release. -type GetReleaseStatusRequest struct { - // Name is the name of the release - Name string `json:"name,omitempty"` - // Version is the version of the release - Version int `json:"version,omitempty"` -} - -// GetReleaseStatusResponse is the response indicating the status of the named release. -type GetReleaseStatusResponse struct { - // Name is the name of the release. - Name string `json:"name,omitempty"` - // Info contains information about the release. - Info *release.Info `json:"info,omitempty"` - // Namespace the release was released into - Namespace string `json:"namespace,omitempty"` -} - -// GetReleaseContentRequest is a request to get the contents of a release. -type GetReleaseContentRequest struct { - // The name of the release - Name string `json:"name,omitempty"` - // Version is the version of the release - Version int `json:"version,omitempty"` -} - -// UpdateReleaseRequest updates a release. -type UpdateReleaseRequest struct { - // The name of the release - Name string `json:"name,omitempty"` - // Chart is the protobuf representation of a chart. - Chart *chart.Chart `json:"chart,omitempty"` - // Values is a string containing (unparsed) YAML values. - Values map[string]interface{} `json:"values,omitempty"` - // dry_run, if true, will run through the release logic, but neither create - DryRun bool `json:"dry_run,omitempty"` - // DisableHooks causes the server to skip running any hooks for the upgrade. - DisableHooks bool `json:"disable_hooks,omitempty"` - // Performs pods restart for resources if applicable - Recreate bool `json:"recreate,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `json:"timeout,omitempty"` - // ResetValues will cause Tiller to ignore stored values, resetting to default values. - ResetValues bool `json:"reset_values,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `json:"wait,omitempty"` - // ReuseValues will cause Tiller to reuse the values from the last release. - // This is ignored if reset_values is set. - ReuseValues bool `json:"reuse_values,omitempty"` - // Force resource update through delete/recreate if needed. - Force bool `json:"force,omitempty"` - // Limit the maximum number of revisions saved per release. - MaxHistory int `json:"max_history,omitempty"` -} - -// RollbackReleaseRequest is the request for a release to be rolledback to a -// previous version. -type RollbackReleaseRequest struct { - // The name of the release - Name string `json:"name,omitempty"` - // dry_run, if true, will run through the release logic but no create - DryRun bool `json:"dry_run,omitempty"` - // DisableHooks causes the server to skip running any hooks for the rollback - DisableHooks bool `json:"disable_hooks,omitempty"` - // Version is the version of the release to deploy. - Version int `json:"version,omitempty"` - // Performs pods restart for resources if applicable - Recreate bool `json:"recreate,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `json:"timeout,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `json:"wait,omitempty"` - // Force resource update through delete/recreate if needed. - Force bool `json:"force,omitempty"` -} - -// InstallReleaseRequest is the request for an installation of a chart. -type InstallReleaseRequest struct { - // Chart is the protobuf representation of a chart. - Chart *chart.Chart `json:"chart,omitempty"` - // Values is a string containing (unparsed) YAML values. - Values map[string]interface{} `json:"values,omitempty"` - // DryRun, if true, will run through the release logic, but neither create - // a release object nor deploy to Kubernetes. The release object returned - // in the response will be fake. - DryRun bool `json:"dry_run,omitempty"` - // Name is the candidate release name. This must be unique to the - // namespace, otherwise the server will return an error. If it is not - // supplied, the server will autogenerate one. - Name string `json:"name,omitempty"` - // DisableHooks causes the server to skip running any hooks for the install. - DisableHooks bool `json:"disable_hooks,omitempty"` - // Namepace is the kubernetes namespace of the release. - Namespace string `json:"namespace,omitempty"` - // ReuseName requests that Tiller re-uses a name, instead of erroring out. - ReuseName bool `json:"reuse_name,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `json:"timeout,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `json:"wait,omitempty"` -} - -// UninstallReleaseRequest represents a request to uninstall a named release. -type UninstallReleaseRequest struct { - // Name is the name of the release to delete. - Name string `json:"name,omitempty"` - // DisableHooks causes the server to skip running any hooks for the uninstall. - DisableHooks bool `json:"disable_hooks,omitempty"` - // Purge removes the release from the store and make its name free for later use. - Purge bool `json:"purge,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `json:"timeout,omitempty"` -} - -// UninstallReleaseResponse represents a successful response to an uninstall request. -type UninstallReleaseResponse struct { - // Release is the release that was marked deleted. - Release *release.Release `json:"release,omitempty"` - // Info is an uninstall message - Info string `json:"info,omitempty"` -} - -// GetHistoryRequest requests a release's history. -type GetHistoryRequest struct { - // The name of the release. - Name string `json:"name,omitempty"` - // The maximum number of releases to include. - Max int `json:"max,omitempty"` -} - -// TestReleaseRequest is a request to get the status of a release. -type TestReleaseRequest struct { - // Name is the name of the release - Name string `json:"name,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `json:"timeout,omitempty"` - // cleanup specifies whether or not to attempt pod deletion after test completes - Cleanup bool `json:"cleanup,omitempty"` -} - -// TestReleaseResponse represents a message from executing a test -type TestReleaseResponse struct { - Msg string `json:"msg,omitempty"` - Status release.TestRunStatus `json:"status,omitempty"` -} diff --git a/pkg/helm/client.go b/pkg/helm/client.go deleted file mode 100644 index 645a48f0e..000000000 --- a/pkg/helm/client.go +++ /dev/null @@ -1,209 +0,0 @@ -/* -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 helm // import "k8s.io/helm/pkg/helm" - -import ( - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/tiller" -) - -// Client manages client side of the Helm-Tiller protocol. -type Client struct { - opts options - tiller *tiller.ReleaseServer -} - -// NewClient creates a new client. -func NewClient(opts ...Option) *Client { - var c Client - return c.Option(opts...).init() -} - -func (c *Client) init() *Client { - c.tiller = tiller.NewReleaseServer(c.opts.discovery, c.opts.kubeClient) - c.tiller.Releases = storage.Init(c.opts.driver) - return c -} - -// Option configures the Helm client with the provided options. -func (c *Client) Option(opts ...Option) *Client { - for _, opt := range opts { - opt(&c.opts) - } - return c -} - -// InstallRelease loads a chart from chstr, installs it, and returns the release response. -func (c *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*release.Release, error) { - // load the chart to install - chart, err := loader.Load(chstr) - if err != nil { - return nil, err - } - - return c.InstallReleaseFromChart(chart, ns, opts...) -} - -// InstallReleaseFromChart installs a new chart and returns the release response. -func (c *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*release.Release, error) { - // apply the install options - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.instReq - req.Chart = chart - req.Namespace = ns - req.DryRun = reqOpts.dryRun - req.DisableHooks = reqOpts.disableHooks - req.ReuseName = reqOpts.reuseName - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - if err := chartutil.ProcessDependencies(req.Chart, req.Values); err != nil { - return nil, err - } - return c.tiller.InstallRelease(req) -} - -// UninstallRelease uninstalls a named release and returns the response. -func (c *Client) UninstallRelease(rlsName string, opts ...UninstallOption) (*hapi.UninstallReleaseResponse, error) { - // apply the uninstall options - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - - if reqOpts.dryRun { - // In the dry run case, just see if the release exists - r, err := c.ReleaseContent(rlsName, 0) - if err != nil { - return &hapi.UninstallReleaseResponse{}, err - } - return &hapi.UninstallReleaseResponse{Release: r}, nil - } - - req := &reqOpts.uninstallReq - req.Name = rlsName - req.DisableHooks = reqOpts.disableHooks - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - return c.tiller.UninstallRelease(req) -} - -// UpdateRelease loads a chart from chstr and updates a release to a new/different chart. -func (c *Client) UpdateRelease(rlsName, chstr string, opts ...UpdateOption) (*release.Release, error) { - // load the chart to update - chart, err := loader.Load(chstr) - if err != nil { - return nil, err - } - - return c.UpdateReleaseFromChart(rlsName, chart, opts...) -} - -// UpdateReleaseFromChart updates a release to a new/different chart. -func (c *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*release.Release, error) { - // apply the update options - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.updateReq - req.Chart = chart - req.DryRun = reqOpts.dryRun - req.Name = rlsName - req.DisableHooks = reqOpts.disableHooks - req.Recreate = reqOpts.recreate - req.Force = reqOpts.force - req.ResetValues = reqOpts.resetValues - req.ReuseValues = reqOpts.reuseValues - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - if err := chartutil.ProcessDependencies(req.Chart, req.Values); err != nil { - return nil, err - } - return c.tiller.UpdateRelease(req) -} - -// RollbackRelease rolls back a release to the previous version. -func (c *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*release.Release, error) { - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.rollbackReq - req.Recreate = reqOpts.recreate - req.Force = reqOpts.force - req.DisableHooks = reqOpts.disableHooks - req.DryRun = reqOpts.dryRun - req.Name = rlsName - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - return c.tiller.RollbackRelease(req) -} - -// ReleaseContent returns the configuration for a given release. -func (c *Client) ReleaseContent(name string, version int) (*release.Release, error) { - reqOpts := c.opts - req := &reqOpts.contentReq - req.Name = name - req.Version = version - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - return c.tiller.GetReleaseContent(req) -} - -// ReleaseHistory returns a release's revision history. -func (c *Client) ReleaseHistory(rlsName string, max int) ([]*release.Release, error) { - reqOpts := c.opts - req := &reqOpts.histReq - req.Name = rlsName - req.Max = max - - if err := reqOpts.runBefore(req); err != nil { - return nil, err - } - return c.tiller.GetHistory(req) -} - -// RunReleaseTest executes a pre-defined test on a release. -func (c *Client) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *hapi.TestReleaseResponse, <-chan error) { - reqOpts := c.opts - for _, opt := range opts { - opt(&reqOpts) - } - - req := &reqOpts.testReq - req.Name = rlsName - - return c.tiller.RunReleaseTest(req) -} diff --git a/pkg/helm/fake.go b/pkg/helm/fake.go deleted file mode 100644 index 31e1db9ce..000000000 --- a/pkg/helm/fake.go +++ /dev/null @@ -1,244 +0,0 @@ -/* -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 helm // import "k8s.io/helm/pkg/helm" - -import ( - "math/rand" - "sync" - "time" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -// FakeClient implements Interface -type FakeClient struct { - Rels []*release.Release - TestRunStatus map[string]release.TestRunStatus - Opts options -} - -// Option returns the fake release client -func (c *FakeClient) Option(opts ...Option) Interface { - for _, opt := range opts { - opt(&c.Opts) - } - return c -} - -var _ Interface = &FakeClient{} -var _ Interface = (*FakeClient)(nil) - -// InstallRelease creates a new release and returns the release -func (c *FakeClient) InstallRelease(chStr, ns string, opts ...InstallOption) (*release.Release, error) { - chart := &chart.Chart{} - return c.InstallReleaseFromChart(chart, ns, opts...) -} - -// InstallReleaseFromChart adds a new MockRelease to the fake client and -// returns the release -func (c *FakeClient) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*release.Release, error) { - for _, opt := range opts { - opt(&c.Opts) - } - - releaseName := c.Opts.instReq.Name - - // Check to see if the release already exists. - rel, err := c.ReleaseContent(releaseName, 0) - if err == nil && rel != nil { - return nil, errors.New("cannot re-use a name that is still in use") - } - - release := ReleaseMock(&MockReleaseOptions{Name: releaseName, Namespace: ns}) - c.Rels = append(c.Rels, release) - return release, nil -} - -// UninstallRelease uninstalls a release from the FakeClient -func (c *FakeClient) UninstallRelease(rlsName string, opts ...UninstallOption) (*hapi.UninstallReleaseResponse, error) { - for i, rel := range c.Rels { - if rel.Name == rlsName { - c.Rels = append(c.Rels[:i], c.Rels[i+1:]...) - return &hapi.UninstallReleaseResponse{ - Release: rel, - }, nil - } - } - - return nil, errors.Errorf("no such release: %s", rlsName) -} - -// UpdateRelease returns the updated release, if it exists -func (c *FakeClient) UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*release.Release, error) { - return c.UpdateReleaseFromChart(rlsName, &chart.Chart{}, opts...) -} - -// UpdateReleaseFromChart returns the updated release, if it exists -func (c *FakeClient) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*release.Release, error) { - // Check to see if the release already exists. - return c.ReleaseContent(rlsName, 0) -} - -// RollbackRelease returns nil, nil -func (c *FakeClient) RollbackRelease(rlsName string, opts ...RollbackOption) (*release.Release, error) { - return nil, nil -} - -// ReleaseStatus returns a release status response with info from the matching release name. -func (c *FakeClient) ReleaseStatus(rlsName string, version int) (*hapi.GetReleaseStatusResponse, error) { - for _, rel := range c.Rels { - if rel.Name == rlsName { - return &hapi.GetReleaseStatusResponse{ - Name: rel.Name, - Info: rel.Info, - Namespace: rel.Namespace, - }, nil - } - } - return nil, errors.Errorf("no such release: %s", rlsName) -} - -// ReleaseContent returns the configuration for the matching release name in the fake release client. -func (c *FakeClient) ReleaseContent(rlsName string, version int) (*release.Release, error) { - for _, rel := range c.Rels { - if rel.Name == rlsName { - return rel, nil - } - } - return nil, errors.Errorf("no such release: %s", rlsName) -} - -// ReleaseHistory returns a release's revision history. -func (c *FakeClient) ReleaseHistory(rlsName string, max int) ([]*release.Release, error) { - return c.Rels, nil -} - -// RunReleaseTest executes a pre-defined tests on a release -func (c *FakeClient) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *hapi.TestReleaseResponse, <-chan error) { - - results := make(chan *hapi.TestReleaseResponse) - errc := make(chan error, 1) - - go func() { - var wg sync.WaitGroup - for m, s := range c.TestRunStatus { - wg.Add(1) - - go func(msg string, status release.TestRunStatus) { - defer wg.Done() - results <- &hapi.TestReleaseResponse{Msg: msg, Status: status} - }(m, s) - } - - wg.Wait() - close(results) - close(errc) - }() - - return results, errc -} - -// MockHookTemplate is the hook template used for all mock release objects. -var MockHookTemplate = `apiVersion: v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-install -` - -// MockManifest is the manifest used for all mock release objects. -var MockManifest = `apiVersion: v1 -kind: Secret -metadata: - name: fixture -` - -// MockReleaseOptions allows for user-configurable options on mock release objects. -type MockReleaseOptions struct { - Name string - Version int - Chart *chart.Chart - Status release.Status - Namespace string -} - -// ReleaseMock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. -func ReleaseMock(opts *MockReleaseOptions) *release.Release { - date := time.Unix(242085845, 0).UTC() - - name := opts.Name - if name == "" { - name = "testrelease-" + string(rand.Intn(100)) - } - - version := 1 - if opts.Version != 0 { - version = opts.Version - } - - namespace := opts.Namespace - if namespace == "" { - namespace = "default" - } - - ch := opts.Chart - if opts.Chart == nil { - ch = &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "foo", - Version: "0.1.0-beta.1", - }, - Templates: []*chart.File{ - {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, - }, - } - } - - scode := release.StatusDeployed - if len(opts.Status) > 0 { - scode = opts.Status - } - - return &release.Release{ - Name: name, - Info: &release.Info{ - FirstDeployed: date, - LastDeployed: date, - Status: scode, - Description: "Release mock", - }, - Chart: ch, - Config: map[string]interface{}{"name": "value"}, - Version: version, - Namespace: namespace, - Hooks: []*release.Hook{ - { - Name: "pre-install-hook", - Kind: "Job", - Path: "pre-install-hook.yaml", - Manifest: MockHookTemplate, - LastRun: date, - Events: []release.HookEvent{release.HookPreInstall}, - }, - }, - Manifest: MockManifest, - } -} diff --git a/pkg/helm/fake_test.go b/pkg/helm/fake_test.go deleted file mode 100644 index 230ae01c2..000000000 --- a/pkg/helm/fake_test.go +++ /dev/null @@ -1,190 +0,0 @@ -/* -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 helm - -import ( - "reflect" - "testing" - - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestFakeClient_InstallReleaseFromChart(t *testing.T) { - installChart := &chart.Chart{} - type fields struct { - Rels []*release.Release - } - type args struct { - ns string - opts []InstallOption - } - tests := []struct { - name string - fields fields - args args - want *release.Release - relsAfter []*release.Release - wantErr bool - }{ - { - name: "Add release to an empty list.", - fields: fields{ - Rels: []*release.Release{}, - }, - args: args{ - ns: "default", - opts: []InstallOption{ReleaseName("new-release")}, - }, - want: ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - wantErr: false, - }, - { - name: "Try to add a release where the name already exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - }, - args: args{ - ns: "default", - opts: []InstallOption{ReleaseName("new-release")}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &FakeClient{ - Rels: tt.fields.Rels, - } - got, err := c.InstallReleaseFromChart(installChart, tt.args.ns, tt.args.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("FakeClient.InstallReleaseFromChart() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("FakeClient.InstallReleaseFromChart() = %v, want %v", got, tt.want) - } - if !reflect.DeepEqual(c.Rels, tt.relsAfter) { - t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", got, tt.relsAfter) - } - }) - } -} - -func TestFakeClient_UninstallRelease(t *testing.T) { - type fields struct { - Rels []*release.Release - } - type args struct { - rlsName string - opts []UninstallOption - } - tests := []struct { - name string - fields fields - args args - want *hapi.UninstallReleaseResponse - relsAfter []*release.Release - wantErr bool - }{ - { - name: "Uninstall a release that exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "trepid-tapir", - opts: []UninstallOption{}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - }, - want: &hapi.UninstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - wantErr: false, - }, - { - name: "Uninstall a release that does not exist.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "release-that-does-not-exists", - opts: []UninstallOption{}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - want: nil, - wantErr: true, - }, - { - name: "Uninstall when only 1 item exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "trepid-tapir", - opts: []UninstallOption{}, - }, - relsAfter: []*release.Release{}, - want: &hapi.UninstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &FakeClient{ - Rels: tt.fields.Rels, - } - got, err := c.UninstallRelease(tt.args.rlsName, tt.args.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("FakeClient.UninstallRelease() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("FakeClient.UninstallRelease() = %v, want %v", got, tt.want) - } - - if !reflect.DeepEqual(c.Rels, tt.relsAfter) { - t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", got, tt.relsAfter) - } - }) - } -} diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go deleted file mode 100644 index 63fc76674..000000000 --- a/pkg/helm/helm_test.go +++ /dev/null @@ -1,262 +0,0 @@ -/* -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 helm // import "k8s.io/helm/pkg/helm" - -import ( - "path/filepath" - "reflect" - "testing" - - "github.com/pkg/errors" - - cpb "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" - "k8s.io/helm/pkg/hapi" -) - -// Path to example charts relative to pkg/helm. -const chartsDir = "../../docs/examples/" - -// Sentinel error to indicate to the Helm client to not send the request to Tiller. -var errSkip = errors.New("test: skip") - -// Verify each ReleaseListOption is applied to a ListReleasesRequest correctly. - -// Verify each InstallOption is applied to an InstallReleaseRequest correctly. -func TestInstallRelease_VerifyOptions(t *testing.T) { - // Options testdata - var disableHooks = true - var releaseName = "test" - var reuseName = true - var dryRun = true - var chartName = "alpine" - var chartPath = filepath.Join(chartsDir, chartName) - - // Expected InstallReleaseRequest message - exp := &hapi.InstallReleaseRequest{ - Chart: loadChart(t, chartName), - DryRun: dryRun, - Name: releaseName, - DisableHooks: disableHooks, - ReuseName: reuseName, - } - - // Options used in InstallRelease - ops := []InstallOption{ - InstallDryRun(dryRun), - ReleaseName(releaseName), - InstallReuseName(reuseName), - InstallDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client InstallReleaseRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.InstallReleaseRequest: - t.Logf("InstallReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type InstallReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.InstallRelease(chartPath, "", ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.instReq.Name) -} - -// Verify each UninstallOptions is applied to an UninstallReleaseRequest correctly. -func TestUninstallRelease_VerifyOptions(t *testing.T) { - // Options testdata - var releaseName = "test" - var disableHooks = true - var purgeFlag = true - - // Expected UninstallReleaseRequest message - exp := &hapi.UninstallReleaseRequest{ - Name: releaseName, - Purge: purgeFlag, - DisableHooks: disableHooks, - } - - // Options used in UninstallRelease - ops := []UninstallOption{ - UninstallPurge(purgeFlag), - UninstallDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client UninstallReleaseRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.UninstallReleaseRequest: - t.Logf("UninstallReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type UninstallReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.UninstallRelease(releaseName, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.uninstallReq.Name) -} - -// Verify each UpdateOption is applied to an UpdateReleaseRequest correctly. -func TestUpdateRelease_VerifyOptions(t *testing.T) { - // Options testdata - var chartName = "alpine" - var chartPath = filepath.Join(chartsDir, chartName) - var releaseName = "test" - var disableHooks = true - var dryRun = false - - // Expected UpdateReleaseRequest message - exp := &hapi.UpdateReleaseRequest{ - Name: releaseName, - Chart: loadChart(t, chartName), - DryRun: dryRun, - DisableHooks: disableHooks, - } - - // Options used in UpdateRelease - ops := []UpdateOption{ - UpgradeDryRun(dryRun), - UpgradeDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client UpdateReleaseRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.UpdateReleaseRequest: - t.Logf("UpdateReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type UpdateReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.UpdateRelease(releaseName, chartPath, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.updateReq.Name) -} - -// Verify each RollbackOption is applied to a RollbackReleaseRequest correctly. -func TestRollbackRelease_VerifyOptions(t *testing.T) { - // Options testdata - var disableHooks = true - var releaseName = "test" - var revision = 2 - var dryRun = true - - // Expected RollbackReleaseRequest message - exp := &hapi.RollbackReleaseRequest{ - Name: releaseName, - DryRun: dryRun, - Version: revision, - DisableHooks: disableHooks, - } - - // Options used in RollbackRelease - ops := []RollbackOption{ - RollbackDryRun(dryRun), - RollbackVersion(revision), - RollbackDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client RollbackReleaseRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.RollbackReleaseRequest: - t.Logf("RollbackReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type RollbackReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.RollbackRelease(releaseName, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.rollbackReq.Name) -} - -// Verify each ContentOption is applied to a GetReleaseContentRequest correctly. -func TestReleaseContent_VerifyOptions(t *testing.T) { - t.Skip("refactoring out") - // Options testdata - var releaseName = "test" - var revision = 2 - - // Expected GetReleaseContentRequest message - exp := &hapi.GetReleaseContentRequest{ - Name: releaseName, - Version: revision, - } - - // BeforeCall option to intercept Helm client GetReleaseContentRequest - b4c := BeforeCall(func(msg interface{}) error { - switch act := msg.(type) { - case *hapi.GetReleaseContentRequest: - t.Logf("GetReleaseContentRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type GetReleaseContentRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.ReleaseContent(releaseName, revision); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.contentReq.Name) -} - -func assert(t *testing.T, expect, actual interface{}) { - if !reflect.DeepEqual(expect, actual) { - t.Fatalf("expected %#+v, actual %#+v\n", expect, actual) - } -} - -func loadChart(t *testing.T, name string) *cpb.Chart { - c, err := loader.Load(filepath.Join(chartsDir, name)) - if err != nil { - t.Fatalf("failed to load test chart (%q): %s\n", name, err) - } - return c -} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go deleted file mode 100644 index 8fa8d9f2c..000000000 --- a/pkg/helm/interface.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 helm - -import ( - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -// Interface for helm client for mocking in tests -type Interface interface { - InstallRelease(chStr, namespace string, opts ...InstallOption) (*release.Release, error) - InstallReleaseFromChart(chart *chart.Chart, namespace string, opts ...InstallOption) (*release.Release, error) - UninstallRelease(rlsName string, opts ...UninstallOption) (*hapi.UninstallReleaseResponse, error) - UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*release.Release, error) - UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*release.Release, error) - RollbackRelease(rlsName string, opts ...RollbackOption) (*release.Release, error) - ReleaseContent(rlsName string, version int) (*release.Release, error) - ReleaseHistory(rlsName string, max int) ([]*release.Release, error) - RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *hapi.TestReleaseResponse, <-chan error) -} diff --git a/pkg/helm/option.go b/pkg/helm/option.go deleted file mode 100644 index 5115a61c9..000000000 --- a/pkg/helm/option.go +++ /dev/null @@ -1,338 +0,0 @@ -/* -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 helm - -import ( - "k8s.io/client-go/discovery" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller/environment" -) - -// Option allows specifying various settings configurable by -// the helm client user for overriding the defaults. -type Option func(*options) - -// options specify optional settings used by the helm client. -type options struct { - // if set dry-run helm client calls - dryRun bool - // if set, re-use an existing name - reuseName bool - // if set, performs pod restart during upgrade/rollback - recreate bool - // if set, force resource update through uninstall/recreate if needed - force bool - // if set, skip running hooks - disableHooks bool - // release install options are applied directly to the install release request - instReq hapi.InstallReleaseRequest - // release update options are applied directly to the update release request - updateReq hapi.UpdateReleaseRequest - // release uninstall options are applied directly to the uninstall release request - uninstallReq hapi.UninstallReleaseRequest - // release get content options are applied directly to the get release content request - contentReq hapi.GetReleaseContentRequest - // release rollback options are applied directly to the rollback release request - rollbackReq hapi.RollbackReleaseRequest - // before intercepts client calls before sending - before func(interface{}) error - // release history options are applied directly to the get release history request - histReq hapi.GetHistoryRequest - // resetValues instructs Helm to reset values to their defaults. - resetValues bool - // reuseValues instructs Helm to reuse the values from the last release. - reuseValues bool - // release test options are applied directly to the test release history request - testReq hapi.TestReleaseRequest - - driver driver.Driver - kubeClient environment.KubeClient - discovery discovery.DiscoveryInterface -} - -func (opts *options) runBefore(msg interface{}) error { - if opts.before != nil { - return opts.before(msg) - } - return nil -} - -// BeforeCall returns an option that allows intercepting a helm client rpc -// before being sent OTA to tiller. The intercepting function should return -// an error to indicate that the call should not proceed or nil otherwise. -func BeforeCall(fn func(interface{}) error) Option { - return func(opts *options) { - opts.before = fn - } -} - -// InstallOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm install` command. -type InstallOption func(*options) - -// ValueOverrides specifies a list of values to include when installing. -func ValueOverrides(raw map[string]interface{}) InstallOption { - return func(opts *options) { - opts.instReq.Values = raw - } -} - -// ReleaseName specifies the name of the release when installing. -func ReleaseName(name string) InstallOption { - return func(opts *options) { - opts.instReq.Name = name - } -} - -// InstallTimeout specifies the number of seconds before kubernetes calls timeout -func InstallTimeout(timeout int64) InstallOption { - return func(opts *options) { - opts.instReq.Timeout = timeout - } -} - -// UpgradeTimeout specifies the number of seconds before kubernetes calls timeout -func UpgradeTimeout(timeout int64) UpdateOption { - return func(opts *options) { - opts.updateReq.Timeout = timeout - } -} - -// UninstallTimeout specifies the number of seconds before kubernetes calls timeout -func UninstallTimeout(timeout int64) UninstallOption { - return func(opts *options) { - opts.uninstallReq.Timeout = timeout - } -} - -// ReleaseTestTimeout specifies the number of seconds before kubernetes calls timeout -func ReleaseTestTimeout(timeout int64) ReleaseTestOption { - return func(opts *options) { - opts.testReq.Timeout = timeout - } -} - -// ReleaseTestCleanup is a boolean value representing whether to cleanup test pods -func ReleaseTestCleanup(cleanup bool) ReleaseTestOption { - return func(opts *options) { - opts.testReq.Cleanup = cleanup - } -} - -// RollbackTimeout specifies the number of seconds before kubernetes calls timeout -func RollbackTimeout(timeout int64) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Timeout = timeout - } -} - -// InstallWait specifies whether or not to wait for all resources to be ready -func InstallWait(wait bool) InstallOption { - return func(opts *options) { - opts.instReq.Wait = wait - } -} - -// UpgradeWait specifies whether or not to wait for all resources to be ready -func UpgradeWait(wait bool) UpdateOption { - return func(opts *options) { - opts.updateReq.Wait = wait - } -} - -// RollbackWait specifies whether or not to wait for all resources to be ready -func RollbackWait(wait bool) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Wait = wait - } -} - -// UpdateValueOverrides specifies a list of values to include when upgrading -func UpdateValueOverrides(raw map[string]interface{}) UpdateOption { - return func(opts *options) { - opts.updateReq.Values = raw - } -} - -// UninstallDisableHooks will disable hooks for a deletion operation. -func UninstallDisableHooks(disable bool) UninstallOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// UninstallDryRun will (if true) execute a deletion as a dry run. -func UninstallDryRun(dry bool) UninstallOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// UninstallPurge removes the release from the store and make its name free for later use. -func UninstallPurge(purge bool) UninstallOption { - return func(opts *options) { - opts.uninstallReq.Purge = purge - } -} - -// InstallDryRun will (if true) execute an installation as a dry run. -func InstallDryRun(dry bool) InstallOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// InstallDisableHooks disables hooks during installation. -func InstallDisableHooks(disable bool) InstallOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// InstallReuseName will (if true) instruct Helm to re-use an existing name. -func InstallReuseName(reuse bool) InstallOption { - return func(opts *options) { - opts.reuseName = reuse - } -} - -// RollbackDisableHooks will disable hooks for a rollback operation -func RollbackDisableHooks(disable bool) RollbackOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// RollbackDryRun will (if true) execute a rollback as a dry run. -func RollbackDryRun(dry bool) RollbackOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// RollbackRecreate will (if true) recreate pods after rollback. -func RollbackRecreate(recreate bool) RollbackOption { - return func(opts *options) { - opts.recreate = recreate - } -} - -// RollbackForce will (if true) force resource update through uninstall/recreate if needed -func RollbackForce(force bool) RollbackOption { - return func(opts *options) { - opts.force = force - } -} - -// RollbackVersion sets the version of the release to deploy. -func RollbackVersion(ver int) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Version = ver - } -} - -// UpgradeDisableHooks will disable hooks for an upgrade operation. -func UpgradeDisableHooks(disable bool) UpdateOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// UpgradeDryRun will (if true) execute an upgrade as a dry run. -func UpgradeDryRun(dry bool) UpdateOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// ResetValues will (if true) trigger resetting the values to their original state. -func ResetValues(reset bool) UpdateOption { - return func(opts *options) { - opts.resetValues = reset - } -} - -// ReuseValues will cause Helm to reuse the values from the last release. -// This is ignored if ResetValues is true. -func ReuseValues(reuse bool) UpdateOption { - return func(opts *options) { - opts.reuseValues = reuse - } -} - -// UpgradeRecreate will (if true) recreate pods after upgrade. -func UpgradeRecreate(recreate bool) UpdateOption { - return func(opts *options) { - opts.recreate = recreate - } -} - -// UpgradeForce will (if true) force resource update through uninstall/recreate if needed -func UpgradeForce(force bool) UpdateOption { - return func(opts *options) { - opts.force = force - } -} - -// MaxHistory limits the maximum number of revisions saved per release -func MaxHistory(maxHistory int) UpdateOption { - return func(opts *options) { - opts.updateReq.MaxHistory = maxHistory - } -} - -// UninstallOption allows setting optional attributes when -// performing a UninstallRelease tiller rpc. -type UninstallOption func(*options) - -// UpdateOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm upgrade` command. -type UpdateOption func(*options) - -// RollbackOption allows specififying various settings configurable -// by the helm client user for overriding the defaults used when -// running the `helm rollback` command. -type RollbackOption func(*options) - -// ReleaseTestOption allows configuring optional request data for -// issuing a TestRelease rpc. -type ReleaseTestOption func(*options) - -// Driver set the driver option -func Driver(d driver.Driver) Option { - return func(opts *options) { - opts.driver = d - } -} - -// KubeClient sets the cluster environment -func KubeClient(kc environment.KubeClient) Option { - return func(opts *options) { - opts.kubeClient = kc - } -} - -// Discovery sets the discovery interface -func Discovery(dc discovery.DiscoveryInterface) Option { - return func(opts *options) { - opts.discovery = dc - } -} diff --git a/pkg/helm/helmpath/helmhome.go b/pkg/helmpath/helmhome.go similarity index 100% rename from pkg/helm/helmpath/helmhome.go rename to pkg/helmpath/helmhome.go diff --git a/pkg/helm/helmpath/helmhome_unix_test.go b/pkg/helmpath/helmhome_unix_test.go similarity index 100% rename from pkg/helm/helmpath/helmhome_unix_test.go rename to pkg/helmpath/helmhome_unix_test.go diff --git a/pkg/helm/helmpath/helmhome_windows_test.go b/pkg/helmpath/helmhome_windows_test.go similarity index 100% rename from pkg/helm/helmpath/helmhome_windows_test.go rename to pkg/helmpath/helmhome_windows_test.go diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go index fbcbb9076..011cdc960 100644 --- a/pkg/hooks/hooks.go +++ b/pkg/hooks/hooks.go @@ -17,7 +17,7 @@ limitations under the License. package hooks import ( - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) // HookAnno is the label name for a hook diff --git a/pkg/tiller/environment/environment.go b/pkg/kube/environment.go similarity index 86% rename from pkg/tiller/environment/environment.go rename to pkg/kube/environment.go index b68af00c8..13fd90228 100644 --- a/pkg/tiller/environment/environment.go +++ b/pkg/kube/environment.go @@ -14,13 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package environment describes the operating environment for Tiller. - -Tiller's environment encapsulates all of the service dependencies Tiller has. -These dependencies are expressed as interfaces so that alternate implementations -(mocks, etc.) can be easily generated. -*/ -package environment +package kube import ( "io" @@ -28,14 +22,12 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions/resource" - - "k8s.io/helm/pkg/kube" ) -// KubeClient represents a client capable of communicating with the Kubernetes API. +// KubernetesClient represents a client capable of communicating with the Kubernetes API. // -// A KubeClient must be concurrency safe. -type KubeClient interface { +// A KubernetesClient must be concurrency safe. +type KubernetesClient interface { // Create creates one or more resources. // // namespace must contain a valid existing namespace. @@ -77,8 +69,8 @@ type KubeClient interface { // by "\n---\n"). Update(namespace string, originalReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error - Build(namespace string, reader io.Reader) (kube.Result, error) - BuildUnstructured(namespace string, reader io.Reader) (kube.Result, error) + Build(namespace string, reader io.Reader) (Result, error) + BuildUnstructured(namespace string, reader io.Reader) (Result, error) // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase // and returns said phase (PodSucceeded or PodFailed qualify). @@ -124,12 +116,12 @@ func (p *PrintingKubeClient) Update(ns string, currentReader, modifiedReader io. } // Build implements KubeClient Build. -func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { +func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (Result, error) { return []*resource.Info{}, nil } // BuildUnstructured implements KubeClient BuildUnstructured. -func (p *PrintingKubeClient) BuildUnstructured(ns string, reader io.Reader) (kube.Result, error) { +func (p *PrintingKubeClient) BuildUnstructured(ns string, reader io.Reader) (Result, error) { return []*resource.Info{}, nil } diff --git a/pkg/tiller/environment/environment_test.go b/pkg/kube/environment_test.go similarity index 89% rename from pkg/tiller/environment/environment_test.go rename to pkg/kube/environment_test.go index 33a53d98b..5cdc365bf 100644 --- a/pkg/tiller/environment/environment_test.go +++ b/pkg/kube/environment_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package environment +package kube import ( "bytes" @@ -24,8 +24,6 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions/resource" - - "k8s.io/helm/pkg/kube" ) type mockKubeClient struct{} @@ -45,10 +43,10 @@ func (k *mockKubeClient) Update(ns string, currentReader, modifiedReader io.Read func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { return nil } -func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { +func (k *mockKubeClient) Build(ns string, reader io.Reader) (Result, error) { return []*resource.Info{}, nil } -func (k *mockKubeClient) BuildUnstructured(ns string, reader io.Reader) (kube.Result, error) { +func (k *mockKubeClient) BuildUnstructured(ns string, reader io.Reader) (Result, error) { return []*resource.Info{}, nil } func (k *mockKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (v1.PodPhase, error) { @@ -59,8 +57,8 @@ func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader i return "", nil } -var _ KubeClient = &mockKubeClient{} -var _ KubeClient = &PrintingKubeClient{} +var _ KubernetesClient = &mockKubeClient{} +var _ KubernetesClient = &PrintingKubeClient{} func TestKubeClient(t *testing.T) { kc := &mockKubeClient{} diff --git a/pkg/plugin/installer/base.go b/pkg/plugin/installer/base.go index 15ce3cbcf..61f3dfb63 100644 --- a/pkg/plugin/installer/base.go +++ b/pkg/plugin/installer/base.go @@ -19,7 +19,7 @@ import ( "os" "path/filepath" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) type base struct { diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index b4103d60a..c1348a63e 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -27,9 +27,9 @@ import ( "github.com/pkg/errors" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin/cache" ) @@ -79,7 +79,7 @@ func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) return nil, err } - getConstructor, err := getter.ByScheme("http", environment.EnvSettings{}) + getConstructor, err := getter.ByScheme("http", cli.EnvSettings{}) if err != nil { return nil, err } diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index 4eea99e9f..dee9c3a4d 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) var _ Installer = new(HTTPInstaller) diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 48cea1ac8..b94629072 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) // ErrMissingMetadata indicates that plugin.yaml is missing. diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index 82f0f7e2f..056a311ce 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -20,7 +20,7 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) // LocalInstaller installs plugins from the filesystem. diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index fb5fa2675..288162079 100644 --- a/pkg/plugin/installer/local_installer_test.go +++ b/pkg/plugin/installer/local_installer_test.go @@ -21,7 +21,7 @@ import ( "path/filepath" "testing" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) var _ Installer = new(LocalInstaller) diff --git a/pkg/plugin/installer/vcs_installer.go b/pkg/plugin/installer/vcs_installer.go index 211f46481..428ed4263 100644 --- a/pkg/plugin/installer/vcs_installer.go +++ b/pkg/plugin/installer/vcs_installer.go @@ -23,7 +23,7 @@ import ( "github.com/Masterminds/vcs" "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/plugin/cache" ) diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index f114a8a23..33b36060c 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -24,7 +24,7 @@ import ( "github.com/Masterminds/vcs" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" ) var _ Installer = new(VCSInstaller) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 60efcb573..4f91ba26c 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -25,7 +25,7 @@ import ( "github.com/ghodss/yaml" - helm_env "k8s.io/helm/pkg/helm/environment" + helm_env "k8s.io/helm/pkg/cli" ) const pluginFileName = "plugin.yaml" diff --git a/pkg/hapi/release/hook.go b/pkg/release/hook.go similarity index 100% rename from pkg/hapi/release/hook.go rename to pkg/release/hook.go diff --git a/pkg/hapi/release/info.go b/pkg/release/info.go similarity index 100% rename from pkg/hapi/release/info.go rename to pkg/release/info.go diff --git a/pkg/release/mock.go b/pkg/release/mock.go new file mode 100644 index 000000000..0b5e8df35 --- /dev/null +++ b/pkg/release/mock.go @@ -0,0 +1,122 @@ +/* +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 release + +import ( + "math/rand" + "time" + + "k8s.io/helm/pkg/chart" +) + +// MockHookTemplate is the hook template used for all mock release objects. +var MockHookTemplate = `apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-install +` + +// MockManifest is the manifest used for all mock release objects. +var MockManifest = `apiVersion: v1 +kind: Secret +metadata: + name: fixture +` + +// MockReleaseOptions allows for user-configurable options on mock release objects. +type MockReleaseOptions struct { + Name string + Version int + Chart *chart.Chart + Status Status + Namespace string + TestSuiteResults []*TestRun +} + +// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. +func Mock(opts *MockReleaseOptions) *Release { + date := time.Unix(242085845, 0).UTC() + + name := opts.Name + if name == "" { + name = "testrelease-" + string(rand.Intn(100)) + } + + version := 1 + if opts.Version != 0 { + version = opts.Version + } + + namespace := opts.Namespace + if namespace == "" { + namespace = "default" + } + + ch := opts.Chart + if opts.Chart == nil { + ch = &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + }, + Templates: []*chart.File{ + {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, + }, + } + } + + scode := StatusDeployed + if len(opts.Status) > 0 { + scode = opts.Status + } + + info := &Info{ + FirstDeployed: date, + LastDeployed: date, + Status: scode, + Description: "Release mock", + } + + if len(opts.TestSuiteResults) > 0 { + info.LastTestSuiteRun = &TestSuite{ + StartedAt: date, + CompletedAt: date, + Results: opts.TestSuiteResults, + } + } + + return &Release{ + Name: name, + Info: info, + Chart: ch, + Config: map[string]interface{}{"name": "value"}, + Version: version, + Namespace: namespace, + Hooks: []*Hook{ + { + Name: "pre-install-hook", + Kind: "Job", + Path: "pre-install-hook.yaml", + Manifest: MockHookTemplate, + LastRun: date, + Events: []HookEvent{HookPreInstall}, + }, + }, + Manifest: MockManifest, + } +} diff --git a/pkg/hapi/release/release.go b/pkg/release/release.go similarity index 97% rename from pkg/hapi/release/release.go rename to pkg/release/release.go index ed660d2ea..4f006bef9 100644 --- a/pkg/hapi/release/release.go +++ b/pkg/release/release.go @@ -25,7 +25,7 @@ type Release struct { // Info provides information about a release Info *Info `json:"info,omitempty"` // Chart is the chart that was released. - Chart *chart.Chart `json:"chart,omitempty"` + Chart *chart.Chart `json:"-"` // Config is the set of extra Values added to the chart. // These values override the default values inside of the chart. Config map[string]interface{} `json:"config,omitempty"` diff --git a/pkg/release/responses.go b/pkg/release/responses.go new file mode 100644 index 000000000..6eb9cbb5a --- /dev/null +++ b/pkg/release/responses.go @@ -0,0 +1,40 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +// GetReleaseStatusResponse is the response indicating the status of the named release. +type GetReleaseStatusResponse struct { + // Name is the name of the release. + Name string `json:"name,omitempty"` + // Info contains information about the release. + Info *Info `json:"info,omitempty"` + // Namespace the release was released into + Namespace string `json:"namespace,omitempty"` +} + +// UninstallReleaseResponse represents a successful response to an uninstall request. +type UninstallReleaseResponse struct { + // Release is the release that was marked deleted. + Release *Release `json:"release,omitempty"` + // Info is an uninstall message + Info string `json:"info,omitempty"` +} + +// TestReleaseResponse represents a message from executing a test +type TestReleaseResponse struct { + Msg string `json:"msg,omitempty"` + Status TestRunStatus `json:"status,omitempty"` +} diff --git a/pkg/hapi/release/status.go b/pkg/release/status.go similarity index 100% rename from pkg/hapi/release/status.go rename to pkg/release/status.go diff --git a/pkg/hapi/release/test_run.go b/pkg/release/test_run.go similarity index 100% rename from pkg/hapi/release/test_run.go rename to pkg/release/test_run.go diff --git a/pkg/hapi/release/test_suite.go b/pkg/release/test_suite.go similarity index 100% rename from pkg/hapi/release/test_suite.go rename to pkg/release/test_suite.go diff --git a/pkg/releasetesting/environment.go b/pkg/releasetesting/environment.go index 95336ac6b..2da6105a1 100644 --- a/pkg/releasetesting/environment.go +++ b/pkg/releasetesting/environment.go @@ -24,16 +24,15 @@ import ( v1 "k8s.io/api/core/v1" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/tiller/environment" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" ) // Environment encapsulates information about where test suite executes and returns results type Environment struct { Namespace string - KubeClient environment.KubeClient - Mesages chan *hapi.TestReleaseResponse + KubeClient kube.KubernetesClient + Messages chan *release.TestReleaseResponse Timeout int64 } @@ -106,8 +105,8 @@ func (env *Environment) streamUnknown(name, info string) error { } func (env *Environment) streamMessage(msg string, status release.TestRunStatus) error { - resp := &hapi.TestReleaseResponse{Msg: msg, Status: status} - env.Mesages <- resp + resp := &release.TestReleaseResponse{Msg: msg, Status: status} + env.Messages <- resp return nil } diff --git a/pkg/releasetesting/environment_test.go b/pkg/releasetesting/environment_test.go index 08e385c00..14b9ba5ec 100644 --- a/pkg/releasetesting/environment_test.go +++ b/pkg/releasetesting/environment_test.go @@ -21,7 +21,7 @@ import ( "github.com/pkg/errors" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) func TestCreateTestPodSuccess(t *testing.T) { @@ -53,7 +53,7 @@ func TestCreateTestPodFailure(t *testing.T) { func TestStreamMessage(t *testing.T) { env := testEnvFixture() - defer close(env.Mesages) + defer close(env.Messages) expectedMessage := "testing streamMessage" expectedStatus := release.TestRunSuccess @@ -61,7 +61,7 @@ func TestStreamMessage(t *testing.T) { t.Errorf("Expected no errors, got: %s", err) } - got := <-env.Mesages + got := <-env.Messages if got.Msg != expectedMessage { t.Errorf("Expected message: %s, got: %s", expectedMessage, got.Msg) } diff --git a/pkg/releasetesting/test_suite.go b/pkg/releasetesting/test_suite.go index 3470624db..96f4b1fb0 100644 --- a/pkg/releasetesting/test_suite.go +++ b/pkg/releasetesting/test_suite.go @@ -24,8 +24,8 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" - "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/release" util "k8s.io/helm/pkg/releaseutil" ) diff --git a/pkg/releasetesting/test_suite_test.go b/pkg/releasetesting/test_suite_test.go index 3e8e8423e..47e119591 100644 --- a/pkg/releasetesting/test_suite_test.go +++ b/pkg/releasetesting/test_suite_test.go @@ -23,9 +23,8 @@ import ( v1 "k8s.io/api/core/v1" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/tiller/environment" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/release" ) const manifestWithTestSuccessHook = ` @@ -71,16 +70,16 @@ func TestRun(t *testing.T) { env := testEnvFixture() go func() { - defer close(env.Mesages) + defer close(env.Messages) if err := ts.Run(env); err != nil { t.Error(err) } }() for i := 0; i <= 4; i++ { - <-env.Mesages + <-env.Messages } - if _, ok := <-env.Mesages; ok { + if _, ok := <-env.Messages; ok { t.Errorf("Expected 4 messages streamed") } @@ -127,18 +126,18 @@ func TestRunEmptyTestSuite(t *testing.T) { env := testEnvFixture() go func() { - defer close(env.Mesages) + defer close(env.Messages) if err := ts.Run(env); err != nil { t.Error(err) } }() - msg := <-env.Mesages + msg := <-env.Messages if msg.Msg != "No Tests Found" { t.Errorf("Expected message 'No Tests Found', Got: %v", msg.Msg) } - for range env.Mesages { + for range env.Messages { } if ts.StartedAt.IsZero() { @@ -158,16 +157,16 @@ func TestRunSuccessWithTestFailureHook(t *testing.T) { env.KubeClient = &mockKubeClient{podFail: true} go func() { - defer close(env.Mesages) + defer close(env.Messages) if err := ts.Run(env); err != nil { t.Error(err) } }() for i := 0; i <= 4; i++ { - <-env.Mesages + <-env.Messages } - if _, ok := <-env.Mesages; ok { + if _, ok := <-env.Messages; ok { t.Errorf("Expected 4 messages streamed") } @@ -240,12 +239,12 @@ func testEnvFixture() *Environment { Namespace: "default", KubeClient: &mockKubeClient{}, Timeout: 1, - Mesages: make(chan *hapi.TestReleaseResponse, 1), + Messages: make(chan *release.TestReleaseResponse, 1), } } type mockKubeClient struct { - environment.KubeClient + kube.KubernetesClient podFail bool err error } diff --git a/pkg/releaseutil/filter.go b/pkg/releaseutil/filter.go index 40ca3a027..93ddbc46e 100644 --- a/pkg/releaseutil/filter.go +++ b/pkg/releaseutil/filter.go @@ -16,7 +16,7 @@ limitations under the License. package releaseutil // import "k8s.io/helm/pkg/releaseutil" -import rspb "k8s.io/helm/pkg/hapi/release" +import rspb "k8s.io/helm/pkg/release" // FilterFunc returns true if the release object satisfies // the predicate of the underlying filter func. diff --git a/pkg/releaseutil/filter_test.go b/pkg/releaseutil/filter_test.go index c0ce85b90..0a1f8a024 100644 --- a/pkg/releaseutil/filter_test.go +++ b/pkg/releaseutil/filter_test.go @@ -19,7 +19,7 @@ package releaseutil // import "k8s.io/helm/pkg/releaseutil" import ( "testing" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func TestFilterAny(t *testing.T) { diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index 06ba0d2bd..dd27e484c 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -22,12 +22,12 @@ import ( "strconv" "strings" + "github.com/ghodss/yaml" "github.com/pkg/errors" - yaml "gopkg.in/yaml.v2" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/release" ) // Manifest represents a manifest file, which has a name and some content. diff --git a/pkg/releaseutil/manifest_sorter_test.go b/pkg/releaseutil/manifest_sorter_test.go index 91b98f83f..c8e0a1f43 100644 --- a/pkg/releaseutil/manifest_sorter_test.go +++ b/pkg/releaseutil/manifest_sorter_test.go @@ -23,7 +23,7 @@ import ( "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" + "k8s.io/helm/pkg/release" ) func TestSortManifests(t *testing.T) { diff --git a/pkg/releaseutil/sorter.go b/pkg/releaseutil/sorter.go index 0f27fabb0..ed93ccbee 100644 --- a/pkg/releaseutil/sorter.go +++ b/pkg/releaseutil/sorter.go @@ -19,7 +19,7 @@ package releaseutil // import "k8s.io/helm/pkg/releaseutil" import ( "sort" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) type list []*rspb.Release diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go index 3cfcf77d4..1da83c5ac 100644 --- a/pkg/releaseutil/sorter_test.go +++ b/pkg/releaseutil/sorter_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) // note: this test data is shared with filter_test.go. diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index 53ae0a4eb..afbc31fd1 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -28,8 +28,8 @@ import ( "time" "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" ) const ( @@ -41,7 +41,7 @@ func TestLoadChartRepository(t *testing.T) { r, err := NewChartRepository(&Entry{ Name: testRepository, URL: testURL, - }, getter.All(environment.EnvSettings{})) + }, getter.All(cli.EnvSettings{})) if err != nil { t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) } @@ -74,7 +74,7 @@ func TestIndex(t *testing.T) { r, err := NewChartRepository(&Entry{ Name: testRepository, URL: testURL, - }, getter.All(environment.EnvSettings{})) + }, getter.All(cli.EnvSettings{})) if err != nil { t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) } @@ -221,7 +221,7 @@ func TestFindChartInRepoURL(t *testing.T) { } defer srv.Close() - chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(environment.EnvSettings{})) + chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(cli.EnvSettings{})) if err != nil { t.Errorf("%s", err) } @@ -229,7 +229,7 @@ func TestFindChartInRepoURL(t *testing.T) { t.Errorf("%s is not the valid URL", chartURL) } - chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(environment.EnvSettings{})) + chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(cli.EnvSettings{})) if err != nil { t.Errorf("%s", err) } @@ -239,7 +239,7 @@ func TestFindChartInRepoURL(t *testing.T) { } func TestErrorFindChartInRepoURL(t *testing.T) { - _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", getter.All(environment.EnvSettings{})) + _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", getter.All(cli.EnvSettings{})) if err == nil { t.Errorf("Expected error for bad chart URL, but did not get any errors") } @@ -253,7 +253,7 @@ func TestErrorFindChartInRepoURL(t *testing.T) { } defer srv.Close() - _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", getter.All(environment.EnvSettings{})) + _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", getter.All(cli.EnvSettings{})) if err == nil { t.Errorf("Expected error for chart not found, but did not get any errors") } @@ -261,7 +261,7 @@ func TestErrorFindChartInRepoURL(t *testing.T) { t.Errorf("Expected error for chart not found, but got a different error (%v)", err) } - _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", getter.All(environment.EnvSettings{})) + _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", getter.All(cli.EnvSettings{})) if err == nil { t.Errorf("Expected error for chart not found, but did not get any errors") } @@ -269,7 +269,7 @@ func TestErrorFindChartInRepoURL(t *testing.T) { t.Errorf("Expected error for chart not found, but got a different error (%v)", err) } - _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", getter.All(environment.EnvSettings{})) + _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", getter.All(cli.EnvSettings{})) if err == nil { t.Errorf("Expected error for no chart URLs available, but did not get any errors") } diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 6c577f469..b49314f53 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -211,8 +211,6 @@ func (i *IndexFile) Merge(f *IndexFile) { } } -// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 - // ChartVersion represents a chart entry in the IndexFile type ChartVersion struct { *chart.Metadata diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 1ad7ebcc6..81fb973b6 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -23,8 +23,8 @@ import ( "testing" "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/cli" "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" ) const ( @@ -146,7 +146,7 @@ func TestDownloadIndexFile(t *testing.T) { Name: testRepo, URL: srv.URL, Cache: indexFilePath, - }, getter.All(environment.EnvSettings{})) + }, getter.All(cli.EnvSettings{})) if err != nil { t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) } diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index a59a2fa3a..790b9732b 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -24,7 +24,7 @@ import ( "github.com/ghodss/yaml" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/repo" ) diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go index d658f5f6f..74cdab624 100644 --- a/pkg/resolver/resolver.go +++ b/pkg/resolver/resolver.go @@ -27,7 +27,7 @@ import ( "github.com/pkg/errors" "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/helmpath" "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" ) diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 19a30eb3e..c2efc3100 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -29,7 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) var _ Driver = (*ConfigMaps)(nil) diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 75938dcdc..c9239d645 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -21,7 +21,7 @@ import ( v1 "k8s.io/api/core/v1" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func TestConfigMapName(t *testing.T) { diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 1bd8d2e6d..efd78bc91 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -19,7 +19,7 @@ package driver // import "k8s.io/helm/pkg/storage/driver" import ( "github.com/pkg/errors" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) var ( diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index df89bfed3..13ecf86f2 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -21,7 +21,7 @@ import ( "strings" "sync" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) var _ Driver = (*Memory)(nil) diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 9bab0a74c..884af3b9f 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func TestMemoryName(t *testing.T) { diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index bd0779f99..42dcb4c15 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -25,7 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go index 5bb497e7d..e0368c9b1 100644 --- a/pkg/storage/driver/records.go +++ b/pkg/storage/driver/records.go @@ -20,7 +20,7 @@ import ( "sort" "strconv" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) // records holds a list of in-memory release records diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 141a5c590..e87286f34 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -19,7 +19,7 @@ package driver // import "k8s.io/helm/pkg/storage/driver" import ( "testing" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func TestRecordsAdd(t *testing.T) { diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 88f213a92..b42bc9a0a 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -29,7 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) var _ Driver = (*Secrets)(nil) diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index f32d4a394..8d9fc6b00 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -21,7 +21,7 @@ import ( v1 "k8s.io/api/core/v1" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) func TestSecretName(t *testing.T) { diff --git a/pkg/storage/driver/util.go b/pkg/storage/driver/util.go index a4aba5d9c..f0a231d13 100644 --- a/pkg/storage/driver/util.go +++ b/pkg/storage/driver/util.go @@ -23,7 +23,7 @@ import ( "encoding/json" "io/ioutil" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" ) var b64 = base64.StdEncoding diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 0b1cf9279..f1f06843c 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -22,7 +22,7 @@ import ( "github.com/pkg/errors" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" relutil "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/storage/driver" ) diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 30d3cd41c..f345b67ca 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - rspb "k8s.io/helm/pkg/hapi/release" + rspb "k8s.io/helm/pkg/release" "k8s.io/helm/pkg/storage/driver" ) diff --git a/pkg/tiller/engine.go b/pkg/tiller/engine.go deleted file mode 100644 index d1485fe50..000000000 --- a/pkg/tiller/engine.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -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 tiller - -import ( - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chartutil" -) - -// Engine represents a template engine that can render templates. -// -// For some engines, "rendering" includes both compiling and executing. (Other -// engines do not distinguish between phases.) -// -// The engine returns a map where the key is the named output entity (usually -// a file name) and the value is the rendered content of the template. -// -// An Engine must be capable of executing multiple concurrent requests, but -// without tainting one request's environment with data from another request. -type Engine interface { - // Render renders a chart. - // - // It receives a chart, a config, and a map of overrides to the config. - // Overrides are assumed to be passed from the system, not the user. - Render(*chart.Chart, chartutil.Values) (map[string]string, error) -} diff --git a/pkg/tiller/hook_sorter_test.go b/pkg/tiller/hook_sorter_test.go deleted file mode 100644 index c46c44b57..000000000 --- a/pkg/tiller/hook_sorter_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -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 tiller - -import ( - "sort" - "testing" - - "k8s.io/helm/pkg/hapi/release" -) - -func TestHookSorter(t *testing.T) { - hooks := []*release.Hook{ - { - Name: "g", - Kind: "pre-install", - Weight: 99, - }, - { - Name: "f", - Kind: "pre-install", - Weight: 3, - }, - { - Name: "b", - Kind: "pre-install", - Weight: -3, - }, - { - Name: "e", - Kind: "pre-install", - Weight: 3, - }, - { - Name: "a", - Kind: "pre-install", - Weight: -10, - }, - { - Name: "c", - Kind: "pre-install", - Weight: 0, - }, - { - Name: "d", - Kind: "pre-install", - Weight: 3, - }, - } - - sort.Sort(hookByWeight(hooks)) - got := "" - expect := "abcdefg" - for _, r := range hooks { - got += r.Name - } - if got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go deleted file mode 100644 index 93cd8a715..000000000 --- a/pkg/tiller/hooks.go +++ /dev/null @@ -1,222 +0,0 @@ -/* -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 tiller - -import ( - "log" - "path" - "strconv" - "strings" - - "github.com/ghodss/yaml" - "github.com/pkg/errors" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" - util "k8s.io/helm/pkg/releaseutil" -) - -var events = map[string]release.HookEvent{ - hooks.PreInstall: release.HookPreInstall, - hooks.PostInstall: release.HookPostInstall, - hooks.PreDelete: release.HookPreDelete, - hooks.PostDelete: release.HookPostDelete, - hooks.PreUpgrade: release.HookPreUpgrade, - hooks.PostUpgrade: release.HookPostUpgrade, - hooks.PreRollback: release.HookPreRollback, - hooks.PostRollback: release.HookPostRollback, - hooks.ReleaseTestSuccess: release.HookReleaseTestSuccess, - hooks.ReleaseTestFailure: release.HookReleaseTestFailure, -} - -// Manifest represents a manifest file, which has a name and some content. -type Manifest struct { - Name string - Content string - Head *util.SimpleHead -} - -type result struct { - hooks []*release.Hook - generic []Manifest -} - -type manifestFile struct { - entries map[string]string - path string - apis chartutil.VersionSet -} - -// SortManifests takes a map of filename/YAML contents, splits the file -// by manifest entries, and sorts the entries into hook types. -// -// The resulting hooks struct will be populated with all of the generated hooks. -// Any file that does not declare one of the hook types will be placed in the -// 'generic' bucket. -// -// Files that do not parse into the expected format are simply placed into a map and -// returned. -func SortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) { - result := &result{} - - for filePath, c := range files { - - // Skip partials. We could return these as a separate map, but there doesn't - // seem to be any need for that at this time. - if strings.HasPrefix(path.Base(filePath), "_") { - continue - } - // Skip empty files and log this. - if len(strings.TrimSpace(c)) == 0 { - log.Printf("info: manifest %q is empty. Skipping.", filePath) - continue - } - - manifestFile := &manifestFile{ - entries: util.SplitManifests(c), - path: filePath, - apis: apis, - } - - if err := manifestFile.sort(result); err != nil { - return result.hooks, result.generic, err - } - } - - return result.hooks, sortByKind(result.generic, sort), nil -} - -// sort takes a manifestFile object which may contain multiple resource definition -// entries and sorts each entry by hook types, and saves the resulting hooks and -// generic manifests (or non-hooks) to the result struct. -// -// To determine hook type, it looks for a YAML structure like this: -// -// kind: SomeKind -// apiVersion: v1 -// metadata: -// annotations: -// helm.sh/hook: pre-install -// -// To determine the policy to delete the hook, it looks for a YAML structure like this: -// -// kind: SomeKind -// apiVersion: v1 -// metadata: -// annotations: -// helm.sh/hook-delete-policy: hook-succeeded -func (file *manifestFile) sort(result *result) error { - for _, m := range file.entries { - var entry util.SimpleHead - if err := yaml.Unmarshal([]byte(m), &entry); err != nil { - return errors.Wrapf(err, "YAML parse error on %s", file.path) - } - - if entry.Version != "" && !file.apis.Has(entry.Version) { - return errors.Errorf("apiVersion %q in %s is not available", entry.Version, file.path) - } - - if !hasAnyAnnotation(entry) { - result.generic = append(result.generic, Manifest{ - Name: file.path, - Content: m, - Head: &entry, - }) - continue - } - - hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] - if !ok { - result.generic = append(result.generic, Manifest{ - Name: file.path, - Content: m, - Head: &entry, - }) - continue - } - - hw := calculateHookWeight(entry) - - h := &release.Hook{ - Name: entry.Metadata.Name, - Kind: entry.Kind, - Path: file.path, - Manifest: m, - Events: []release.HookEvent{}, - Weight: hw, - DeletePolicies: []release.HookDeletePolicy{}, - } - - isUnknownHook := false - for _, hookType := range strings.Split(hookTypes, ",") { - hookType = strings.ToLower(strings.TrimSpace(hookType)) - e, ok := events[hookType] - if !ok { - isUnknownHook = true - break - } - h.Events = append(h.Events, e) - } - - if isUnknownHook { - log.Printf("info: skipping unknown hook: %q", hookTypes) - continue - } - - result.hooks = append(result.hooks, h) - - operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) { - policy, exist := deletePolices[value] - if exist { - h.DeletePolicies = append(h.DeletePolicies, policy) - } else { - log.Printf("info: skipping unknown hook delete policy: %q", value) - } - }) - } - - return nil -} - -func hasAnyAnnotation(entry util.SimpleHead) bool { - if entry.Metadata == nil || - entry.Metadata.Annotations == nil || - len(entry.Metadata.Annotations) == 0 { - return false - } - - return true -} - -func calculateHookWeight(entry util.SimpleHead) int { - hws := entry.Metadata.Annotations[hooks.HookWeightAnno] - hw, err := strconv.Atoi(hws) - if err != nil { - hw = 0 - } - return hw -} - -func operateAnnotationValues(entry util.SimpleHead, annotation string, operate func(p string)) { - if dps, ok := entry.Metadata.Annotations[annotation]; ok { - for _, dp := range strings.Split(dps, ",") { - dp = strings.ToLower(strings.TrimSpace(dp)) - operate(dp) - } - } -} diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go deleted file mode 100644 index abd2adf63..000000000 --- a/pkg/tiller/hooks_test.go +++ /dev/null @@ -1,246 +0,0 @@ -/* -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 tiller - -import ( - "reflect" - "testing" - - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi/release" - util "k8s.io/helm/pkg/releaseutil" -) - -func TestSortManifests(t *testing.T) { - - data := []struct { - name []string - path string - kind []string - hooks map[string][]release.HookEvent - manifest string - }{ - { - name: []string{"first"}, - path: "one", - kind: []string{"Job"}, - hooks: map[string][]release.HookEvent{"first": {release.HookPreInstall}}, - manifest: `apiVersion: v1 -kind: Job -metadata: - name: first - labels: - doesnot: matter - annotations: - "helm.sh/hook": pre-install -`, - }, - { - name: []string{"second"}, - path: "two", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.HookEvent{"second": {release.HookPostInstall}}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: second - annotations: - "helm.sh/hook": post-install -`, - }, { - name: []string{"third"}, - path: "three", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.HookEvent{"third": nil}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: third - annotations: - "helm.sh/hook": no-such-hook -`, - }, { - name: []string{"fourth"}, - path: "four", - kind: []string{"Pod"}, - hooks: map[string][]release.HookEvent{"fourth": nil}, - manifest: `kind: Pod -apiVersion: v1 -metadata: - name: fourth - annotations: - nothing: here`, - }, { - name: []string{"fifth"}, - path: "five", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.HookEvent{"fifth": {release.HookPostDelete, release.HookPostInstall}}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: fifth - annotations: - "helm.sh/hook": post-delete, post-install -`, - }, { - // Regression test: files with an underscore in the base name should be skipped. - name: []string{"sixth"}, - path: "six/_six", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.HookEvent{"sixth": nil}, - manifest: `invalid manifest`, // This will fail if partial is not skipped. - }, { - // Regression test: files with no content should be skipped. - name: []string{"seventh"}, - path: "seven", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.HookEvent{"seventh": nil}, - manifest: "", - }, - { - name: []string{"eighth", "example-test"}, - path: "eight", - kind: []string{"ConfigMap", "Pod"}, - hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookReleaseTestSuccess}}, - manifest: `kind: ConfigMap -apiVersion: v1 -metadata: - name: eighth -data: - name: value ---- -apiVersion: v1 -kind: Pod -metadata: - name: example-test - annotations: - "helm.sh/hook": test-success -`, - }, - } - - manifests := make(map[string]string, len(data)) - for _, o := range data { - manifests[o.path] = o.manifest - } - - hs, generic, err := SortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - - // This test will fail if 'six' or 'seven' was added. - if len(generic) != 2 { - t.Errorf("Expected 2 generic manifests, got %d", len(generic)) - } - - if len(hs) != 4 { - t.Errorf("Expected 4 hooks, got %d", len(hs)) - } - - for _, out := range hs { - found := false - for _, expect := range data { - if out.Path == expect.path { - found = true - if out.Path != expect.path { - t.Errorf("Expected path %s, got %s", expect.path, out.Path) - } - nameFound := false - for _, expectedName := range expect.name { - if out.Name == expectedName { - nameFound = true - } - } - if !nameFound { - t.Errorf("Got unexpected name %s", out.Name) - } - kindFound := false - for _, expectedKind := range expect.kind { - if out.Kind == expectedKind { - kindFound = true - } - } - if !kindFound { - t.Errorf("Got unexpected kind %s", out.Kind) - } - - expectedHooks := expect.hooks[out.Name] - if !reflect.DeepEqual(expectedHooks, out.Events) { - t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) - } - - } - } - if !found { - t.Errorf("Result not found: %v", out) - } - } - - // Verify the sort order - sorted := []Manifest{} - for _, s := range data { - manifests := util.SplitManifests(s.manifest) - - for _, m := range manifests { - var sh util.SimpleHead - err := yaml.Unmarshal([]byte(m), &sh) - if err != nil { - // This is expected for manifests that are corrupt or empty. - t.Log(err) - continue - } - - name := sh.Metadata.Name - - //only keep track of non-hook manifests - if err == nil && s.hooks[name] == nil { - another := Manifest{ - Content: m, - Name: name, - Head: &sh, - } - sorted = append(sorted, another) - } - } - } - - sorted = sortByKind(sorted, InstallOrder) - for i, m := range generic { - if m.Content != sorted[i].Content { - t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) - } - } -} - -func TestVersionSet(t *testing.T) { - vs := chartutil.NewVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1") - - if l := len(vs); l != 4 { - t.Errorf("Expected 4, got %d", l) - } - - if !vs.Has("extensions/alpha5") { - t.Error("No match for alpha5") - } - - if vs.Has("nosuch/extension") { - t.Error("Found nonexistent extension") - } -} diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go deleted file mode 100644 index d0e4d0912..000000000 --- a/pkg/tiller/kind_sorter.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -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 tiller - -import ( - "sort" -) - -// SortOrder is an ordering of Kinds. -type SortOrder []string - -// InstallOrder is the order in which manifests should be installed (by Kind). -// -// Those occurring earlier in the list get installed before those occurring later in the list. -var InstallOrder SortOrder = []string{ - "Namespace", - "ResourceQuota", - "LimitRange", - "Secret", - "ConfigMap", - "StorageClass", - "PersistentVolume", - "PersistentVolumeClaim", - "ServiceAccount", - "CustomResourceDefinition", - "ClusterRole", - "ClusterRoleBinding", - "Role", - "RoleBinding", - "Service", - "DaemonSet", - "Pod", - "ReplicationController", - "ReplicaSet", - "Deployment", - "StatefulSet", - "Job", - "CronJob", - "Ingress", - "APIService", -} - -// UninstallOrder is the order in which manifests should be uninstalled (by Kind). -// -// Those occurring earlier in the list get uninstalled before those occurring later in the list. -var UninstallOrder SortOrder = []string{ - "APIService", - "Ingress", - "Service", - "CronJob", - "Job", - "StatefulSet", - "Deployment", - "ReplicaSet", - "ReplicationController", - "Pod", - "DaemonSet", - "RoleBinding", - "Role", - "ClusterRoleBinding", - "ClusterRole", - "CustomResourceDefinition", - "ServiceAccount", - "PersistentVolumeClaim", - "PersistentVolume", - "StorageClass", - "ConfigMap", - "Secret", - "LimitRange", - "ResourceQuota", - "Namespace", -} - -// sortByKind does an in-place sort of manifests by Kind. -// -// Results are sorted by 'ordering' -func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { - ks := newKindSorter(manifests, ordering) - sort.Sort(ks) - return ks.manifests -} - -type kindSorter struct { - ordering map[string]int - manifests []Manifest -} - -func newKindSorter(m []Manifest, s SortOrder) *kindSorter { - o := make(map[string]int, len(s)) - for v, k := range s { - o[k] = v - } - - return &kindSorter{ - manifests: m, - ordering: o, - } -} - -func (k *kindSorter) Len() int { return len(k.manifests) } - -func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } - -func (k *kindSorter) Less(i, j int) bool { - a := k.manifests[i] - b := k.manifests[j] - first, aok := k.ordering[a.Head.Kind] - second, bok := k.ordering[b.Head.Kind] - // if same kind (including unknown) sub sort alphanumeric - if first == second { - // if both are unknown and of different kind sort by kind alphabetically - if !aok && !bok && a.Head.Kind != b.Head.Kind { - return a.Head.Kind < b.Head.Kind - } - return a.Name < b.Name - } - // unknown kind is last - if !aok { - return false - } - if !bok { - return true - } - // sort different kinds - return first < second -} - -// SortByKind sorts manifests in InstallOrder -func SortByKind(manifests []Manifest) []Manifest { - ordering := InstallOrder - ks := newKindSorter(manifests, ordering) - sort.Sort(ks) - return ks.manifests -} diff --git a/pkg/tiller/kind_sorter_test.go b/pkg/tiller/kind_sorter_test.go deleted file mode 100644 index ef7d3c1b7..000000000 --- a/pkg/tiller/kind_sorter_test.go +++ /dev/null @@ -1,217 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "testing" - - util "k8s.io/helm/pkg/releaseutil" -) - -func TestKindSorter(t *testing.T) { - manifests := []Manifest{ - { - Name: "i", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "j", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "e", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "u", - Head: &util.SimpleHead{Kind: "CronJob"}, - }, - { - Name: "2", - Head: &util.SimpleHead{Kind: "CustomResourceDefinition"}, - }, - { - Name: "n", - Head: &util.SimpleHead{Kind: "DaemonSet"}, - }, - { - Name: "r", - Head: &util.SimpleHead{Kind: "Deployment"}, - }, - { - Name: "!", - Head: &util.SimpleHead{Kind: "HonkyTonkSet"}, - }, - { - Name: "v", - Head: &util.SimpleHead{Kind: "Ingress"}, - }, - { - Name: "t", - Head: &util.SimpleHead{Kind: "Job"}, - }, - { - Name: "c", - Head: &util.SimpleHead{Kind: "LimitRange"}, - }, - { - Name: "a", - Head: &util.SimpleHead{Kind: "Namespace"}, - }, - { - Name: "f", - Head: &util.SimpleHead{Kind: "PersistentVolume"}, - }, - { - Name: "g", - Head: &util.SimpleHead{Kind: "PersistentVolumeClaim"}, - }, - { - Name: "o", - Head: &util.SimpleHead{Kind: "Pod"}, - }, - { - Name: "q", - Head: &util.SimpleHead{Kind: "ReplicaSet"}, - }, - { - Name: "p", - Head: &util.SimpleHead{Kind: "ReplicationController"}, - }, - { - Name: "b", - Head: &util.SimpleHead{Kind: "ResourceQuota"}, - }, - { - Name: "k", - Head: &util.SimpleHead{Kind: "Role"}, - }, - { - Name: "l", - Head: &util.SimpleHead{Kind: "RoleBinding"}, - }, - { - Name: "d", - Head: &util.SimpleHead{Kind: "Secret"}, - }, - { - Name: "m", - Head: &util.SimpleHead{Kind: "Service"}, - }, - { - Name: "h", - Head: &util.SimpleHead{Kind: "ServiceAccount"}, - }, - { - Name: "s", - Head: &util.SimpleHead{Kind: "StatefulSet"}, - }, - { - Name: "1", - Head: &util.SimpleHead{Kind: "StorageClass"}, - }, - { - Name: "w", - Head: &util.SimpleHead{Kind: "APIService"}, - }, - } - - for _, test := range []struct { - description string - order SortOrder - expected string - }{ - {"install", InstallOrder, "abcde1fgh2ijklmnopqrstuvw!"}, - {"uninstall", UninstallOrder, "wvmutsrqponlkji2hgf1edcba!"}, - } { - var buf bytes.Buffer - t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(manifests); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } - defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { - buf.WriteString(r.Name) - } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } - }) - } -} - -// TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric -func TestKindSorterSubSort(t *testing.T) { - manifests := []Manifest{ - { - Name: "a", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "A", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "0", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "1", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "z", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "!", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "u2", - Head: &util.SimpleHead{Kind: "Unknown"}, - }, - { - Name: "u1", - Head: &util.SimpleHead{Kind: "Unknown"}, - }, - { - Name: "t3", - Head: &util.SimpleHead{Kind: "Unknown2"}, - }, - } - for _, test := range []struct { - description string - order SortOrder - expected string - }{ - // expectation is sorted by kind (unknown is last) and then sub sorted alphabetically within each group - {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01Aa!zu1u2t3"}, - } { - var buf bytes.Buffer - t.Run(test.description, func(t *testing.T) { - defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { - buf.WriteString(r.Name) - } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } - }) - } -} diff --git a/pkg/tiller/release_history.go b/pkg/tiller/release_history.go deleted file mode 100644 index 1b69d9f0e..000000000 --- a/pkg/tiller/release_history.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -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 tiller - -import ( - "github.com/pkg/errors" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - relutil "k8s.io/helm/pkg/releaseutil" -) - -// GetHistory gets the history for a given release. -func (s *ReleaseServer) GetHistory(req *hapi.GetHistoryRequest) ([]*release.Release, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, errors.Errorf("getHistory: Release name is invalid: %s", req.Name) - } - - s.Log("getting history for release %s", req.Name) - h, err := s.Releases.History(req.Name) - if err != nil { - return nil, err - } - - relutil.Reverse(h, relutil.SortByRevision) - - var rels []*release.Release - for i := 0; i < min(len(h), req.Max); i++ { - rels = append(rels, h[i]) - } - - return rels, nil -} - -func min(x, y int) int { - if x < y { - return x - } - return y -} diff --git a/pkg/tiller/release_history_test.go b/pkg/tiller/release_history_test.go deleted file mode 100644 index 11aa72374..000000000 --- a/pkg/tiller/release_history_test.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -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 tiller - -import ( - "reflect" - "testing" - - "k8s.io/helm/pkg/hapi" - rpb "k8s.io/helm/pkg/hapi/release" -) - -func TestGetHistory_WithRevisions(t *testing.T) { - mk := func(name string, vers int, status rpb.Status) *rpb.Release { - return &rpb.Release{ - Name: name, - Version: vers, - Info: &rpb.Info{Status: status}, - } - } - - // GetReleaseHistoryTests - tests := []struct { - desc string - req *hapi.GetHistoryRequest - res []*rpb.Release - }{ - { - desc: "get release with history and default limit (max=256)", - req: &hapi.GetHistoryRequest{Name: "angry-bird", Max: 256}, - res: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), - mk("angry-bird", 2, rpb.StatusSuperseded), - mk("angry-bird", 1, rpb.StatusSuperseded), - }, - }, - { - desc: "get release with history using result limit (max=2)", - req: &hapi.GetHistoryRequest{Name: "angry-bird", Max: 2}, - res: []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), - }, - }, - } - - // test release history for release 'angry-bird' - hist := []*rpb.Release{ - mk("angry-bird", 4, rpb.StatusDeployed), - mk("angry-bird", 3, rpb.StatusSuperseded), - mk("angry-bird", 2, rpb.StatusSuperseded), - mk("angry-bird", 1, rpb.StatusSuperseded), - } - - srv := rsFixture(t) - for _, rls := range hist { - if err := srv.Releases.Create(rls); err != nil { - t.Fatalf("Failed to create release: %s", err) - } - } - - // run tests - for _, tt := range tests { - res, err := srv.GetHistory(tt.req) - if err != nil { - t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) - } - if !reflect.DeepEqual(res, tt.res) { - t.Fatalf("%s:\nExpected:\n\t%+v\nActual\n\t%+v", tt.desc, tt.res, res) - } - } -} - -func TestGetHistory_WithNoRevisions(t *testing.T) { - tests := []struct { - desc string - req *hapi.GetHistoryRequest - }{ - { - desc: "get release with no history", - req: &hapi.GetHistoryRequest{Name: "sad-panda", Max: 256}, - }, - } - - // create release 'sad-panda' with no revision history - rls := namedReleaseStub("sad-panda", rpb.StatusDeployed) - srv := rsFixture(t) - srv.Releases.Create(rls) - - for _, tt := range tests { - res, err := srv.GetHistory(tt.req) - if err != nil { - t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) - } - if len(res) > 1 { - t.Fatalf("%s:\nExpected zero items, got %d", tt.desc, len(res)) - } - } -} diff --git a/pkg/tiller/release_install.go b/pkg/tiller/release_install.go deleted file mode 100644 index 8cfe8a074..000000000 --- a/pkg/tiller/release_install.go +++ /dev/null @@ -1,225 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "fmt" - "strings" - "time" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" - relutil "k8s.io/helm/pkg/releaseutil" -) - -// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning -// FIXME: Can we refactor this out? -var deletePolices = map[string]release.HookDeletePolicy{ - hooks.HookSucceeded: release.HookSucceeded, - hooks.HookFailed: release.HookFailed, - hooks.BeforeHookCreation: release.HookBeforeHookCreation, -} - -// InstallRelease installs a release and stores the release record. -func (s *ReleaseServer) InstallRelease(req *hapi.InstallReleaseRequest) (*release.Release, error) { - s.Log("preparing install for %s", req.Name) - rel, err := s.prepareRelease(req) - if err != nil { - // On dry run, append the manifest contents to a failed release. This is - // a stop-gap until we can revisit an error backchannel post-2.0. - if req.DryRun && strings.HasPrefix(err.Error(), "YAML parse error") { - err = errors.Wrap(err, rel.Manifest) - } - return rel, errors.Wrap(err, "failed install prepare step") - } - - s.Log("performing install for %s", req.Name) - res, err := s.performRelease(rel, req) - return res, errors.Wrap(err, "failed install perform step") -} - -// prepareRelease builds a release for an install operation. -func (s *ReleaseServer) prepareRelease(req *hapi.InstallReleaseRequest) (*release.Release, error) { - if req.Chart == nil { - return nil, errMissingChart - } - - name, err := s.uniqName(req.Name, req.ReuseName) - if err != nil { - return nil, err - } - - caps, err := newCapabilities(s.discovery) - if err != nil { - return nil, err - } - - revision := 1 - options := chartutil.ReleaseOptions{ - Name: name, - IsInstall: true, - } - valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options, caps) - if err != nil { - return nil, err - } - - ts := time.Now() - hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions) - if err != nil { - // Return a release with partial data so that client can show debugging - // information. - rel := &release.Release{ - Name: name, - Namespace: req.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: release.StatusUnknown, - Description: fmt.Sprintf("Install failed: %s", err), - }, - Version: 0, - } - if manifestDoc != nil { - rel.Manifest = manifestDoc.String() - } - return rel, err - } - - // Store a release. - rel := &release.Release{ - Name: name, - Namespace: req.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: release.StatusPendingInstall, - Description: "Initial install underway", // Will be overwritten. - }, - Manifest: manifestDoc.String(), - Hooks: hooks, - Version: revision, - } - if len(notesTxt) > 0 { - rel.Info.Notes = notesTxt - } - - err = validateManifest(s.KubeClient, req.Namespace, manifestDoc.Bytes()) - return rel, err -} - -// performRelease runs a release. -func (s *ReleaseServer) performRelease(r *release.Release, req *hapi.InstallReleaseRequest) (*release.Release, error) { - - if req.DryRun { - s.Log("dry run for %s", r.Name) - r.Info.Description = "Dry run complete" - return r, nil - } - - // pre-install hooks - if !req.DisableHooks { - if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PreInstall, req.Timeout); err != nil { - return r, err - } - } else { - s.Log("install hooks disabled for %s", req.Name) - } - - switch h, err := s.Releases.History(req.Name); { - // if this is a replace operation, append to the release history - case req.ReuseName && err == nil && len(h) >= 1: - s.Log("name reuse for %s requested, replacing release", req.Name) - // get latest release revision - relutil.Reverse(h, relutil.SortByRevision) - - // old release - old := h[0] - - // update old release status - old.Info.Status = release.StatusSuperseded - s.recordRelease(old, true) - - // update new release with next revision number - // so as to append to the old release's history - r.Version = old.Version + 1 - updateReq := &hapi.UpdateReleaseRequest{ - Wait: req.Wait, - Recreate: false, - Timeout: req.Timeout, - } - s.recordRelease(r, false) - if err := s.updateRelease(old, r, updateReq); err != nil { - msg := fmt.Sprintf("Release replace %q failed: %s", r.Name, err) - s.Log("warning: %s", msg) - old.Info.Status = release.StatusSuperseded - r.Info.Status = release.StatusFailed - r.Info.Description = msg - s.recordRelease(old, true) - s.recordRelease(r, true) - return r, err - } - - default: - // nothing to replace, create as normal - // regular manifests - s.recordRelease(r, false) - b := bytes.NewBufferString(r.Manifest) - if err := s.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait); err != nil { - msg := fmt.Sprintf("Release %q failed: %s", r.Name, err) - s.Log("warning: %s", msg) - r.Info.Status = release.StatusFailed - r.Info.Description = msg - s.recordRelease(r, true) - return r, errors.Wrapf(err, "release %s failed", r.Name) - } - } - - // post-install hooks - if !req.DisableHooks { - if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PostInstall, req.Timeout); err != nil { - msg := fmt.Sprintf("Release %q failed post-install: %s", r.Name, err) - s.Log("warning: %s", msg) - r.Info.Status = release.StatusFailed - r.Info.Description = msg - s.recordRelease(r, true) - return r, err - } - } - - r.Info.Status = release.StatusDeployed - r.Info.Description = "Install complete" - // This is a tricky case. The release has been created, but the result - // cannot be recorded. The truest thing to tell the user is that the - // release was created. However, the user will not be able to do anything - // further with this release. - // - // One possible strategy would be to do a timed retry to see if we can get - // this stored in the future. - s.recordRelease(r, true) - - return r, nil -} diff --git a/pkg/tiller/release_install_test.go b/pkg/tiller/release_install_test.go deleted file mode 100644 index 47503f93d..000000000 --- a/pkg/tiller/release_install_test.go +++ /dev/null @@ -1,390 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - "testing" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestInstallRelease(t *testing.T) { - rs := rsFixture(t) - - req := installRequest(withName("test-install-release")) - res, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Name != "test-install-release" { - t.Errorf("Expected release name.") - } - if res.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Namespace) - } - - rel, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - t.Logf("rel: %v", rel) - - if len(rel.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) - } - if rel.Hooks[0].Manifest != manifestWithHook { - t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) - } - - if rel.Hooks[0].Events[0] != release.HookPostInstall { - t.Errorf("Expected event 0 is post install") - } - if rel.Hooks[0].Events[1] != release.HookPreDelete { - t.Errorf("Expected event 0 is pre-delete") - } - - if len(res.Manifest) == 0 { - t.Errorf("No manifest returned: %v", res) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_NoName(t *testing.T) { - rs := rsFixture(t) - - // No name supplied here, should cause failure. - req := installRequest() - _, err := rs.InstallRelease(req) - if err == nil { - t.Fatal("expected failure when no name is specified") - } - if !strings.Contains(err.Error(), "name is required") { - t.Errorf("Expected message %q to include 'name is required'", err.Error()) - } -} - -func TestInstallRelease_WithNotes(t *testing.T) { - rs := rsFixture(t) - - req := installRequest( - withChart(withNotes(notesText)), - withName("with-notes"), - ) - res, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Name == "" { - t.Errorf("Expected release name.") - } - if res.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Namespace) - } - - rel, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - t.Logf("rel: %v", rel) - - if len(rel.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) - } - if rel.Hooks[0].Manifest != manifestWithHook { - t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) - } - - if rel.Info.Notes != notesText { - t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Notes) - } - - if rel.Hooks[0].Events[0] != release.HookPostInstall { - t.Errorf("Expected event 0 is post install") - } - if rel.Hooks[0].Events[1] != release.HookPreDelete { - t.Errorf("Expected event 0 is pre-delete") - } - - if len(res.Manifest) == 0 { - t.Errorf("No manifest returned: %v", res) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_WithNotesRendered(t *testing.T) { - rs := rsFixture(t) - - req := installRequest( - withChart(withNotes(notesText+" {{.Release.Name}}")), - withName("with-notes"), - ) - res, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Name == "" { - t.Errorf("Expected release name.") - } - if res.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Namespace) - } - - rel, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - t.Logf("rel: %v", rel) - - if len(rel.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) - } - if rel.Hooks[0].Manifest != manifestWithHook { - t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) - } - - expectedNotes := fmt.Sprintf("%s %s", notesText, res.Name) - if rel.Info.Notes != expectedNotes { - t.Fatalf("Expected '%s', got '%s'", expectedNotes, rel.Info.Notes) - } - - if rel.Hooks[0].Events[0] != release.HookPostInstall { - t.Errorf("Expected event 0 is post install") - } - if rel.Hooks[0].Events[1] != release.HookPreDelete { - t.Errorf("Expected event 0 is pre-delete") - } - - if len(res.Manifest) == 0 { - t.Errorf("No manifest returned: %v", res) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) { - rs := rsFixture(t) - - req := installRequest(withChart( - withNotes(notesText), - withDependency(withNotes(notesText+" child")), - ), withName("with-chart-and-dependency-notes")) - res, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Name == "" { - t.Errorf("Expected release name.") - } - - rel, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - t.Logf("rel: %v", rel) - - if rel.Info.Notes != notesText { - t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Notes) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_DryRun(t *testing.T) { - rs := rsFixture(t) - - req := installRequest(withDryRun(), - withChart(withSampleTemplates()), - withName("test-dry-run"), - ) - res, err := rs.InstallRelease(req) - if err != nil { - t.Errorf("Failed install: %s", err) - } - if res.Name != "test-dry-run" { - t.Errorf("unexpected release name: %q", res.Name) - } - - if !strings.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", res.Manifest) - } - - if !strings.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") { - t.Errorf("unexpected output: %s", res.Manifest) - } - - if !strings.Contains(res.Manifest, "hello: Earth") { - t.Errorf("Should contain partial content. %s", res.Manifest) - } - - if strings.Contains(res.Manifest, "hello: {{ template \"_planet\" . }}") { - t.Errorf("Should not contain partial templates itself. %s", res.Manifest) - } - - if strings.Contains(res.Manifest, "empty") { - t.Errorf("Should not contain template data for an empty file. %s", res.Manifest) - } - - if _, err := rs.Releases.Get(res.Name, res.Version); err == nil { - t.Errorf("Expected no stored release.") - } - - if l := len(res.Hooks); l != 1 { - t.Fatalf("Expected 1 hook, got %d", l) - } - - if !res.Hooks[0].LastRun.IsZero() { - t.Error("Expected hook to not be marked as run.") - } - - if res.Info.Description != "Dry run complete" { - t.Errorf("unexpected description: %s", res.Info.Description) - } -} - -func TestInstallRelease_NoHooks(t *testing.T) { - rs := rsFixture(t) - rs.Releases.Create(releaseStub()) - - req := installRequest(withDisabledHooks(), withName("no-hooks")) - res, err := rs.InstallRelease(req) - if err != nil { - t.Errorf("Failed install: %s", err) - } - - if !res.Hooks[0].LastRun.IsZero() { - t.Errorf("Expected that no hooks were run. Got %s", res.Hooks[0].LastRun) - } -} - -func TestInstallRelease_FailedHooks(t *testing.T) { - rs := rsFixture(t) - rs.Releases.Create(releaseStub()) - rs.KubeClient = newHookFailingKubeClient() - - req := installRequest(withName("failed-hooks")) - res, err := rs.InstallRelease(req) - if err == nil { - t.Error("Expected failed install") - } - - if hl := res.Info.Status; hl != release.StatusFailed { - t.Errorf("Expected FAILED release. Got %s", hl) - } -} - -func TestInstallRelease_ReuseName(t *testing.T) { - rs := rsFixture(t) - rs.Log = t.Logf - rel := releaseStub() - rel.Info.Status = release.StatusUninstalled - rs.Releases.Create(rel) - - req := installRequest( - withReuseName(), - withName(rel.Name), - ) - res, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - if res.Name != rel.Name { - t.Errorf("expected %q, got %q", rel.Name, res.Name) - } - - getreq := &hapi.GetReleaseStatusRequest{Name: rel.Name, Version: 0} - getres, err := rs.GetReleaseStatus(getreq) - if err != nil { - t.Errorf("Failed to retrieve release: %s", err) - } - if getres.Info.Status != release.StatusDeployed { - t.Errorf("Release status is %q", getres.Info.Status) - } -} - -func TestInstallRelease_KubeVersion(t *testing.T) { - rs := rsFixture(t) - - req := installRequest( - withChart(withKube(">=0.0.0")), - withName("kube-version"), - ) - _, err := rs.InstallRelease(req) - if err != nil { - t.Fatalf("Expected valid range. Got %q", err) - } -} - -func TestInstallRelease_WrongKubeVersion(t *testing.T) { - rs := rsFixture(t) - - req := installRequest( - withChart(withKube(">=5.0.0")), - withName("wrong-kube-version"), - ) - - _, err := rs.InstallRelease(req) - if err == nil { - t.Fatalf("Expected to fail because of wrong version") - } - - expect := "chart requires kubernetesVersion" - if !strings.Contains(err.Error(), expect) { - t.Errorf("Expected %q to contain %q", err.Error(), expect) - } -} diff --git a/pkg/tiller/release_rollback.go b/pkg/tiller/release_rollback.go deleted file mode 100644 index 2387062ef..000000000 --- a/pkg/tiller/release_rollback.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "fmt" - "time" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" -) - -// RollbackRelease rolls back to a previous version of the given release. -func (s *ReleaseServer) RollbackRelease(req *hapi.RollbackReleaseRequest) (*release.Release, error) { - s.Log("preparing rollback of %s", req.Name) - currentRelease, targetRelease, err := s.prepareRollback(req) - if err != nil { - return nil, err - } - - if !req.DryRun { - s.Log("creating rolled back release for %s", req.Name) - if err := s.Releases.Create(targetRelease); err != nil { - return nil, err - } - } - s.Log("performing rollback of %s", req.Name) - res, err := s.performRollback(currentRelease, targetRelease, req) - if err != nil { - return res, err - } - - if !req.DryRun { - s.Log("updating status for rolled back release for %s", req.Name) - if err := s.Releases.Update(targetRelease); err != nil { - return res, err - } - } - - return res, nil -} - -// prepareRollback finds the previous release and prepares a new release object with -// the previous release's configuration -func (s *ReleaseServer) prepareRollback(req *hapi.RollbackReleaseRequest) (*release.Release, *release.Release, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", req.Name) - } - - if req.Version < 0 { - return nil, nil, errInvalidRevision - } - - currentRelease, err := s.Releases.Last(req.Name) - if err != nil { - return nil, nil, err - } - - previousVersion := req.Version - if req.Version == 0 { - previousVersion = currentRelease.Version - 1 - } - - s.Log("rolling back %s (current: v%d, target: v%d)", req.Name, currentRelease.Version, previousVersion) - - previousRelease, err := s.Releases.Get(req.Name, previousVersion) - if err != nil { - return nil, nil, err - } - - // Store a new release object with previous release's configuration - targetRelease := &release.Release{ - Name: req.Name, - Namespace: currentRelease.Namespace, - Chart: previousRelease.Chart, - Config: previousRelease.Config, - Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: time.Now(), - Status: release.StatusPendingRollback, - Notes: previousRelease.Info.Notes, - // Because we lose the reference to previous version elsewhere, we set the - // message here, and only override it later if we experience failure. - Description: fmt.Sprintf("Rollback to %d", previousVersion), - }, - Version: currentRelease.Version + 1, - Manifest: previousRelease.Manifest, - Hooks: previousRelease.Hooks, - } - - return currentRelease, targetRelease, nil -} - -func (s *ReleaseServer) performRollback(currentRelease, targetRelease *release.Release, req *hapi.RollbackReleaseRequest) (*release.Release, error) { - - if req.DryRun { - s.Log("dry run for %s", targetRelease.Name) - return targetRelease, nil - } - - // pre-rollback hooks - if !req.DisableHooks { - if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, hooks.PreRollback, req.Timeout); err != nil { - return targetRelease, err - } - } else { - s.Log("rollback hooks disabled for %s", req.Name) - } - - c := bytes.NewBufferString(currentRelease.Manifest) - t := bytes.NewBufferString(targetRelease.Manifest) - if err := s.KubeClient.Update(targetRelease.Namespace, c, t, req.Force, req.Recreate, req.Timeout, req.Wait); err != nil { - msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) - s.Log("warning: %s", msg) - currentRelease.Info.Status = release.StatusSuperseded - targetRelease.Info.Status = release.StatusFailed - targetRelease.Info.Description = msg - s.recordRelease(currentRelease, true) - s.recordRelease(targetRelease, true) - return targetRelease, err - } - - // post-rollback hooks - if !req.DisableHooks { - if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, hooks.PostRollback, req.Timeout); err != nil { - return targetRelease, err - } - } - - deployed, err := s.Releases.DeployedAll(currentRelease.Name) - if err != nil { - return nil, err - } - // Supersede all previous deployments, see issue #2941. - for _, r := range deployed { - s.Log("superseding previous deployment %d", r.Version) - r.Info.Status = release.StatusSuperseded - s.recordRelease(r, true) - } - - targetRelease.Info.Status = release.StatusDeployed - - return targetRelease, nil -} diff --git a/pkg/tiller/release_rollback_test.go b/pkg/tiller/release_rollback_test.go deleted file mode 100644 index 295c372d7..000000000 --- a/pkg/tiller/release_rollback_test.go +++ /dev/null @@ -1,247 +0,0 @@ -/* -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 tiller - -import ( - "strings" - "testing" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestRollbackRelease(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - upgradedRel.Hooks = []*release.Hook{ - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithRollbackHooks, - Events: []release.HookEvent{ - release.HookPreRollback, - release.HookPostRollback, - }, - }, - } - - upgradedRel.Manifest = "hello world" - rs.Releases.Update(rel) - rs.Releases.Create(upgradedRel) - - req := &hapi.RollbackReleaseRequest{ - Name: rel.Name, - } - res, err := rs.RollbackRelease(req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - if res.Name == "" { - t.Errorf("Expected release name.") - } - - if res.Name != rel.Name { - t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Name) - } - - if res.Namespace != rel.Namespace { - t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Namespace) - } - - if res.Version != 3 { - t.Errorf("Expected release version to be %v, got %v", 3, res.Version) - } - - updated, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - if len(updated.Hooks) != 2 { - t.Fatalf("Expected 2 hooks, got %d", len(updated.Hooks)) - } - - if updated.Hooks[0].Manifest != manifestWithHook { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - anotherUpgradedRelease := upgradeReleaseVersion(upgradedRel) - rs.Releases.Update(upgradedRel) - rs.Releases.Create(anotherUpgradedRelease) - - res, err = rs.RollbackRelease(req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - updated, err = rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - if len(updated.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) - } - - if updated.Hooks[0].Manifest != manifestWithRollbackHooks { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - if res.Version != 4 { - t.Errorf("Expected release version to be %v, got %v", 3, res.Version) - } - - if updated.Hooks[0].Events[0] != release.HookPreRollback { - t.Errorf("Expected event 0 to be pre rollback") - } - - if updated.Hooks[0].Events[1] != release.HookPostRollback { - t.Errorf("Expected event 1 to be post rollback") - } - - if len(res.Manifest) == 0 { - t.Errorf("No manifest returned: %v", res) - } - - if len(updated.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(updated.Manifest, "hello world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if res.Info.Description != "Rollback to 2" { - t.Errorf("Expected rollback to 2, got %q", res.Info.Description) - } -} - -func TestRollbackWithReleaseVersion(t *testing.T) { - rs := rsFixture(t) - rel2 := releaseStub() - rel2.Name = "other" - rs.Releases.Create(rel2) - rel := releaseStub() - rs.Releases.Create(rel) - v2 := upgradeReleaseVersion(rel) - rs.Releases.Update(rel) - rs.Releases.Create(v2) - v3 := upgradeReleaseVersion(v2) - // retain the original release as DEPLOYED while the update should fail - v2.Info.Status = release.StatusDeployed - v3.Info.Status = release.StatusFailed - rs.Releases.Update(v2) - rs.Releases.Create(v3) - - req := &hapi.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Version: 1, - } - - _, err := rs.RollbackRelease(req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - // check that v2 is now in a SUPERSEDED state - oldRel, err := rs.Releases.Get(rel.Name, 2) - if err != nil { - t.Fatalf("Failed to retrieve v1: %s", err) - } - if oldRel.Info.Status != release.StatusSuperseded { - t.Errorf("Expected v2 to be in a SUPERSEDED state, got %q", oldRel.Info.Status) - } - // make sure we didn't update some other deployments. - otherRel, err := rs.Releases.Get(rel2.Name, 1) - if err != nil { - t.Fatalf("Failed to retrieve other v1: %s", err) - } - if otherRel.Info.Status != release.StatusDeployed { - t.Errorf("Expected other deployed release to stay untouched, got %q", otherRel.Info.Status) - } -} - -func TestRollbackReleaseNoHooks(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rel.Hooks = []*release.Hook{ - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithRollbackHooks, - Events: []release.HookEvent{ - release.HookPreRollback, - release.HookPostRollback, - }, - }, - } - rs.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.Releases.Update(rel) - rs.Releases.Create(upgradedRel) - - req := &hapi.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - } - - res, err := rs.RollbackRelease(req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - if hl := res.Hooks[0].LastRun; !hl.IsZero() { - t.Errorf("Expected that no hooks were run. Got %s", hl) - } -} - -func TestRollbackReleaseFailure(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.Releases.Update(rel) - rs.Releases.Create(upgradedRel) - - req := &hapi.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - } - - rs.KubeClient = newUpdateFailingKubeClient() - res, err := rs.RollbackRelease(req) - if err == nil { - t.Error("Expected failed rollback") - } - - if targetStatus := res.Info.Status; targetStatus != release.StatusFailed { - t.Errorf("Expected FAILED release. Got %v", targetStatus) - } - - oldRelease, err := rs.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status; oldStatus != release.StatusSuperseded { - t.Errorf("Expected SUPERSEDED status on previous Release version. Got %v", oldStatus) - } -} diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go deleted file mode 100644 index 17051e342..000000000 --- a/pkg/tiller/release_server.go +++ /dev/null @@ -1,369 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "path" - "regexp" - "sort" - "strings" - "time" - - "github.com/pkg/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/discovery" - - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller/environment" - "k8s.io/helm/pkg/version" -) - -// releaseNameMaxLen is the maximum length of a release name. -// -// As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for -// charts to add data. Effectively, that gives us 53 chars. -// See https://github.com/helm/helm/issues/1528 -const releaseNameMaxLen = 53 - -// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine -// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually -// wants to see this file after rendering in the status command. However, it must be a suffix -// since there can be filepath in front of it. -const notesFileSuffix = "NOTES.txt" - -var ( - // errMissingChart indicates that a chart was not provided. - errMissingChart = errors.New("no chart provided") - // errMissingRelease indicates that a release (name) was not provided. - errMissingRelease = errors.New("no release provided") - // errInvalidRevision indicates that an invalid release revision number was provided. - errInvalidRevision = errors.New("invalid release revision") - //errInvalidName indicates that an invalid release name was provided - errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53") -) - -// ValidName is a regular expression for 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])? -// -// 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])+$") - -// ReleaseServer implements the server-side gRPC endpoint for the HAPI services. -type ReleaseServer struct { - engine Engine - discovery discovery.DiscoveryInterface - - // Releases stores records of releases. - Releases *storage.Storage - // KubeClient is a Kubernetes API client. - KubeClient environment.KubeClient - - Log func(string, ...interface{}) -} - -// NewReleaseServer creates a new release server. -func NewReleaseServer(discovery discovery.DiscoveryInterface, kubeClient environment.KubeClient) *ReleaseServer { - return &ReleaseServer{ - engine: new(engine.Engine), - discovery: discovery, - Releases: storage.Init(driver.NewMemory()), - KubeClient: kubeClient, - Log: func(_ string, _ ...interface{}) {}, - } -} - -// reuseValues copies values from the current release to a new release if the -// new release does not have any values. -// -// If the request already has values, or if there are no values in the current -// release, this does nothing. -// -// This is skipped if the req.ResetValues flag is set, in which case the -// request values are not altered. -func (s *ReleaseServer) reuseValues(req *hapi.UpdateReleaseRequest, current *release.Release) error { - if req.ResetValues { - // If ResetValues is set, we comletely ignore current.Config. - s.Log("resetting values to the chart's original version") - return nil - } - - // If the ReuseValues flag is set, we always copy the old values over the new config's values. - if req.ReuseValues { - s.Log("reusing the old release's values") - - // We have to regenerate the old coalesced values: - oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) - if err != nil { - return errors.Wrap(err, "failed to rebuild old values") - } - - req.Values = chartutil.CoalesceTables(current.Config, req.Values) - - req.Chart.Values = oldVals - - return nil - } - - if len(req.Values) == 0 && len(current.Config) > 0 { - s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) - req.Values = current.Config - } - return nil -} - -func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) { - - if start == "" { - return "", errors.New("name is required") - } - - if len(start) > releaseNameMaxLen { - return "", errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) - } - - h, err := s.Releases.History(start) - if err != nil || len(h) < 1 { - return start, nil - } - relutil.Reverse(h, relutil.SortByRevision) - rel := h[0] - - if st := rel.Info.Status; reuse && (st == release.StatusUninstalled || st == release.StatusFailed) { - // Allowe re-use of names if the previous release is marked deleted. - s.Log("name %s exists but is not in use, reusing name", start) - return start, nil - } else if reuse { - return "", errors.New("cannot re-use a name that is still in use") - } - - return "", errors.Errorf("a release named %s already exists.\nRun: helm ls --all %s; to check the status of the release\nOr run: helm del --purge %s; to delete it", start, start, start) -} - -func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { - if ch.Metadata.KubeVersion != "" { - cap, _ := values["Capabilities"].(*chartutil.Capabilities) - gitVersion := cap.KubeVersion.String() - k8sVersion := strings.Split(gitVersion, "+")[0] - if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { - return nil, nil, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) - } - } - - s.Log("rendering %s chart using values", ch.Name()) - files, err := s.engine.Render(ch, values) - if err != nil { - return nil, nil, "", err - } - - // 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. - notes := "" - for k, v := range files { - if strings.HasSuffix(k, notesFileSuffix) { - // Only apply the notes if it belongs to the parent chart - // Note: Do not use filePath.Join since it creates a path with \ which is not expected - if k == path.Join(ch.Name(), "templates", notesFileSuffix) { - notes = v - } - delete(files, k) - } - } - - // 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. - hooks, manifests, err := SortManifests(files, vs, 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. - b := bytes.NewBuffer(nil) - for name, content := range files { - if len(strings.TrimSpace(content)) == 0 { - continue - } - b.WriteString("\n---\n# Source: " + name + "\n") - b.WriteString(content) - } - return nil, b, "", err - } - - // Aggregate all valid manifests into one big doc. - b := bytes.NewBuffer(nil) - for _, m := range manifests { - b.WriteString("\n---\n# Source: " + m.Name + "\n") - b.WriteString(m.Content) - } - - return hooks, b, notes, nil -} - -// recordRelease with an update operation in case reuse has been set. -func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) { - if reuse { - if err := s.Releases.Update(r); err != nil { - s.Log("warning: Failed to update release %s: %s", r.Name, err) - } - } else if err := s.Releases.Create(r); err != nil { - s.Log("warning: Failed to record release %s: %s", r.Name, err) - } -} - -func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error { - code, ok := events[hook] - if !ok { - return errors.Errorf("unknown hook %s", hook) - } - - s.Log("executing %d %s hooks for %s", len(hs), hook, name) - executingHooks := []*release.Hook{} - for _, h := range hs { - for _, e := range h.Events { - if e == code { - executingHooks = append(executingHooks, h) - } - } - } - - sort.Sort(hookByWeight(executingHooks)) - - for _, h := range executingHooks { - if err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.BeforeHookCreation, name, namespace, hook, s.KubeClient); err != nil { - return err - } - - b := bytes.NewBufferString(h.Manifest) - if err := s.KubeClient.Create(namespace, b, timeout, false); err != nil { - return errors.Wrapf(err, "warning: Release %s %s %s failed", name, hook, h.Path) - } - // No way to rewind a bytes.Buffer()? - b.Reset() - b.WriteString(h.Manifest) - - if err := s.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil { - s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) - // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted - // under failed condition. If so, then clear the corresponding resource object in the hook - if err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.HookFailed, name, namespace, hook, s.KubeClient); err != nil { - return err - } - return err - } - } - - s.Log("hooks complete for %s %s", hook, name) - // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted - // under succeeded condition. If so, then clear the corresponding resource object in each hook - for _, h := range executingHooks { - if err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.HookSucceeded, name, namespace, hook, s.KubeClient); err != nil { - return err - } - h.LastRun = time.Now() - } - - return nil -} - -func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { - r := bytes.NewReader(manifest) - _, err := c.BuildUnstructured(ns, r) - return err -} - -func validateReleaseName(releaseName string) error { - if releaseName == "" { - return errMissingRelease - } - - if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { - return errInvalidName - } - - return nil -} - -func (s *ReleaseServer) deleteHookIfShouldBeDeletedByDeletePolicy(h *release.Hook, policy, name, namespace, hook string, kubeCli environment.KubeClient) error { - b := bytes.NewBufferString(h.Manifest) - if hookHasDeletePolicy(h, policy) { - s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy) - if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil { - s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) - return errHookDelete - } - } - return nil -} - -// hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices -// supported by helm. If so, mark the hook as one should be deleted. -func hookHasDeletePolicy(h *release.Hook, policy string) bool { - if dp, ok := deletePolices[policy]; ok { - for _, v := range h.DeletePolicies { - if dp == v { - return true - } - } - } - return false -} - -func newCapabilities(dc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) { - kubeVersion, err := dc.ServerVersion() - if err != nil { - return nil, err - } - - apiVersions, err := GetVersionSet(dc) - if err != nil { - return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") - } - - return &chartutil.Capabilities{ - KubeVersion: kubeVersion, - APIVersions: apiVersions, - }, nil -} - -// GetVersionSet retrieves a set of available k8s API versions -func GetVersionSet(dc discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { - groups, err := dc.ServerGroups() - if groups.Size() > 0 { - versions := metav1.ExtractGroupVersions(groups) - return chartutil.NewVersionSet(versions...), err - } - return chartutil.DefaultVersionSet, err - -} diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go deleted file mode 100644 index 082c3a2db..000000000 --- a/pkg/tiller/release_server_test.go +++ /dev/null @@ -1,814 +0,0 @@ -/* -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 tiller - -import ( - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "regexp" - "testing" - "time" - - "github.com/ghodss/yaml" - "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - "k8s.io/cli-runtime/pkg/genericclioptions/resource" - "k8s.io/client-go/kubernetes/fake" - - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/tiller/environment" -) - -var verbose = flag.Bool("test.log", false, "enable tiller logging") - -const notesText = "my notes here" - -var manifestWithHook = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-install,pre-delete -data: - name: value` - -var manifestWithTestHook = `kind: Pod -metadata: - name: finding-nemo, - annotations: - "helm.sh/hook": test-success -spec: - containers: - - name: nemo-test - image: fake-image - cmd: fake-command -` - -var manifestWithKeep = `kind: ConfigMap -metadata: - name: test-cm-keep - annotations: - "helm.sh/resource-policy": keep -data: - name: value -` - -var manifestWithUpgradeHooks = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-upgrade,pre-upgrade -data: - name: value` - -var manifestWithRollbackHooks = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-rollback,pre-rollback -data: - name: value -` - -func rsFixture(t *testing.T) *ReleaseServer { - t.Helper() - - dc := fake.NewSimpleClientset().Discovery() - kc := &environment.PrintingKubeClient{Out: ioutil.Discard} - rs := NewReleaseServer(dc, kc) - rs.Log = func(format string, v ...interface{}) { - t.Helper() - if *verbose { - t.Logf(format, v...) - } - } - return rs -} - -type chartOptions struct { - *chart.Chart -} - -type chartOption func(*chartOptions) - -func buildChart(opts ...chartOption) *chart.Chart { - c := &chartOptions{ - Chart: &chart.Chart{ - // TODO: This should be more complete. - Metadata: &chart.Metadata{ - Name: "hello", - }, - // This adds a basic template and hooks. - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, - }, - }, - } - - for _, opt := range opts { - opt(c) - } - - return c.Chart -} - -func withKube(version string) chartOption { - return func(opts *chartOptions) { - opts.Metadata.KubeVersion = version - } -} - -func withDependency(dependencyOpts ...chartOption) chartOption { - return func(opts *chartOptions) { - opts.AddDependency(buildChart(dependencyOpts...)) - } -} - -func withNotes(notes string) chartOption { - return func(opts *chartOptions) { - opts.Templates = append(opts.Templates, &chart.File{ - Name: "templates/NOTES.txt", - Data: []byte(notes), - }) - } -} - -func withSampleTemplates() chartOption { - return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ - // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, - } - opts.Templates = append(opts.Templates, sampleTemplates...) - } -} - -type installOptions struct { - *hapi.InstallReleaseRequest -} - -type installOption func(*installOptions) - -func withName(name string) installOption { - return func(opts *installOptions) { - opts.Name = name - } -} - -func withDryRun() installOption { - return func(opts *installOptions) { - opts.DryRun = true - } -} - -func withDisabledHooks() installOption { - return func(opts *installOptions) { - opts.DisableHooks = true - } -} - -func withReuseName() installOption { - return func(opts *installOptions) { - opts.ReuseName = true - } -} - -func withChart(chartOpts ...chartOption) installOption { - return func(opts *installOptions) { - opts.Chart = buildChart(chartOpts...) - } -} - -func installRequest(opts ...installOption) *hapi.InstallReleaseRequest { - reqOpts := &installOptions{ - &hapi.InstallReleaseRequest{ - Namespace: "spaced", - Chart: buildChart(), - }, - } - - for _, opt := range opts { - opt(reqOpts) - } - - return reqOpts.InstallReleaseRequest -} - -// chartStub creates a fully stubbed out chart. -func chartStub() *chart.Chart { - return buildChart(withSampleTemplates()) -} - -// releaseStub creates a release stub, complete with the chartStub as its chart. -func releaseStub() *release.Release { - return namedReleaseStub("angry-panda", release.StatusDeployed) -} - -func namedReleaseStub(name string, status release.Status) *release.Release { - return &release.Release{ - Name: name, - Info: &release.Info{ - FirstDeployed: time.Now(), - LastDeployed: time.Now(), - Status: status, - Description: "Named Release Stub", - }, - Chart: chartStub(), - Config: map[string]interface{}{"name": "value"}, - Version: 1, - Hooks: []*release.Hook{ - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithHook, - Events: []release.HookEvent{ - release.HookPostInstall, - release.HookPreDelete, - }, - }, - { - Name: "finding-nemo", - Kind: "Pod", - Path: "finding-nemo", - Manifest: manifestWithTestHook, - Events: []release.HookEvent{ - release.HookReleaseTestSuccess, - }, - }, - }, - } -} - -func upgradeReleaseVersion(rel *release.Release) *release.Release { - rel.Info.Status = release.StatusSuperseded - return &release.Release{ - Name: rel.Name, - Info: &release.Info{ - FirstDeployed: rel.Info.FirstDeployed, - LastDeployed: time.Now(), - Status: release.StatusDeployed, - }, - Chart: rel.Chart, - Config: rel.Config, - Version: rel.Version + 1, - } -} - -func TestValidName(t *testing.T) { - for name, valid := range map[string]error{ - "nina pinta santa-maria": errInvalidName, - "nina-pinta-santa-maria": nil, - "-nina": errInvalidName, - "pinta-": errInvalidName, - "santa-maria": nil, - "niña": errInvalidName, - "...": errInvalidName, - "pinta...": errInvalidName, - "santa...maria": nil, - "": errMissingRelease, - " ": errInvalidName, - ".nina.": errInvalidName, - "nina.pinta": nil, - "abcdefghi-abcdefghi-abcdefghi-abcdefghi-abcdefghi-abcd": errInvalidName, - } { - if valid != validateReleaseName(name) { - t.Errorf("Expected %q to be %s", name, valid) - } - } -} - -func TestUniqName(t *testing.T) { - rs := rsFixture(t) - - rel1 := releaseStub() - rel2 := releaseStub() - rel2.Name = "happy-panda" - rel2.Info.Status = release.StatusUninstalled - - rs.Releases.Create(rel1) - rs.Releases.Create(rel2) - - tests := []struct { - name string - expect string - reuse bool - err bool - }{ - {"", "", false, true}, // Blank name is illegal - {"first", "first", false, false}, - {"angry-panda", "", false, true}, - {"happy-panda", "", false, true}, - {"happy-panda", "happy-panda", true, false}, - {"hungry-hungry-hungry-hungry-hungry-hungry-hungry-hungry-hippos", "", true, true}, // Exceeds max name length - } - - for _, tt := range tests { - u, err := rs.uniqName(tt.name, tt.reuse) - if err != nil { - if tt.err { - continue - } - t.Fatal(err) - } - if tt.err { - t.Errorf("Expected an error for %q", tt.name) - } - if match, err := regexp.MatchString(tt.expect, u); err != nil { - t.Fatal(err) - } else if !match { - t.Errorf("Expected %q to match %q", u, tt.expect) - } - } -} - -func releaseWithKeepStub(rlsName string) *release.Release { - ch := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "bunnychart", - }, - Templates: []*chart.File{ - {Name: "templates/configmap", Data: []byte(manifestWithKeep)}, - }, - } - - return &release.Release{ - Name: rlsName, - Info: &release.Info{ - FirstDeployed: time.Now(), - LastDeployed: time.Now(), - Status: release.StatusDeployed, - }, - Chart: ch, - Config: map[string]interface{}{"name": "value"}, - Version: 1, - Manifest: manifestWithKeep, - } -} - -func newUpdateFailingKubeClient() *updateFailingKubeClient { - return &updateFailingKubeClient{ - PrintingKubeClient: environment.PrintingKubeClient{Out: os.Stdout}, - } - -} - -type updateFailingKubeClient struct { - environment.PrintingKubeClient -} - -func (u *updateFailingKubeClient) Update(namespace string, originalReader, modifiedReader io.Reader, force, recreate bool, timeout int64, shouldWait bool) error { - return errors.New("Failed update in kube client") -} - -func newHookFailingKubeClient() *hookFailingKubeClient { - return &hookFailingKubeClient{ - PrintingKubeClient: environment.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -type hookFailingKubeClient struct { - environment.PrintingKubeClient -} - -func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - return errors.New("Failed watch") -} - -type mockHooksManifest struct { - Metadata struct { - Name string - Annotations map[string]string - } -} -type mockHooksKubeClient struct { - Resources map[string]*mockHooksManifest -} - -var errResourceExists = errors.New("resource already exists") - -func (kc *mockHooksKubeClient) makeManifest(r io.Reader) (*mockHooksManifest, error) { - b, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - - manifest := &mockHooksManifest{} - if err = yaml.Unmarshal(b, manifest); err != nil { - return nil, err - } - - return manifest, nil -} -func (kc *mockHooksKubeClient) Create(ns string, r io.Reader, timeout int64, shouldWait bool) error { - manifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - if _, hasKey := kc.Resources[manifest.Metadata.Name]; hasKey { - return errResourceExists - } - - kc.Resources[manifest.Metadata.Name] = manifest - - return nil -} -func (kc *mockHooksKubeClient) Get(ns string, r io.Reader) (string, error) { - return "", nil -} -func (kc *mockHooksKubeClient) Delete(ns string, r io.Reader) error { - manifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - delete(kc.Resources, manifest.Metadata.Name) - - return nil -} -func (kc *mockHooksKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - paramManifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - manifest, hasManifest := kc.Resources[paramManifest.Metadata.Name] - if !hasManifest { - return errors.Errorf("mockHooksKubeClient.WatchUntilReady: no such resource %s found", paramManifest.Metadata.Name) - } - - if manifest.Metadata.Annotations["mockHooksKubeClient/Emulate"] == "hook-failed" { - return errors.Errorf("mockHooksKubeClient.WatchUntilReady: hook-failed") - } - - return nil -} -func (kc *mockHooksKubeClient) Update(_ string, _, _ io.Reader, _, _ bool, _ int64, _ bool) error { - return nil -} -func (kc *mockHooksKubeClient) Build(_ string, _ io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (kc *mockHooksKubeClient) BuildUnstructured(_ string, _ io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(_ string, _ io.Reader, _ time.Duration) (v1.PodPhase, error) { - return v1.PodUnknown, nil -} - -func deletePolicyStub(kubeClient *mockHooksKubeClient) *ReleaseServer { - return &ReleaseServer{ - engine: new(engine.Engine), - discovery: fake.NewSimpleClientset().Discovery(), - KubeClient: kubeClient, - Log: func(_ string, _ ...interface{}) {}, - } -} - -func deletePolicyHookStub(hookName string, extraAnnotations map[string]string, DeletePolicies []release.HookDeletePolicy) *release.Hook { - extraAnnotationsStr := "" - for k, v := range extraAnnotations { - extraAnnotationsStr += fmt.Sprintf(" \"%s\": \"%s\"\n", k, v) - } - - return &release.Hook{ - Name: hookName, - Kind: "Job", - Path: hookName, - Manifest: fmt.Sprintf(`kind: Job -metadata: - name: %s - annotations: - "helm.sh/hook": pre-install,pre-upgrade -%sdata: -name: value`, hookName, extraAnnotationsStr), - Events: []release.HookEvent{ - release.HookPreInstall, - release.HookPreUpgrade, - }, - DeletePolicies: DeletePolicies, - } -} - -func execHookShouldSucceed(rs *ReleaseServer, hook *release.Hook, releaseName, namespace, hookType string) error { - err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600) - return errors.Wrapf(err, "expected hook %s to be successful", hook.Name) -} - -func execHookShouldFail(rs *ReleaseServer, hook *release.Hook, releaseName, namespace, hookType string) error { - if err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600); err == nil { - return errors.Errorf("expected hook %s to be failed", hook.Name) - } - return nil -} - -func execHookShouldFailWithError(rs *ReleaseServer, hook *release.Hook, releaseName, namespace, hookType string, expectedError error) error { - err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600) - if cause := errors.Cause(err); cause != expectedError { - return errors.Errorf("expected hook %s to fail with error \n%v \ngot \n%v", hook.Name, expectedError, cause) - } - return nil -} - -type deletePolicyContext struct { - ReleaseServer *ReleaseServer - ReleaseName string - Namespace string - HookName string - KubeClient *mockHooksKubeClient -} - -func newDeletePolicyContext() *deletePolicyContext { - kubeClient := &mockHooksKubeClient{ - Resources: make(map[string]*mockHooksManifest), - } - - return &deletePolicyContext{ - KubeClient: kubeClient, - ReleaseServer: deletePolicyStub(kubeClient), - ReleaseName: "flying-carp", - Namespace: "river", - HookName: "migration-job", - } -} - -func TestSuccessfulHookWithoutDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, nil, nil) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be created by kube client", hook.Name) - } -} - -func TestFailedHookWithoutDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"mockHooksKubeClient/Emulate": "hook-failed"}, - nil, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be created by kube client", hook.Name) - } -} - -func TestSuccessfulHookWithSucceededDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "hook-succeeded"}, - []release.HookDeletePolicy{release.HookSucceeded}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestSuccessfulHookWithFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "hook-failed"}, - []release.HookDeletePolicy{release.HookFailed}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithSucceededDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded", - }, - []release.HookDeletePolicy{release.HookSucceeded}, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } -} - -func TestFailedHookWithFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-failed", - }, - []release.HookDeletePolicy{release.HookFailed}, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook failed", hook.Name) - } -} - -func TestSuccessfulHookWithSuccededOrFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookFailed}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithSuccededOrFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookFailed}, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook failed", hook.Name) - } -} - -func TestHookAlreadyExists(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, nil, nil) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } - if err := execHookShouldFailWithError(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade, errResourceExists); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after already exists error", hook.Name) - } -} - -func TestHookDeletingWithBeforeHookCreationDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "before-hook-creation"}, - []release.HookDeletePolicy{release.HookBeforeHookCreation}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } -} - -func TestSuccessfulHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookBeforeHookCreation}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookBeforeHookCreation}, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } -} - -func TestFailedThenSuccessfulHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookBeforeHookCreation}, - ) - - if err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } - - hook = deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.HookDeletePolicy{release.HookSucceeded, release.HookBeforeHookCreation}, - ) - - if err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade); err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} diff --git a/pkg/tiller/release_status.go b/pkg/tiller/release_status.go deleted file mode 100644 index 9d3390c8d..000000000 --- a/pkg/tiller/release_status.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -// GetReleaseStatus gets the status information for a named release. -func (s *ReleaseServer) GetReleaseStatus(req *hapi.GetReleaseStatusRequest) (*hapi.GetReleaseStatusResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, errors.Errorf("getStatus: Release name is invalid: %s", req.Name) - } - - var rel *release.Release - - if req.Version <= 0 { - var err error - rel, err = s.Releases.Last(req.Name) - if err != nil { - return nil, errors.Wrapf(err, "getting deployed release %q", req.Name) - } - } else { - var err error - if rel, err = s.Releases.Get(req.Name, req.Version); err != nil { - return nil, errors.Wrapf(err, "getting release '%s' (v%d)", req.Name, req.Version) - } - } - - if rel.Info == nil { - return nil, errors.New("release info is missing") - } - if rel.Chart == nil { - return nil, errors.New("release chart is missing") - } - - sc := rel.Info.Status - statusResp := &hapi.GetReleaseStatusResponse{ - Name: rel.Name, - Namespace: rel.Namespace, - Info: rel.Info, - } - - // Ok, we got the status of the release as we had jotted down, now we need to match the - // manifest we stashed away with reality from the cluster. - resp, err := s.KubeClient.Get(rel.Namespace, bytes.NewBufferString(rel.Manifest)) - if sc == release.StatusUninstalled || sc == release.StatusFailed { - // Skip errors if this is already deleted or failed. - return statusResp, nil - } else if err != nil { - return nil, errors.Wrapf(err, "warning: Get for %s failed", rel.Name) - } - rel.Info.Resources = resp - return statusResp, nil -} diff --git a/pkg/tiller/release_status_test.go b/pkg/tiller/release_status_test.go deleted file mode 100644 index 4a2adc78a..000000000 --- a/pkg/tiller/release_status_test.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -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 tiller - -import ( - "testing" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestGetReleaseStatus(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - - res, err := rs.GetReleaseStatus(&hapi.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Errorf("Error getting release content: %s", err) - } - - if res.Name != rel.Name { - t.Errorf("Expected name %q, got %q", rel.Name, res.Name) - } - if res.Info.Status != release.StatusDeployed { - t.Errorf("Expected %s, got %s", release.StatusDeployed, res.Info.Status) - } -} - -func TestGetReleaseStatusUninstalled(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rel.Info.Status = release.StatusUninstalled - if err := rs.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - - res, err := rs.GetReleaseStatus(&hapi.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Fatalf("Error getting release content: %s", err) - } - - if res.Info.Status != release.StatusUninstalled { - t.Errorf("Expected %s, got %s", release.StatusUninstalled, res.Info.Status) - } -} diff --git a/pkg/tiller/release_uninstall.go b/pkg/tiller/release_uninstall.go deleted file mode 100644 index dea552c8f..000000000 --- a/pkg/tiller/release_uninstall.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "strings" - "time" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/kube" - relutil "k8s.io/helm/pkg/releaseutil" -) - -// UninstallRelease deletes all of the resources associated with this release, and marks the release UNINSTALLED. -func (s *ReleaseServer) UninstallRelease(req *hapi.UninstallReleaseRequest) (*hapi.UninstallReleaseResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, errors.Errorf("uninstall: Release name is invalid: %s", req.Name) - } - - rels, err := s.Releases.History(req.Name) - if err != nil { - return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", req.Name) - } - if len(rels) < 1 { - return nil, errMissingRelease - } - - relutil.SortByRevision(rels) - rel := rels[len(rels)-1] - - // TODO: Are there any cases where we want to force a delete even if it's - // already marked deleted? - if rel.Info.Status == release.StatusUninstalled { - if req.Purge { - if err := s.purgeReleases(rels...); err != nil { - return nil, errors.Wrap(err, "uninstall: Failed to purge the release") - } - return &hapi.UninstallReleaseResponse{Release: rel}, nil - } - return nil, errors.Errorf("the release named %q is already deleted", req.Name) - } - - s.Log("uninstall: Deleting %s", req.Name) - rel.Info.Status = release.StatusUninstalling - rel.Info.Deleted = time.Now() - rel.Info.Description = "Deletion in progress (or silently failed)" - res := &hapi.UninstallReleaseResponse{Release: rel} - - if !req.DisableHooks { - if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, hooks.PreDelete, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("delete hooks disabled for %s", req.Name) - } - - // From here on out, the release is currently considered to be in StatusUninstalling - // state. - if err := s.Releases.Update(rel); err != nil { - s.Log("uninstall: Failed to store updated release: %s", err) - } - - kept, errs := s.deleteRelease(rel) - res.Info = kept - - if !req.DisableHooks { - if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, hooks.PostDelete, req.Timeout); err != nil { - errs = append(errs, err) - } - } - - rel.Info.Status = release.StatusUninstalled - rel.Info.Description = "Uninstallation complete" - - if req.Purge { - s.Log("purge requested for %s", req.Name) - err := s.purgeReleases(rels...) - return res, errors.Wrap(err, "uninstall: Failed to purge the release") - } - - if err := s.Releases.Update(rel); err != nil { - s.Log("uninstall: Failed to store updated release: %s", err) - } - - if len(errs) > 0 { - return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) - } - return res, nil -} - -func joinErrors(errs []error) string { - es := make([]string, 0, len(errs)) - for _, e := range errs { - es = append(es, e.Error()) - } - return strings.Join(es, "; ") -} - -func (s *ReleaseServer) purgeReleases(rels ...*release.Release) error { - for _, rel := range rels { - if _, err := s.Releases.Delete(rel.Name, rel.Version); err != nil { - return err - } - } - return nil -} - -// deleteRelease deletes the release and returns manifests that were kept in the deletion process -func (s *ReleaseServer) deleteRelease(rel *release.Release) (kept string, errs []error) { - caps, err := newCapabilities(s.discovery) - if err != nil { - return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} - } - - manifests := relutil.SplitManifests(rel.Manifest) - _, files, err := SortManifests(manifests, caps.APIVersions, UninstallOrder) - if err != nil { - // We could instead just delete everything in no particular order. - // FIXME: One way to delete at this point would be to try a label-based - // deletion. The problem with this is that we could get a false positive - // and delete something that was not legitimately part of this release. - return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} - } - - filesToKeep, filesToDelete := filterManifestsToKeep(files) - if len(filesToKeep) > 0 { - kept = summarizeKeptManifests(filesToKeep, s.KubeClient, rel.Namespace) - } - - for _, file := range filesToDelete { - b := bytes.NewBufferString(strings.TrimSpace(file.Content)) - if b.Len() == 0 { - continue - } - if err := s.KubeClient.Delete(rel.Namespace, b); err != nil { - s.Log("uninstall: Failed deletion of %q: %s", rel.Name, err) - if err == kube.ErrNoObjectsVisited { - // Rewrite the message from "no objects visited" - err = errors.New("object not found, skipping delete") - } - errs = append(errs, err) - } - } - return kept, errs -} diff --git a/pkg/tiller/release_uninstall_test.go b/pkg/tiller/release_uninstall_test.go deleted file mode 100644 index 666134fad..000000000 --- a/pkg/tiller/release_uninstall_test.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -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 tiller - -import ( - "strings" - "testing" - - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestUninstallRelease(t *testing.T) { - rs := rsFixture(t) - rs.Releases.Create(releaseStub()) - - req := &hapi.UninstallReleaseRequest{ - Name: "angry-panda", - } - - res, err := rs.UninstallRelease(req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != "angry-panda" { - t.Errorf("Expected angry-panda, got %q", res.Release.Name) - } - - if res.Release.Info.Status != release.StatusUninstalled { - t.Errorf("Expected status code to be UNINSTALLED, got %s", res.Release.Info.Status) - } - - if res.Release.Hooks[0].LastRun.IsZero() { - t.Error("Expected LastRun to be greater than zero.") - } - - if res.Release.Info.Deleted.Second() <= 0 { - t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Second()) - } - - if res.Release.Info.Description != "Uninstallation complete" { - t.Errorf("Expected Uninstallation complete, got %q", res.Release.Info.Description) - } -} - -func TestUninstallPurgeRelease(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.Releases.Update(rel) - rs.Releases.Create(upgradedRel) - - req := &hapi.UninstallReleaseRequest{ - Name: "angry-panda", - Purge: true, - } - - res, err := rs.UninstallRelease(req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != "angry-panda" { - t.Errorf("Expected angry-panda, got %q", res.Release.Name) - } - - if res.Release.Info.Status != release.StatusUninstalled { - t.Errorf("Expected status code to be UNINSTALLED, got %s", res.Release.Info.Status) - } - - if res.Release.Info.Deleted.Second() <= 0 { - t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Second()) - } - rels, err := rs.GetHistory(&hapi.GetHistoryRequest{Name: "angry-panda"}) - if err != nil { - t.Fatal(err) - } - if len(rels) != 0 { - t.Errorf("Expected no releases in storage, got %d", len(rels)) - } -} - -func TestUninstallPurgeDeleteRelease(t *testing.T) { - rs := rsFixture(t) - rs.Releases.Create(releaseStub()) - - req := &hapi.UninstallReleaseRequest{ - Name: "angry-panda", - } - - _, err := rs.UninstallRelease(req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - req2 := &hapi.UninstallReleaseRequest{ - Name: "angry-panda", - Purge: true, - } - - _, err2 := rs.UninstallRelease(req2) - if err2 != nil && err2.Error() != "'angry-panda' has no deployed releases" { - t.Errorf("Failed uninstall: %s", err2) - } -} - -func TestUninstallReleaseWithKeepPolicy(t *testing.T) { - rs := rsFixture(t) - name := "angry-bunny" - rs.Releases.Create(releaseWithKeepStub(name)) - - req := &hapi.UninstallReleaseRequest{ - Name: name, - } - - res, err := rs.UninstallRelease(req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != name { - t.Errorf("Expected angry-bunny, got %q", res.Release.Name) - } - - if res.Release.Info.Status != release.StatusUninstalled { - t.Errorf("Expected status code to be UNINSTALLED, got %s", res.Release.Info.Status) - } - - if res.Info == "" { - t.Errorf("Expected response info to not be empty") - } else { - if !strings.Contains(res.Info, "[ConfigMap] test-cm-keep") { - t.Errorf("unexpected output: %s", res.Info) - } - } -} - -func TestUninstallReleaseNoHooks(t *testing.T) { - rs := rsFixture(t) - rs.Releases.Create(releaseStub()) - - req := &hapi.UninstallReleaseRequest{ - Name: "angry-panda", - DisableHooks: true, - } - - res, err := rs.UninstallRelease(req) - if err != nil { - t.Errorf("Failed uninstall: %s", err) - } - - // The default value for a protobuf timestamp is nil. - if !res.Release.Hooks[0].LastRun.IsZero() { - t.Errorf("Expected LastRun to be zero, got %s.", res.Release.Hooks[0].LastRun) - } -} diff --git a/pkg/tiller/release_update.go b/pkg/tiller/release_update.go deleted file mode 100644 index 35acd4526..000000000 --- a/pkg/tiller/release_update.go +++ /dev/null @@ -1,291 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "fmt" - "strings" - "time" - - "github.com/pkg/errors" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" - "k8s.io/helm/pkg/hooks" -) - -// UpdateRelease takes an existing release and new information, and upgrades the release. -func (s *ReleaseServer) UpdateRelease(req *hapi.UpdateReleaseRequest) (*release.Release, error) { - if err := validateReleaseName(req.Name); err != nil { - return nil, errors.Errorf("updateRelease: Release name is invalid: %s", req.Name) - } - s.Log("preparing update for %s", req.Name) - currentRelease, updatedRelease, err := s.prepareUpdate(req) - if err != nil { - if req.Force { - // Use the --force, Luke. - return s.performUpdateForce(req) - } - return nil, err - } - - s.Releases.MaxHistory = req.MaxHistory - - if !req.DryRun { - s.Log("creating updated release for %s", req.Name) - if err := s.Releases.Create(updatedRelease); err != nil { - return nil, err - } - } - - s.Log("performing update for %s", req.Name) - res, err := s.performUpdate(currentRelease, updatedRelease, req) - if err != nil { - return res, err - } - - if !req.DryRun { - s.Log("updating status for updated release for %s", req.Name) - if err := s.Releases.Update(updatedRelease); err != nil { - return res, err - } - } - - return res, nil -} - -// prepareUpdate builds an updated release for an update operation. -func (s *ReleaseServer) prepareUpdate(req *hapi.UpdateReleaseRequest) (*release.Release, *release.Release, error) { - if req.Chart == nil { - return nil, nil, errMissingChart - } - - // finds the deployed release with the given name - currentRelease, err := s.Releases.Deployed(req.Name) - if err != nil { - return nil, nil, err - } - - // determine if values will be reused - if err := s.reuseValues(req, currentRelease); err != nil { - return nil, nil, err - } - - // finds the non-deleted release with the given name - lastRelease, err := s.Releases.Last(req.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 - - ts := time.Now() - options := chartutil.ReleaseOptions{ - Name: req.Name, - IsUpgrade: true, - } - - caps, err := newCapabilities(s.discovery) - if err != nil { - return nil, nil, err - } - valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options, caps) - if err != nil { - return nil, nil, err - } - - hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions) - if err != nil { - return nil, nil, err - } - - // Store an updated release. - updatedRelease := &release.Release{ - Name: req.Name, - Namespace: currentRelease.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: ts, - Status: release.StatusPendingUpgrade, - Description: "Preparing upgrade", // This should be overwritten later. - }, - Version: revision, - Manifest: manifestDoc.String(), - Hooks: hooks, - } - - if len(notesTxt) > 0 { - updatedRelease.Info.Notes = notesTxt - } - err = validateManifest(s.KubeClient, currentRelease.Namespace, manifestDoc.Bytes()) - return currentRelease, updatedRelease, err -} - -// performUpdateForce performs the same action as a `helm uninstall && helm install --replace`. -func (s *ReleaseServer) performUpdateForce(req *hapi.UpdateReleaseRequest) (*release.Release, error) { - // find the last release with the given name - oldRelease, err := s.Releases.Last(req.Name) - if err != nil { - return nil, err - } - - newRelease, err := s.prepareRelease(&hapi.InstallReleaseRequest{ - Chart: req.Chart, - Values: req.Values, - DryRun: req.DryRun, - Name: req.Name, - DisableHooks: req.DisableHooks, - Namespace: oldRelease.Namespace, - ReuseName: true, - Timeout: req.Timeout, - Wait: req.Wait, - }) - if err != nil { - // On dry run, append the manifest contents to a failed release. This is - // a stop-gap until we can revisit an error backchannel post-2.0. - if req.DryRun && strings.HasPrefix(err.Error(), "YAML parse error") { - err = errors.Wrap(err, newRelease.Manifest) - } - return newRelease, errors.Wrap(err, "failed update prepare step") - } - - // From here on out, the release is considered to be in StatusUninstalling or StatusUninstalled - // state. There is no turning back. - oldRelease.Info.Status = release.StatusUninstalling - oldRelease.Info.Deleted = time.Now() - oldRelease.Info.Description = "Deletion in progress (or silently failed)" - s.recordRelease(oldRelease, true) - - // pre-delete hooks - if !req.DisableHooks { - if err := s.execHook(oldRelease.Hooks, oldRelease.Name, oldRelease.Namespace, hooks.PreDelete, req.Timeout); err != nil { - return newRelease, err - } - } else { - s.Log("hooks disabled for %s", req.Name) - } - - // delete manifests from the old release - _, errs := s.deleteRelease(oldRelease) - - oldRelease.Info.Status = release.StatusUninstalled - oldRelease.Info.Description = "Uninstallation complete" - s.recordRelease(oldRelease, true) - - if len(errs) > 0 { - return newRelease, errors.Errorf("upgrade --force successfully uninstalled the previous release, but encountered %d error(s) and cannot continue: %s", len(errs), joinErrors(errs)) - } - - // post-delete hooks - if !req.DisableHooks { - if err := s.execHook(oldRelease.Hooks, oldRelease.Name, oldRelease.Namespace, hooks.PostDelete, req.Timeout); err != nil { - return newRelease, err - } - } - - // pre-install hooks - if !req.DisableHooks { - if err := s.execHook(newRelease.Hooks, newRelease.Name, newRelease.Namespace, hooks.PreInstall, req.Timeout); err != nil { - return newRelease, err - } - } - - // update new release with next revision number so as to append to the old release's history - newRelease.Version = oldRelease.Version + 1 - s.recordRelease(newRelease, false) - if err := s.updateRelease(oldRelease, newRelease, req); err != nil { - msg := fmt.Sprintf("Upgrade %q failed: %s", newRelease.Name, err) - s.Log("warning: %s", msg) - newRelease.Info.Status = release.StatusFailed - newRelease.Info.Description = msg - s.recordRelease(newRelease, true) - return newRelease, err - } - - // post-install hooks - if !req.DisableHooks { - if err := s.execHook(newRelease.Hooks, newRelease.Name, newRelease.Namespace, hooks.PostInstall, req.Timeout); err != nil { - msg := fmt.Sprintf("Release %q failed post-install: %s", newRelease.Name, err) - s.Log("warning: %s", msg) - newRelease.Info.Status = release.StatusFailed - newRelease.Info.Description = msg - s.recordRelease(newRelease, true) - return newRelease, err - } - } - - newRelease.Info.Status = release.StatusDeployed - newRelease.Info.Description = "Upgrade complete" - s.recordRelease(newRelease, true) - - return newRelease, nil -} - -func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *hapi.UpdateReleaseRequest) (*release.Release, error) { - - if req.DryRun { - s.Log("dry run for %s", updatedRelease.Name) - updatedRelease.Info.Description = "Dry run complete" - return updatedRelease, nil - } - - // pre-upgrade hooks - if !req.DisableHooks { - if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, hooks.PreUpgrade, req.Timeout); err != nil { - return updatedRelease, err - } - } else { - s.Log("update hooks disabled for %s", req.Name) - } - if err := s.updateRelease(originalRelease, updatedRelease, req); err != nil { - msg := fmt.Sprintf("Upgrade %q failed: %s", updatedRelease.Name, err) - s.Log("warning: %s", msg) - updatedRelease.Info.Status = release.StatusFailed - updatedRelease.Info.Description = msg - s.recordRelease(originalRelease, true) - s.recordRelease(updatedRelease, true) - return updatedRelease, err - } - - // post-upgrade hooks - if !req.DisableHooks { - if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, hooks.PostUpgrade, req.Timeout); err != nil { - return updatedRelease, err - } - } - - originalRelease.Info.Status = release.StatusSuperseded - s.recordRelease(originalRelease, true) - - updatedRelease.Info.Status = release.StatusDeployed - updatedRelease.Info.Description = "Upgrade complete" - - return updatedRelease, nil -} - -// updateRelease performs an update from current to target release -func (s *ReleaseServer) updateRelease(current, target *release.Release, req *hapi.UpdateReleaseRequest) error { - c := bytes.NewBufferString(current.Manifest) - t := bytes.NewBufferString(target.Manifest) - return s.KubeClient.Update(target.Namespace, c, t, req.Force, req.Recreate, req.Timeout, req.Wait) -} diff --git a/pkg/tiller/release_update_test.go b/pkg/tiller/release_update_test.go deleted file mode 100644 index f0c480096..000000000 --- a/pkg/tiller/release_update_test.go +++ /dev/null @@ -1,422 +0,0 @@ -/* -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 tiller - -import ( - "reflect" - "strings" - "testing" - - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/hapi" - "k8s.io/helm/pkg/hapi/release" -) - -func TestUpdateRelease(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - } - res, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - if res.Name == "" { - t.Errorf("Expected release name.") - } - - if res.Name != rel.Name { - t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Name) - } - - if res.Namespace != rel.Namespace { - t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Namespace) - } - - updated := compareStoredAndReturnedRelease(t, *rs, res) - - if len(updated.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) - } - if updated.Hooks[0].Manifest != manifestWithUpgradeHooks { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - if updated.Hooks[0].Events[0] != release.HookPostUpgrade { - t.Errorf("Expected event 0 to be post upgrade") - } - - if updated.Hooks[0].Events[1] != release.HookPreUpgrade { - t.Errorf("Expected event 0 to be pre upgrade") - } - - if len(updated.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if res.Config == nil { - t.Errorf("Got release without config: %#v", res) - } else if len(res.Config) != len(rel.Config) { - t.Errorf("Expected release values %q, got %q", rel.Config, res.Config) - } - - if !strings.Contains(updated.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", updated.Manifest) - } - - if res.Version != 2 { - t.Errorf("Expected release version to be %v, got %v", 2, res.Version) - } - - edesc := "Upgrade complete" - if got := res.Info.Description; got != edesc { - t.Errorf("Expected description %q, got %q", edesc, got) - } -} -func TestUpdateRelease_ResetValues(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - ResetValues: true, - } - res, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been unset. Config: &chart.Config{Raw: `name: value`}, - if len(res.Config) > 0 { - t.Errorf("Expected chart config to be empty, got %q", res.Config) - } -} - -// This is a regression test for bug found in issue #3655 -func TestUpdateRelease_ComplexReuseValues(t *testing.T) { - rs := rsFixture(t) - - installReq := &hapi.InstallReleaseRequest{ - Name: "complex-reuse-values", - Namespace: "spaced", - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, - }, - }, - Values: map[string]interface{}{"foo": "bar"}, - } - - t.Log("Running Install release with foo: bar override") - rel, err := rs.InstallRelease(installReq) - if err != nil { - t.Fatal(err) - } - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - } - - t.Log("Running Update release with no overrides and no reuse-values flag") - rel, err = rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - if rel.Config != nil && rel.Config["foo"] != "bar" { - t.Errorf("Expected chart value 'foo' = 'bar', got %s", rel.Config) - } - - req = &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - Values: map[string]interface{}{"foo2": "bar2"}, - ReuseValues: true, - } - - t.Log("Running Update release with foo2: bar2 override and reuse-values") - rel, err = rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - // This should have the newly-passed overrides. - if len(rel.Config) != 2 && rel.Config["foo2"] != "bar2" { - t.Errorf("Expected chart value 'foo2' = 'bar2', got %s", rel.Config) - } - - req = &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - Values: map[string]interface{}{"foo": "baz"}, - ReuseValues: true, - } - - t.Log("Running Update release with foo=baz override with reuse-values flag") - rel, err = rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - if len(rel.Config) != 2 && rel.Config["foo"] != "baz" { - t.Errorf("Expected chart value 'foo' = 'baz', got %s", rel.Config) - } -} - -func TestUpdateRelease_ReuseValues(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - // Since reuseValues is set, this should get ignored. - Values: map[string]interface{}{"foo": "bar"}, - }, - Values: map[string]interface{}{"name2": "val2"}, - ReuseValues: true, - } - res, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been overwritten with the old value. - if got := res.Chart.Values["name"]; got != "value" { - t.Errorf("Expected chart values 'name' to be 'value', got %q", got) - } - // This should have the newly-passed overrides and any other computed values. `name: value` comes from release Config via releaseStub() - expect := "name: value\nname2: val2\n" - if len(res.Config) != 2 || res.Config["name"] != "value" || res.Config["name2"] != "val2" { - t.Errorf("Expected request config to be %q, got %q", expect, res.Config) - } - compareStoredAndReturnedRelease(t, *rs, res) -} - -func TestUpdateRelease_ResetReuseValues(t *testing.T) { - // This verifies that when both reset and reuse are set, reset wins. - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - ResetValues: true, - ReuseValues: true, - } - res, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been unset. Config: &chart.Config{Raw: `name: value`}, - if len(res.Config) > 0 { - t.Errorf("Expected chart config to be empty, got %q", res.Config) - } - compareStoredAndReturnedRelease(t, *rs, res) -} - -func TestUpdateReleaseFailure(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - rs.KubeClient = newUpdateFailingKubeClient() - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/something", Data: []byte("hello: world")}, - }, - }, - } - - res, err := rs.UpdateRelease(req) - if err == nil { - t.Error("Expected failed update") - } - - if updatedStatus := res.Info.Status; updatedStatus != release.StatusFailed { - t.Errorf("Expected FAILED release. Got %s", updatedStatus) - } - - compareStoredAndReturnedRelease(t, *rs, res) - - expectedDescription := "Upgrade \"angry-panda\" failed: Failed update in kube client" - if got := res.Info.Description; got != expectedDescription { - t.Errorf("Expected description %q, got %q", expectedDescription, got) - } - - oldRelease, err := rs.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status; oldStatus != release.StatusDeployed { - t.Errorf("Expected Deployed status on previous Release version. Got %v", oldStatus) - } -} - -func TestUpdateReleaseFailure_Force(t *testing.T) { - rs := rsFixture(t) - rel := namedReleaseStub("forceful-luke", release.StatusFailed) - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/something", Data: []byte("text: 'Did you ever hear the tragedy of Darth Plagueis the Wise? I thought not. It’s not a story the Jedi would tell you. It’s a Sith legend. Darth Plagueis was a Dark Lord of the Sith, so powerful and so wise he could use the Force to influence the Midichlorians to create life... He had such a knowledge of the Dark Side that he could even keep the ones he cared about from dying. The Dark Side of the Force is a pathway to many abilities some consider to be unnatural. He became so powerful... The only thing he was afraid of was losing his power, which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew, then his apprentice killed him in his sleep. Ironic. He could save others from death, but not himself.'")}, - }, - }, - Force: true, - } - - res, err := rs.UpdateRelease(req) - if err != nil { - t.Errorf("Expected successful update, got %v", err) - } - - if updatedStatus := res.Info.Status; updatedStatus != release.StatusDeployed { - t.Errorf("Expected DEPLOYED release. Got %s", updatedStatus) - } - - compareStoredAndReturnedRelease(t, *rs, res) - - expectedDescription := "Upgrade complete" - if got := res.Info.Description; got != expectedDescription { - t.Errorf("Expected description %q, got %q", expectedDescription, got) - } - - oldRelease, err := rs.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status; oldStatus != release.StatusUninstalled { - t.Errorf("Expected Deleted status on previous Release version. Got %v", oldStatus) - } -} - -func TestUpdateReleaseNoHooks(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - } - - res, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - if hl := res.Hooks[0].LastRun; !hl.IsZero() { - t.Errorf("Expected that no hooks were run. Got %s", hl) - } - -} - -func TestUpdateReleaseNoChanges(t *testing.T) { - rs := rsFixture(t) - rel := releaseStub() - rs.Releases.Create(rel) - - req := &hapi.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: rel.Chart, - } - - _, err := rs.UpdateRelease(req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } -} - -func compareStoredAndReturnedRelease(t *testing.T, rs ReleaseServer, res *release.Release) *release.Release { - storedRelease, err := rs.Releases.Get(res.Name, res.Version) - if err != nil { - t.Fatalf("Expected release for %s (%v).", res.Name, rs.Releases) - } - - if !reflect.DeepEqual(storedRelease, res) { - t.Errorf("Stored release doesn't match returned Release") - } - - return storedRelease -}