ref: remove pkg/helm, pkg/hapi, pkg/tiller

Signed-off-by: Matthew Fisher <matt.fisher@microsoft.com>
pull/5365/head
Matthew Fisher 7 years ago
parent 62f144a9d8
commit 2571dbf82f
No known key found for this signature in database
GPG Key ID: 92AA783CBAAE8E3B

@ -35,7 +35,7 @@ func TestCreateCmd(t *testing.T) {
cname := "testchart" cname := "testchart"
// Run a create // 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) t.Errorf("Failed to run create: %s", err)
return return
} }
@ -86,7 +86,7 @@ func TestCreateStarterCmd(t *testing.T) {
defer testChdir(t, tdir)() defer testChdir(t, tdir)()
// Run a create // 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) t.Errorf("Failed to run create: %s", err)
return return
} }

@ -16,18 +16,13 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
"github.com/Masterminds/semver"
"github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart/loader"
) )
const dependencyDesc = ` const dependencyDesc = `
@ -103,14 +98,8 @@ func newDependencyCmd(out io.Writer) *cobra.Command {
return cmd return cmd
} }
type dependencyLisOptions struct {
chartpath string
}
func newDependencyListCmd(out io.Writer) *cobra.Command { func newDependencyListCmd(out io.Writer) *cobra.Command {
o := &dependencyLisOptions{ client := action.NewDependency()
chartpath: ".",
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list CHART", Use: "list CHART",
@ -119,151 +108,12 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
Long: dependencyListDesc, Long: dependencyListDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 { 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 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)
}
}
}

@ -17,10 +17,12 @@ package main
import ( import (
"io" "io"
"path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/getter"
) )
@ -36,17 +38,8 @@ If no lock file is found, 'helm dependency build' will mirror the behavior
of 'helm dependency update'. of 'helm dependency update'.
` `
type dependencyBuildOptions struct {
keyring string // --keyring
verify bool // --verify
chartpath string
}
func newDependencyBuildCmd(out io.Writer) *cobra.Command { func newDependencyBuildCmd(out io.Writer) *cobra.Command {
o := &dependencyBuildOptions{ client := action.NewDependency()
chartpath: ".",
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "build CHART", Use: "build CHART",
@ -54,31 +47,28 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
Long: dependencyBuildDesc, Long: dependencyBuildDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 { 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),
}
if client.Verify {
man.Verify = downloader.VerifyIfPossible
} }
return o.run(out) if settings.Debug {
man.Debug = true
}
return man.Build()
}, },
} }
f := cmd.Flags() client.AddBuildFlags(cmd.Flags())
f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures")
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys")
return cmd 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),
}
if o.verify {
man.Verify = downloader.VerifyIfPossible
}
return man.Build()
}

@ -44,7 +44,7 @@ func TestDependencyBuildCmd(t *testing.T) {
} }
cmd := fmt.Sprintf("--home='%s' dependency build '%s'", hh, hh.Path(chartname)) 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. // In the first pass, we basically want the same results as an update.
if err != nil { if err != nil {
@ -72,7 +72,7 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
out, err = executeCommand(nil, cmd) _, out, err = executeActionCommand(cmd)
if err != nil { if err != nil {
t.Logf("Output: %s", out) t.Logf("Output: %s", out)
t.Fatal(err) t.Fatal(err)

@ -22,9 +22,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
) )
const dependencyUpDesc = ` 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. 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. // newDependencyUpdateCmd creates a new dependency update command.
func newDependencyUpdateCmd(out io.Writer) *cobra.Command { func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
o := &dependencyUpdateOptions{ client := action.NewDependency()
chartpath: ".",
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update CHART", Use: "update CHART",
@ -67,37 +53,29 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
Long: dependencyUpDesc, Long: dependencyUpDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 { 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 if settings.Debug {
return o.run(out) man.Debug = true
}
return man.Update()
}, },
} }
f := cmd.Flags() client.AddUpdateFlags(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")
return cmd 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()
}

@ -16,7 +16,6 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -52,7 +51,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Logf("Output: %s", out) t.Logf("Output: %s", out)
t.Fatal(err) t.Fatal(err)
@ -95,7 +94,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Logf("Output: %s", out) t.Logf("Output: %s", out)
t.Fatal(err) t.Fatal(err)
@ -133,7 +132,7 @@ func TestDependencyUpdateCmd_SkipRefresh(t *testing.T) {
t.Fatal(err) 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 { if err == nil {
t.Fatal("Expected failure to find the repo with skipRefresh") t.Fatal("Expected failure to find the repo with skipRefresh")
} }
@ -164,13 +163,8 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
out := bytes.NewBuffer(nil) _, output, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname)))
o := &dependencyUpdateOptions{} if err != nil {
o.helmhome = hh
o.chartpath = hh.Path(chartname)
if err := o.run(out); err != nil {
output := out.String()
t.Logf("Output: %s", output) t.Logf("Output: %s", output)
t.Fatal(err) t.Fatal(err)
} }
@ -178,14 +172,14 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
// Chart repo is down // Chart repo is down
srv.Stop() srv.Stop()
if err := o.run(out); err == nil { _, output, err = executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname)))
output := out.String() if err == nil {
t.Logf("Output: %s", output) t.Logf("Output: %s", output)
t.Fatal("Expected error, got nil") t.Fatal("Expected error, got nil")
} }
// Make sure charts dir still has dependencies // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -201,7 +195,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
} }
// Make sure tmpcharts is deleted // 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") t.Fatalf("tmpcharts dir still exists")
} }
} }

@ -22,7 +22,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/action"
) )
var getHelp = ` 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. chart, the supplied values, and the generated manifest file.
` `
type getOptions struct { func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
version int // --revision client := action.NewGet(cfg)
release string
client helm.Interface
}
func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &getOptions{client: client}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "get RELEASE_NAME", Use: "get RELEASE_NAME",
@ -55,25 +47,19 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: getHelp, Long: getHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0] res, err := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) if err != nil {
return o.run(out) return err
}
return printRelease(out, res)
}, },
} }
cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") client.AddFlags(cmd.Flags())
cmd.AddCommand(newGetValuesCmd(client, out)) cmd.AddCommand(newGetValuesCmd(cfg, out))
cmd.AddCommand(newGetManifestCmd(client, out)) cmd.AddCommand(newGetManifestCmd(cfg, out))
cmd.AddCommand(newGetHooksCmd(client, out)) cmd.AddCommand(newGetHooksCmd(cfg, out))
return cmd 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)
}

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/action"
) )
const getHooksHelp = ` 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. Hooks are formatted in YAML and separated by the YAML '---\n' separator.
` `
type getHooksOptions struct { func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
release string client := action.NewGet(cfg)
client helm.Interface
version int
}
func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &getHooksOptions{client: client}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "hooks RELEASE_NAME", Use: "hooks RELEASE_NAME",
@ -47,24 +41,18 @@ func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: getHooksHelp, Long: getHooksHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0] res, err := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) if err != nil {
return o.run(out) 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 { client.AddFlags(cmd.Flags())
res, err := o.client.ReleaseContent(o.release, o.version)
if err != nil {
fmt.Fprintln(out, o.release)
return err
}
for _, hook := range res.Hooks { return cmd
fmt.Fprintf(out, "---\n# %s\n%s", hook.Name, hook.Manifest)
}
return nil
} }

@ -19,8 +19,7 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestGetHooks(t *testing.T) { func TestGetHooks(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetHooks(t *testing.T) {
name: "get hooks with release", name: "get hooks with release",
cmd: "get hooks aeneas", cmd: "get hooks aeneas",
golden: "output/get-hooks.txt", 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", name: "get hooks without args",
cmd: "get hooks", cmd: "get hooks",

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/action"
) )
var getManifestHelp = ` 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. charts, those resources will also be included in the manifest.
` `
type getManifestOptions struct { func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
version int // --revision client := action.NewGet(cfg)
release string
client helm.Interface
}
func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &getManifestOptions{client: client}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "manifest RELEASE_NAME", Use: "manifest RELEASE_NAME",
@ -51,22 +43,16 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: getManifestHelp, Long: getManifestHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0] res, err := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) if err != nil {
return o.run(out) return err
}
fmt.Fprintln(out, res.Manifest)
return nil
}, },
} }
cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision") client.AddFlags(cmd.Flags())
return cmd
}
// getManifest implements 'helm get manifest' return cmd
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
} }

@ -19,8 +19,7 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestGetManifest(t *testing.T) { func TestGetManifest(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetManifest(t *testing.T) {
name: "get manifest with release", name: "get manifest with release",
cmd: "get manifest juno", cmd: "get manifest juno",
golden: "output/get-manifest.txt", 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", name: "get manifest without args",
cmd: "get manifest", cmd: "get manifest",

@ -19,8 +19,7 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestGetCmd(t *testing.T) { func TestGetCmd(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetCmd(t *testing.T) {
name: "get with a release", name: "get with a release",
cmd: "get thomas-guide", cmd: "get thomas-guide",
golden: "output/get-release.txt", 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", name: "get requires release name arg",
cmd: "get", cmd: "get",

@ -20,29 +20,18 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/ghodss/yaml"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
) )
var getValuesHelp = ` var getValuesHelp = `
This command downloads a values file for a given release. This command downloads a values file for a given release.
` `
type getValuesOptions struct { func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
allValues bool // --all client := action.NewGetValues(cfg)
version int // --revision
release string
client helm.Interface
}
func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &getValuesOptions{client: client}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "values RELEASE_NAME", Use: "values RELEASE_NAME",
@ -50,43 +39,15 @@ func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: getValuesHelp, Long: getValuesHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0] res, err := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) if err != nil {
return o.run(out) return err
}
fmt.Fprintln(out, res)
return nil
}, },
} }
cmd.Flags().BoolVarP(&o.allValues, "all", "a", false, "dump all (computed) values") client.AddFlags(cmd.Flags())
cmd.Flags().IntVar(&o.version, "revision", 0, "get the named release with revision")
return cmd 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
}

@ -19,8 +19,7 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestGetValuesCmd(t *testing.T) { func TestGetValuesCmd(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetValuesCmd(t *testing.T) {
name: "get values with a release", name: "get values with a release",
cmd: "get values thomas-guide", cmd: "get values thomas-guide",
golden: "output/get-values.txt", 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", name: "get values requires release name arg",
cmd: "get values", cmd: "get values",

@ -27,15 +27,14 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/helm/pkg/action" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/cli"
"k8s.io/helm/pkg/helm/environment"
"k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
) )
var ( var (
settings environment.EnvSettings settings cli.EnvSettings
config genericclioptions.RESTClientGetter config genericclioptions.RESTClientGetter
configOnce sync.Once configOnce sync.Once
) )
@ -52,45 +51,13 @@ func logf(format string, v ...interface{}) {
} }
func main() { 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 { if err := cmd.Execute(); err != nil {
logf("%+v", err) logf("%+v", err)
os.Exit(1) 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 { func newActionConfig(allNamespaces bool) *action.Configuration {
kc := kube.New(kubeConfig()) kc := kube.New(kubeConfig())
kc.Log = logf kc.Log = logf

@ -30,13 +30,12 @@ import (
"k8s.io/helm/internal/test" "k8s.io/helm/internal/test"
"k8s.io/helm/pkg/action" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller/environment"
) )
func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } 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) { t.Run(tt.name, func(t *testing.T) {
defer resetEnv()() defer resetEnv()()
c := &helm.FakeClient{ storage := storageFixture()
Rels: tt.rels, for _, rel := range tt.rels {
TestRunStatus: tt.testRunStatus, 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 { if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err) t.Errorf("expected error, got '%v'", err)
} }
@ -115,12 +116,12 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command,
actionConfig := &action.Configuration{ actionConfig := &action.Configuration{
Releases: store, Releases: store,
KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(), Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) {}, Log: func(format string, v ...interface{}) {},
} }
root := newRootCmd(nil, actionConfig, buf, args) root := newRootCmd(actionConfig, buf, args)
root.SetOutput(buf) root.SetOutput(buf)
root.SetArgs(args) root.SetArgs(args)
@ -136,35 +137,11 @@ type cmdTestCase struct {
golden string golden string
wantError bool wantError bool
// Rels are the available releases at the start of the test. // Rels are the available releases at the start of the test.
rels []*release.Release 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
} }
// deprecated: Switch to executeActionCommandC func executeActionCommand(cmd string) (*cobra.Command, string, error) {
func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string, error) { return executeActionCommandC(storageFixture(), cmd)
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
} }
// ensureTestHome creates a home directory like ensureHome, but without remote references. // ensureTestHome creates a home directory like ensureHome, but without remote references.

@ -17,31 +17,15 @@ limitations under the License.
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"github.com/ghodss/yaml"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
) )
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 = ` var historyHelp = `
History prints historical revisions for a given release. 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 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 Upgraded successfully
` `
type historyOptions struct { func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
colWidth uint // --col-width client := action.NewHistory(cfg)
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}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "history RELEASE_NAME", Use: "history RELEASE_NAME",
@ -78,94 +52,16 @@ func newHistoryCmd(c helm.Interface, out io.Writer) *cobra.Command {
Aliases: []string{"hist"}, Aliases: []string{"hist"},
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.client = ensureHelmClient(o.client, false) history, err := client.Run(args[0])
o.release = args[0] if err != nil {
return o.run(out) return err
}
fmt.Fprintln(out, history)
return nil
}, },
} }
f := cmd.Flags() client.AddFlags(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)")
return cmd 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)
}

@ -19,13 +19,12 @@ package main
import ( import (
"testing" "testing"
rpb "k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestHistoryCmd(t *testing.T) { func TestHistoryCmd(t *testing.T) {
mk := func(name string, vers int, status rpb.Status) *rpb.Release { mk := func(name string, vers int, status release.Status) *release.Release {
return helm.ReleaseMock(&helm.MockReleaseOptions{ return release.Mock(&release.MockReleaseOptions{
Name: name, Name: name,
Version: vers, Version: vers,
Status: status, Status: status,
@ -35,35 +34,35 @@ func TestHistoryCmd(t *testing.T) {
tests := []cmdTestCase{{ tests := []cmdTestCase{{
name: "get history for release", name: "get history for release",
cmd: "history angry-bird", cmd: "history angry-bird",
rels: []*rpb.Release{ rels: []*release.Release{
mk("angry-bird", 4, rpb.StatusDeployed), mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded), mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 2, rpb.StatusSuperseded), mk("angry-bird", 2, release.StatusSuperseded),
mk("angry-bird", 1, rpb.StatusSuperseded), mk("angry-bird", 1, release.StatusSuperseded),
}, },
golden: "output/history.txt", golden: "output/history.txt",
}, { }, {
name: "get history with max limit set", name: "get history with max limit set",
cmd: "history angry-bird --max 2", cmd: "history angry-bird --max 2",
rels: []*rpb.Release{ rels: []*release.Release{
mk("angry-bird", 4, rpb.StatusDeployed), mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded), mk("angry-bird", 3, release.StatusSuperseded),
}, },
golden: "output/history-limit.txt", golden: "output/history-limit.txt",
}, { }, {
name: "get history with yaml output format", name: "get history with yaml output format",
cmd: "history angry-bird --output yaml", cmd: "history angry-bird --output yaml",
rels: []*rpb.Release{ rels: []*release.Release{
mk("angry-bird", 4, rpb.StatusDeployed), mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded), mk("angry-bird", 3, release.StatusSuperseded),
}, },
golden: "output/history.yaml", golden: "output/history.yaml",
}, { }, {
name: "get history with json output format", name: "get history with json output format",
cmd: "history angry-bird --output json", cmd: "history angry-bird --output json",
rels: []*rpb.Release{ rels: []*release.Release{
mk("angry-bird", 4, rpb.StatusDeployed), mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded), mk("angry-bird", 3, release.StatusSuperseded),
}, },
golden: "output/history.json", golden: "output/history.json",
}} }}

@ -29,7 +29,7 @@ import (
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/getter" "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"
"k8s.io/helm/pkg/plugin/installer" "k8s.io/helm/pkg/plugin/installer"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"

@ -21,7 +21,7 @@ import (
"os" "os"
"testing" "testing"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/helmpath"
) )
const testPluginsFile = "testdata/plugins.yaml" const testPluginsFile = "testdata/plugins.yaml"

@ -17,29 +17,18 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"fmt"
"io" "io"
"path/filepath"
"regexp" "k8s.io/helm/pkg/release"
"strings"
"text/tabwriter"
"text/template"
"time"
"github.com/Masterminds/sprig"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
) )
const installDesc = ` const installDesc = `
@ -104,268 +93,87 @@ To see the list of chart repositories, use 'helm repo list'. To search for
charts in a repository, use 'helm search'. 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 { func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &installOptions{cfg: cfg} client := action.NewInstall(cfg)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install [NAME] [CHART]", Use: "install [NAME] [CHART]",
Short: "install a chart", Short: "install a chart",
Long: installDesc, Long: installDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
debug("Original chart version: %q", o.version) rel, err := runInstall(args, client, out)
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)
if err != nil {
return err
}
o.name = name // FIXME
cp, err := o.locateChart(chart)
if err != nil { if err != nil {
return err return err
} }
o.chartPath = cp action.PrintRelease(out, rel)
return nil
return o.run(out)
}, },
} }
f := cmd.Flags() client.AddFlags(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)
return cmd return cmd
} }
// nameAndChart returns the name of the release and the chart that should be used. func runInstall(args []string, client *action.Install, out io.Writer) (*release.Release, error) {
// debug("Original chart version: %q", client.Version)
// This will read the flags and handle name generation if necessary. if client.Version == "" && client.Devel {
func (o *installOptions) nameAndChart(args []string) (string, string, error) { debug("setting version to >0.0.0-0")
flagsNotSet := func() error { client.Version = ">0.0.0-0"
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()
}
if o.nameTemplate != "" {
newName, err := templateName(o.nameTemplate)
return newName, args[0], err
}
if !o.generateName {
return "", args[0], errors.New("must either provide a name or specify --generate-name")
} }
base := filepath.Base(args[0]) name, chart, err := client.NameAndChart(args)
if base == "." || base == "" { if err != nil {
base = "chart" return nil, err
} }
newName := fmt.Sprintf("%s-%d", base, time.Now().Unix()) client.ReleaseName = name
return newName, args[0], nil
}
func (o *installOptions) run(out io.Writer) error { cp, err := client.ChartPathOptions.LocateChart(chart, settings)
debug("CHART PATH: %s\n", o.chartPath)
rawVals, err := o.mergedValues()
if err != nil { if err != nil {
return err return nil, err
} }
// If template is specified, try to run the template. debug("CHART PATH: %s\n", cp)
if o.nameTemplate != "" {
o.name, err = templateName(o.nameTemplate) if err := client.ValueOptions.MergeValues(settings); err != nil {
if err != nil { return nil, err
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)
} }
// Check chart dependencies to make sure all are present in /charts // 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 { if err != nil {
return err return nil, err
} }
validInstallableChart, err := chartutil.IsChartInstallable(chartRequested) validInstallableChart, err := chartutil.IsChartInstallable(chartRequested)
if !validInstallableChart { if !validInstallableChart {
return err return nil, err
} }
if req := chartRequested.Metadata.Dependencies; req != nil { 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: // As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209 // https://github.com/helm/helm/issues/2209
if err := checkDependencies(chartRequested, req); err != nil { if err := action.CheckDependencies(chartRequested, req); err != nil {
if o.depUp { if client.DependencyUpdate {
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: o.chartPath, ChartPath: cp,
HelmHome: settings.Home, HelmHome: settings.Home,
Keyring: o.keyring, Keyring: client.ChartPathOptions.Keyring,
SkipUpdate: false, SkipUpdate: false,
Getters: getter.All(settings), Getters: getter.All(settings),
} }
if err := man.Update(); err != nil { if err := man.Update(); err != nil {
return err return nil, err
} }
} else { } else {
return err return nil, 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
} }
} }
missing = append(missing, r.Name)
} }
if len(missing) > 0 { client.Namespace = getNamespace()
return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) return client.Run(chartRequested)
}
return nil
} }

@ -17,8 +17,6 @@ limitations under the License.
package main package main
import ( import (
"reflect"
"regexp"
"testing" "testing"
) )
@ -142,133 +140,3 @@ func TestInstall(t *testing.T) {
runTestActionCmd(t, tests) 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)
}
}

@ -19,19 +19,12 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"github.com/ghodss/yaml"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/lint"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/strvals"
) )
var longLintHelp = ` var longLintHelp = `
@ -43,159 +36,41 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio
or recommendation, it will emit [WARNING] messages. or recommendation, it will emit [WARNING] messages.
` `
type lintOptions struct {
strict bool
paths []string
valuesOptions
}
func newLintCmd(out io.Writer) *cobra.Command { func newLintCmd(out io.Writer) *cobra.Command {
o := &lintOptions{paths: []string{"."}} client := action.NewLint()
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "lint PATH", Use: "lint PATH",
Short: "examines a chart for possible issues", Short: "examines a chart for possible issues",
Long: longLintHelp, Long: longLintHelp,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
paths := []string{"."}
if len(args) > 0 { if len(args) > 0 {
o.paths = args paths = args
} }
return o.run(out) client.Namespace = getNamespace()
}, if err := client.ValueOptions.MergeValues(settings); err != nil {
} return err
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
} }
} else { result := client.Run(paths)
fmt.Println("==> Linting", path) var message strings.Builder
fmt.Fprintf(&message, "%d chart(s) linted, %d chart(s) failed\n", result.TotalChartsLinted, len(result.Errors))
if len(linter.Messages) == 0 { for _, err := range result.Errors {
fmt.Println("Lint OK") fmt.Fprintf(&message, "\t%s\n", err)
} }
for _, msg := range result.Messages {
for _, msg := range linter.Messages { fmt.Fprintf(&message, "\t%s\n", msg)
fmt.Println(msg)
} }
total = total + 1 if len(result.Errors) > 0 {
if linter.HighestSeverity >= lowestTolerance { return errors.New(message.String())
failures = failures + 1
} }
} fmt.Fprintf(out, message.String())
fmt.Println("") return nil
} },
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, &currentMap); 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 client.AddFlags(cmd.Flags())
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 return cmd
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
} }

@ -19,14 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release"
) )
var listHelp = ` 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 By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date. 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. regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned. Only items that match the filter will be returned.
$ helm list 'ara[a-z]+' $ helm list --filter 'ara[a-z]+'
NAME UPDATED CHART NAME UPDATED CHART
maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0 maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0
@ -56,143 +53,37 @@ server's default, which may be much higher than 256. Pairing the '--max'
flag with the '--offset' flag allows you to page through results. flag with the '--offset' flag allows you to page through results.
` `
type listOptions struct { func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// flags client := action.NewList(cfg)
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{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list [FILTER]", Use: "list",
Short: "list releases", Short: "list releases",
Long: listHelp, Long: listHelp,
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Args: require.MaximumNArgs(1), Args: require.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 { if client.AllNamespaces {
o.filter = strings.Join(args, " ") client.SetConfiguration(newActionConfig(true))
}
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
} }
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 { for _, res := range results {
fmt.Fprintln(out, res.Name) fmt.Fprintln(out, res.Name)
} }
return err return err
} }
fmt.Fprintln(out, formatList(results, 90)) fmt.Fprintln(out, action.FormatList(results))
return err return err
}, },
} }
f := cmd.Flags() client.AddFlags(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")
return cmd 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()
}

@ -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, &currentMap); 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)
}

@ -20,22 +20,15 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"syscall"
"github.com/Masterminds/semver" "k8s.io/helm/pkg/action"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "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/downloader"
"k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/provenance"
) )
const packageDesc = ` const packageDesc = `
@ -49,186 +42,60 @@ Chart.yaml file, and (if found) build the current directory into a chart.
Versioned chart archives are used by Helm package repositories. 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 { func newPackageCmd(out io.Writer) *cobra.Command {
o := &packageOptions{} client := action.NewPackage()
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "package [CHART_PATH] [...]", Use: "package [CHART_PATH] [...]",
Short: "package a chart directory into a chart archive", Short: "package a chart directory into a chart archive",
Long: packageDesc, Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.home = settings.Home
if len(args) == 0 { if len(args) == 0 {
return errors.Errorf("need at least one argument, the path to the chart") return errors.Errorf("need at least one argument, the path to the chart")
} }
if o.sign { if client.Sign {
if o.key == "" { if client.Key == "" {
return errors.New("--key is required for signing a package") 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") 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++ { for i := 0; i < len(args); i++ {
o.path = args[i] path, err := filepath.Abs(args[i])
if err := o.run(out); err != nil { 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 return err
} }
fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p)
} }
return nil return nil
}, },
} }
f := cmd.Flags() client.AddFlags(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)
return cmd 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
}

@ -31,30 +31,9 @@ import (
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil" "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) { func TestPackage(t *testing.T) {
statExe := "stat" statExe := "stat"
statFileMsg := "no such file or directory" statFileMsg := "no such file or directory"

@ -22,7 +22,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "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"
"k8s.io/helm/pkg/plugin/installer" "k8s.io/helm/pkg/plugin/installer"
) )

@ -22,7 +22,7 @@ import (
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/helmpath"
) )
type pluginListOptions struct { type pluginListOptions struct {

@ -24,7 +24,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "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"
) )

@ -25,7 +25,7 @@ import (
"github.com/spf13/cobra" "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"
) )

@ -24,7 +24,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "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"
"k8s.io/helm/pkg/plugin/installer" "k8s.io/helm/pkg/plugin/installer"
) )

@ -24,7 +24,7 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
) )
var printReleaseTemplate = `REVISION: {{.Release.Version}} var printReleaseTemplate = `REVISION: {{.Release.Version}}

@ -17,20 +17,12 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/repo"
) )
const pullDesc = ` const pullDesc = `
@ -48,20 +40,9 @@ 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. 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 { func newPullCmd(out io.Writer) *cobra.Command {
o := &pullOptions{} client := action.NewPull()
client.Out = out
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "pull [chart URL | repo/chartname] [...]", Use: "pull [chart URL | repo/chartname] [...]",
@ -70,14 +51,14 @@ func newPullCmd(out io.Writer) *cobra.Command {
Long: pullDesc, Long: pullDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { 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") 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++ { for i := 0; i < len(args); i++ {
o.chartRef = args[i] if err := client.Run(args[i]); err != nil {
if err := o.run(out); err != nil {
return err return err
} }
} }
@ -85,80 +66,7 @@ func newPullCmd(out io.Writer) *cobra.Command {
}, },
} }
f := cmd.Flags() client.AddFlags(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)
return cmd 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
}

@ -130,7 +130,7 @@ func TestPullCmd(t *testing.T) {
os.Mkdir(outdir, 0755) os.Mkdir(outdir, 0755)
cmd := strings.Join(append(tt.args, "-d", "'"+outdir+"'", "--home", "'"+hh.String()+"'"), " ") 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 err != nil {
if tt.wantError { if tt.wantError {
continue continue

@ -24,8 +24,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/release"
) )
const releaseTestDesc = ` 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. The tests to be run are defined in the chart that was installed.
` `
type releaseTestOptions struct { func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
name string client := action.NewReleaseTesting(cfg)
client helm.Interface
timeout int64
cleanup bool
}
func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &releaseTestOptions{client: c}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test [RELEASE]", Use: "test [RELEASE]",
@ -51,47 +44,35 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: releaseTestDesc, Long: releaseTestDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.name = args[0] c, errc := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) testErr := &testErr{}
return o.run(out)
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() client.AddFlags(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")
return cmd 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 { type testErr struct {
failed int failed int
} }

@ -1,66 +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 (
"testing"
"k8s.io/helm/pkg/hapi/release"
)
func TestReleaseTesting(t *testing.T) {
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: "test unknown",
cmd: "test example-unknown",
testRunStatus: map[string]release.TestRunStatus{"UNKNOWN: yellow lights everywhere": release.TestRunUnknown},
golden: "output/test-unknown.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 running",
cmd: "test example-running",
testRunStatus: map[string]release.TestRunStatus{"RUNNING: things are happpeningggg": release.TestRunRunning},
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,
}}
runTestCmd(t, tests)
}

@ -25,7 +25,7 @@ import (
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/getter" "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"
) )

@ -25,7 +25,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )

@ -25,7 +25,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )

@ -26,7 +26,7 @@ import (
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/getter" "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"
) )

@ -24,7 +24,7 @@ import (
"testing" "testing"
"k8s.io/helm/pkg/getter" "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"
"k8s.io/helm/pkg/repo/repotest" "k8s.io/helm/pkg/repo/repotest"
) )

@ -19,13 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/action"
) )
const rollbackDesc = ` const rollbackDesc = `
@ -36,20 +34,8 @@ second is a revision (version) number. To see revision numbers, run
'helm history RELEASE'. 'helm history RELEASE'.
` `
type rollbackOptions struct { func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
name string client := action.NewRollback(cfg)
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}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "rollback [RELEASE] [REVISION]", Use: "rollback [RELEASE] [REVISION]",
@ -57,45 +43,18 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: rollbackDesc, Long: rollbackDesc,
Args: require.ExactArgs(2), Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.name = args[0] _, err := client.Run(args[0])
v64, err := strconv.ParseInt(args[1], 10, 32)
if err != nil { if err != nil {
return errors.Wrapf(err, "invalid revision number '%q'", args[1]) return err
} }
o.revision = int(v64) fmt.Fprintf(out, "Rollback was a success! Happy Helming!\n")
o.client = ensureHelmClient(o.client, false)
return o.run(out) return nil
}, },
} }
f := cmd.Flags() client.AddFlags(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")
return cmd 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
}

@ -18,25 +18,47 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/release"
) )
func TestRollbackCmd(t *testing.T) { 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{{ tests := []cmdTestCase{{
name: "rollback a release", name: "rollback a release",
cmd: "rollback funny-honey 1", cmd: "rollback funny-honey 1",
golden: "output/rollback.txt", golden: "output/rollback.txt",
rels: rels,
}, { }, {
name: "rollback a release with timeout", name: "rollback a release with timeout",
cmd: "rollback funny-honey 1 --timeout 120", cmd: "rollback funny-honey 1 --timeout 120",
golden: "output/rollback-timeout.txt", golden: "output/rollback-timeout.txt",
rels: rels,
}, { }, {
name: "rollback a release with wait", name: "rollback a release with wait",
cmd: "rollback funny-honey 1 --wait", cmd: "rollback funny-honey 1 --wait",
golden: "output/rollback-wait.txt", golden: "output/rollback-wait.txt",
rels: rels,
}, { }, {
name: "rollback a release without revision", name: "rollback a release without revision",
cmd: "rollback funny-honey", cmd: "rollback funny-honey",
golden: "output/rollback-no-args.txt", golden: "output/rollback-no-args.txt",
rels: rels,
wantError: true, wantError: true,
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)

@ -24,7 +24,6 @@ import (
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/registry" "k8s.io/helm/pkg/registry"
) )
@ -50,8 +49,7 @@ Environment:
$KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config")
` `
// TODO: 'c helm.Interface' is deprecated in favor of actionConfig func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command {
func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "helm", Use: "helm",
Short: "The Helm package manager for Kubernetes.", 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), newChartCmd(actionConfig, out),
// release commands // release commands
newGetCmd(c, out), newGetCmd(actionConfig, out),
newHistoryCmd(c, out), newHistoryCmd(actionConfig, out),
newInstallCmd(actionConfig, out), newInstallCmd(actionConfig, out),
newListCmd(actionConfig, out), newListCmd(actionConfig, out),
newReleaseTestCmd(c, out), newReleaseTestCmd(actionConfig, out),
newRollbackCmd(c, out), newRollbackCmd(actionConfig, out),
newStatusCmd(c, out), newStatusCmd(actionConfig, out),
newUninstallCmd(c, out), newUninstallCmd(actionConfig, out),
newUpgradeCmd(c, out), newUpgradeCmd(actionConfig, out),
newCompletionCmd(out), newCompletionCmd(out),
newHomeCmd(out), newHomeCmd(out),

@ -77,7 +77,7 @@ func TestRootCmd(t *testing.T) {
os.Setenv(k, v) os.Setenv(k, v)
} }
cmd, _, err := executeCommandC(nil, tt.args) cmd, _, err := executeActionCommand(tt.args)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }

@ -27,7 +27,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/search" "k8s.io/helm/cmd/helm/search"
"k8s.io/helm/pkg/helm/helmpath" "k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )

@ -17,16 +17,12 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"strings"
"github.com/ghodss/yaml"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart/loader"
) )
const showDesc = ` const showDesc = `
@ -51,24 +47,8 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file 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 { func newShowCmd(out io.Writer) *cobra.Command {
o := &showOptions{output: all} client := action.NewShow(out, action.ShowAll)
showCommand := &cobra.Command{ showCommand := &cobra.Command{
Use: "show [CHART]", Use: "show [CHART]",
@ -77,12 +57,11 @@ func newShowCmd(out io.Writer) *cobra.Command {
Long: showDesc, Long: showDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { 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 { if err != nil {
return err return err
} }
o.chartpath = cp return client.Run(cp)
return o.run(out)
}, },
} }
@ -92,13 +71,12 @@ func newShowCmd(out io.Writer) *cobra.Command {
Long: showValuesDesc, Long: showValuesDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.output = valuesOnly client.OutputFormat = action.ShowValues
cp, err := o.locateChart(args[0]) cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil { if err != nil {
return err return err
} }
o.chartpath = cp return client.Run(cp)
return o.run(out)
}, },
} }
@ -108,13 +86,12 @@ func newShowCmd(out io.Writer) *cobra.Command {
Long: showChartDesc, Long: showChartDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.output = chartOnly client.OutputFormat = action.ShowChart
cp, err := o.locateChart(args[0]) cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil { if err != nil {
return err return err
} }
o.chartpath = cp return client.Run(cp)
return o.run(out)
}, },
} }
@ -124,19 +101,18 @@ func newShowCmd(out io.Writer) *cobra.Command {
Long: readmeChartDesc, Long: readmeChartDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.output = readmeOnly client.OutputFormat = action.ShowReadme
cp, err := o.locateChart(args[0]) cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil { if err != nil {
return err return err
} }
o.chartpath = cp return client.Run(cp)
return o.run(out)
}, },
} }
cmds := []*cobra.Command{showCommand, readmeSubCmd, valuesSubCmd, chartSubCmd} cmds := []*cobra.Command{showCommand, readmeSubCmd, valuesSubCmd, chartSubCmd}
for _, subCmd := range cmds { for _, subCmd := range cmds {
o.chartPathOptions.addFlags(subCmd.Flags()) client.AddFlags(subCmd.Flags())
} }
for _, subCmd := range cmds[1:] { for _, subCmd := range cmds[1:] {
@ -145,52 +121,3 @@ func newShowCmd(out io.Writer) *cobra.Command {
return showCommand 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
}

@ -18,20 +18,14 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"regexp"
"text/tabwriter"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/gosuri/uitable"
"github.com/gosuri/uitable/util/strutil"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
) )
var statusHelp = ` var statusHelp = `
@ -45,15 +39,8 @@ The status consists of:
- additional notes provided by the chart - additional notes provided by the chart
` `
type statusOptions struct { func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
release string client := action.NewStatus(cfg)
client helm.Interface
version int
outfmt string
}
func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &statusOptions{client: client}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "status RELEASE_NAME", Use: "status RELEASE_NAME",
@ -61,88 +48,42 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: statusHelp, Long: statusHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0] rel, err := client.Run(args[0])
o.client = ensureHelmClient(o.client, false) if err != nil {
return o.run(out) 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") client.AddFlags(cmd.PersistentFlags())
cmd.PersistentFlags().StringVarP(&o.outfmt, "output", "o", "", "output the status in the specified format (json or yaml)")
return cmd 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()
}

@ -20,15 +20,18 @@ import (
"testing" "testing"
"time" "time"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/release"
) )
func TestStatusCmd(t *testing.T) { func TestStatusCmd(t *testing.T) {
releasesMockWithStatus := func(info *release.Info) []*release.Release { releasesMockWithStatus := func(info *release.Info) []*release.Release {
info.LastDeployed = time.Unix(1452902400, 0).UTC() info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{ return []*release.Release{{
Name: "flummoxed-chickadee", Name: "flummoxed-chickadee",
Info: info, Info: info,
Chart: &chart.Chart{},
}} }}
} }

@ -19,39 +19,23 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"os" "io/ioutil"
"path"
"path/filepath"
"regexp"
"strings" "strings"
"time"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/storage/driver"
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)
) )
const templateDesc = ` const templateDesc = `
Render chart templates locally and display the output. 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 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) of the server-side testing of chart validity (e.g. whether an API is supported)
is done. is done.
@ -61,232 +45,38 @@ To render just one template in a chart, use '-x':
$ helm template mychart -x templates/deployment.yaml $ 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 { 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{ cmd := &cobra.Command{
Use: "template CHART", Use: "template CHART",
Short: fmt.Sprintf("locally render templates"), Short: fmt.Sprintf("locally render templates"),
Long: templateDesc, Long: templateDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
// verify chart path exists client.DryRun = true
if _, err := os.Stat(args[0]); err == nil { client.ReleaseName = "RELEASE-NAME"
if o.chartPath, err = filepath.Abs(args[0]); err != nil { client.Replace = true // Skip the name check
return err rel, err := runInstall(args, client, out)
} if err != nil {
} else {
return err return err
} }
return o.run(out) fmt.Fprintln(out, strings.TrimSpace(rel.Manifest))
return nil
}, },
} }
f := cmd.Flags() client.AddFlags(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)
return cmd 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 <data> to <output-dir>/<name>
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)
}

@ -25,10 +25,6 @@ import (
var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1" var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1"
func TestTemplateCmd(t *testing.T) { func TestTemplateCmd(t *testing.T) {
absChartPath, err := filepath.Abs(chartPath)
if err != nil {
t.Fatal(err)
}
tests := []cmdTestCase{ tests := []cmdTestCase{
{ {
name: "check name", name: "check name",
@ -37,24 +33,9 @@ func TestTemplateCmd(t *testing.T) {
}, },
{ {
name: "check set name", 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", 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", name: "check values files",
cmd: fmt.Sprintf("template '%s' --values '%s'", chartPath, filepath.Join(chartPath, "/charts/subchartA/values.yaml")), 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), cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" }}-baz'`, chartPath),
golden: "output/template-name-template.txt", 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", name: "check no args",
cmd: "template", cmd: "template",

@ -1,4 +1,3 @@
FINAL NAME: FOOBAR
NAME: FOOBAR NAME: FOOBAR
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default

@ -1,3 +1,4 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE:
STATUS: deployed STATUS: deployed

@ -1,3 +1,4 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE:
STATUS: deployed STATUS: deployed

@ -1,3 +1,4 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE:
STATUS: deployed STATUS: deployed

@ -1,3 +1,4 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE:
STATUS: deployed STATUS: deployed

@ -15,7 +15,6 @@ spec:
name: apache name: apache
selector: selector:
app: subcharta app: subcharta
--- ---
# Source: subchart1/charts/subchartb/templates/service.yaml # Source: subchart1/charts/subchartb/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -33,7 +32,6 @@ spec:
name: nginx name: nginx
selector: selector:
app: subchartb app: subchartb
--- ---
# Source: subchart1/templates/service.yaml # Source: subchart1/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -55,4 +53,3 @@ spec:
name: nginx name: nginx
selector: selector:
app: subchart1 app: subchart1

@ -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 # Source: subchart1/templates/service.yaml
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -19,4 +53,3 @@ spec:
name: apache name: apache
selector: selector:
app: subchart1 app: subchart1

@ -15,7 +15,6 @@ spec:
name: apache name: apache
selector: selector:
app: subcharta app: subcharta
--- ---
# Source: subchart1/charts/subchartb/templates/service.yaml # Source: subchart1/charts/subchartb/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -33,7 +32,6 @@ spec:
name: nginx name: nginx
selector: selector:
app: subchartb app: subchartb
--- ---
# Source: subchart1/templates/service.yaml # Source: subchart1/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -55,4 +53,3 @@ spec:
name: apache name: apache
selector: selector:
app: subchart1 app: subchart1

@ -15,7 +15,6 @@ spec:
name: apache name: apache
selector: selector:
app: subcharta app: subcharta
--- ---
# Source: subchart1/charts/subchartb/templates/service.yaml # Source: subchart1/charts/subchartb/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -33,7 +32,6 @@ spec:
name: nginx name: nginx
selector: selector:
app: subchartb app: subchartb
--- ---
# Source: subchart1/templates/service.yaml # Source: subchart1/templates/service.yaml
apiVersion: v1 apiVersion: v1
@ -55,4 +53,3 @@ spec:
name: nginx name: nginx
selector: selector:
app: subchart1 app: subchart1

@ -1,2 +0,0 @@
ERROR: yellow lights everywhere
Error: 1 test(s) failed

@ -1,2 +0,0 @@
FAILURE: red lights everywhere
Error: 1 test(s) failed

@ -1 +0,0 @@
RUNNING: things are happpeningggg

@ -1 +0,0 @@
UNKNOWN: yellow lights everywhere

@ -1 +0,0 @@
PASSED: green lights everywhere

@ -1,5 +1,11 @@
Release "crazy-bunny" has been upgraded. Happy Helming! Release "crazy-bunny" has been upgraded. Happy Helming!
NAME: crazy-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "zany-bunny" has been upgraded. Happy Helming! Release "zany-bunny" has been upgraded. Happy Helming!
NAME: zany-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming! Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming! Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming! Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "crazy-bunny" has been upgraded. Happy Helming! Release "crazy-bunny" has been upgraded. Happy Helming!
NAME: crazy-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming! Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default NAMESPACE: default
STATUS: deployed 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

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/action"
) )
const uninstallDesc = ` const uninstallDesc = `
@ -34,20 +34,8 @@ Use the '--dry-run' flag to see which releases will be uninstalled without actua
uninstalling them. uninstalling them.
` `
type uninstallOptions struct { func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
disableHooks bool // --no-hooks client := action.NewUninstall(cfg)
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}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uninstall RELEASE_NAME [...]", Use: "uninstall RELEASE_NAME [...]",
@ -57,40 +45,23 @@ func newUninstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: uninstallDesc, Long: uninstallDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.client = ensureHelmClient(o.client, false)
for i := 0; i < len(args); i++ { 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 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 return nil
}, },
} }
f := cmd.Flags() client.AddFlags(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)")
return cmd 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
}

@ -19,38 +19,34 @@ package main
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestUninstall(t *testing.T) { func TestUninstall(t *testing.T) {
rels := []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}
tests := []cmdTestCase{ tests := []cmdTestCase{
{ {
name: "basic uninstall", name: "basic uninstall",
cmd: "uninstall aeneas", cmd: "uninstall aeneas",
golden: "output/uninstall.txt", golden: "output/uninstall.txt",
rels: rels, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{ {
name: "uninstall with timeout", name: "uninstall with timeout",
cmd: "uninstall aeneas --timeout 120", cmd: "uninstall aeneas --timeout 120",
golden: "output/uninstall-timeout.txt", golden: "output/uninstall-timeout.txt",
rels: rels, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{ {
name: "uninstall without hooks", name: "uninstall without hooks",
cmd: "uninstall aeneas --no-hooks", cmd: "uninstall aeneas --no-hooks",
golden: "output/uninstall-no-hooks.txt", golden: "output/uninstall-no-hooks.txt",
rels: rels, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{ {
name: "purge", name: "purge",
cmd: "uninstall aeneas --purge", cmd: "uninstall aeneas --purge",
golden: "output/uninstall-purge.txt", golden: "output/uninstall-purge.txt",
rels: rels, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{ {
name: "uninstall without release", name: "uninstall without release",

@ -24,8 +24,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/storage/driver" "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 $ helm upgrade --set foo=bar --set foo=newbar redis ./redis
` `
type upgradeOptions struct { func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
devel bool // --devel client := action.NewUpgrade(cfg)
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}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "upgrade [RELEASE] [CHART]", Use: "upgrade [RELEASE] [CHART]",
@ -85,102 +63,79 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: upgradeDesc, Long: upgradeDesc,
Args: require.ExactArgs(2), Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { 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") debug("setting version to >0.0.0-0")
o.version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
o.release = args[0] if err := client.ValueOptions.MergeValues(settings); err != nil {
o.chart = args[1] return err
o.client = ensureHelmClient(o.client, false) }
return o.run(out) chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings)
}, if err != nil {
} return err
}
f := cmd.Flags() if client.Install {
f.BoolVar(&o.dryRun, "dry-run", false, "simulate an upgrade") // If a release does not exist, install it. If another error occurs during
f.BoolVar(&o.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") // the check, ignore the error and continue with the upgrade.
f.BoolVar(&o.force, "force", false, "force resource update through delete/recreate if needed") histClient := action.NewHistory(cfg)
f.BoolVar(&o.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks") histClient.Max = 1
f.BoolVar(&o.disableHooks, "no-hooks", false, "disable pre/post upgrade hooks") if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound {
f.BoolVarP(&o.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0])
f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") instClient := action.NewInstall(cfg)
f.BoolVar(&o.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") instClient.ChartPathOptions = client.ChartPathOptions
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.") instClient.ValueOptions = client.ValueOptions
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") instClient.DryRun = client.DryRun
f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") instClient.DisableHooks = client.DisableHooks
f.IntVar(&o.maxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") instClient.Timeout = client.Timeout
o.valuesOptions.addFlags(f) instClient.Wait = client.Wait
o.chartPathOptions.addFlags(f) 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 { resp, err := client.Run(args[0], ch)
chartPath, err := o.locateChart(o.chart) if err != nil {
if err != nil { return errors.Wrap(err, "UPGRADE FAILED")
return err }
}
if o.install { if settings.Debug {
// If a release does not exist, install it. If another error occurs during action.PrintRelease(out, resp)
// 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,
} }
return io.run(out)
}
}
rawVals, err := o.mergedValues() fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0])
if err != nil {
return err
}
// Check chart dependencies to make sure all are present in /charts // Print the status like status command does
ch, err := loader.Load(chartPath) statusClient := action.NewStatus(cfg)
if err != nil { rel, err := statusClient.Run(args[0])
return err if err != nil {
} return err
if req := ch.Metadata.Dependencies; req != nil { }
if err := checkDependencies(ch, req); err != nil { action.PrintRelease(out, rel)
return err
}
}
resp, err := o.client.UpdateRelease( return nil
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")
} }
if settings.Debug { client.AddFlags(cmd.Flags())
printRelease(out, resp)
}
fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", o.release) return cmd
PrintStatus(out, resp)
return nil
} }

@ -23,8 +23,7 @@ import (
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/helm"
) )
func TestUpgradeCmd(t *testing.T) { func TestUpgradeCmd(t *testing.T) {
@ -42,7 +41,7 @@ func TestUpgradeCmd(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Error loading chart: %v", err) t.Fatalf("Error loading chart: %v", err)
} }
_ = helm.ReleaseMock(&helm.MockReleaseOptions{ _ = release.Mock(&release.MockReleaseOptions{
Name: "funny-bunny", Name: "funny-bunny",
Chart: ch, Chart: ch,
}) })
@ -84,7 +83,7 @@ func TestUpgradeCmd(t *testing.T) {
badDepsPath := "testdata/testcharts/chart-bad-requirements" badDepsPath := "testdata/testcharts/chart-bad-requirements"
relMock := func(n string, v int, ch *chart.Chart) *release.Release { 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{ tests := []cmdTestCase{

@ -21,7 +21,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/action"
) )
const verifyDesc = ` 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. the 'helm package --sign' command.
` `
type verifyOptions struct {
keyring string
chartfile string
}
func newVerifyCmd(out io.Writer) *cobra.Command { func newVerifyCmd(out io.Writer) *cobra.Command {
o := &verifyOptions{} client := action.NewVerify()
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "verify PATH", Use: "verify PATH",
@ -49,18 +44,11 @@ func newVerifyCmd(out io.Writer) *cobra.Command {
Long: verifyDesc, Long: verifyDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
o.chartfile = args[0] return client.Run(args[0])
return o.run(out)
}, },
} }
f := cmd.Flags() client.AddFlags(cmd.Flags())
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys")
return cmd return cmd
} }
func (o *verifyOptions) run(out io.Writer) error {
_, err := downloader.VerifyChart(o.chartfile, o.keyring)
return err
}

@ -72,7 +72,7 @@ func TestVerifyCmd(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
out, err := executeCommand(nil, tt.cmd) _, out, err := executeActionCommand(tt.cmd)
if tt.wantError { if tt.wantError {
if err == nil { if err == nil {
t.Errorf("Expected error, but got none: %q", out) t.Errorf("Expected error, but got none: %q", out)

@ -10,11 +10,11 @@ Helm can be installed either from source, or from pre-built binary releases.
### From the 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 provides binary releases for a variety of OSes. These binary versions
can be manually downloaded and installed. 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`) 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 3. Find the `helm` binary in the unpacked directory, and move it to its
desired destination (`mv linux-amd64/helm /usr/local/bin/helm`) desired destination (`mv linux-amd64/helm /usr/local/bin/helm`)

@ -75,7 +75,7 @@ func compare(actual []byte, filename string) error {
return errors.Wrapf(err, "unable to read testdata %s", filename) return errors.Wrapf(err, "unable to read testdata %s", filename)
} }
if !bytes.Equal(expected, actual) { 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 return nil
} }

@ -17,15 +17,18 @@ limitations under the License.
package action package action
import ( import (
"regexp"
"time" "time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/registry" "k8s.io/helm/pkg/registry"
"k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/tiller/environment"
) )
// Timestamper is a function capable of producing a timestamp.Timestamper. // Timestamper is a function capable of producing a timestamp.Timestamper.
@ -34,6 +37,28 @@ import (
// though, so that timestamps are predictable. // though, so that timestamps are predictable.
var Timestamper = time.Now 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. // Configuration injects the dependencies that all actions share.
type Configuration struct { type Configuration struct {
// Discovery contains a discovery client // Discovery contains a discovery client
@ -43,7 +68,7 @@ type Configuration struct {
Releases *storage.Storage Releases *storage.Storage
// KubeClient is a Kubernetes API client. // KubeClient is a Kubernetes API client.
KubeClient environment.KubeClient KubeClient kube.KubernetesClient
// RegistryClient is a client for working with registries // RegistryClient is a client for working with registries
RegistryClient *registry.Client RegistryClient *registry.Client
@ -69,6 +94,18 @@ func (c *Configuration) Now() time.Time {
return Timestamper() 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 // GetVersionSet retrieves a set of available k8s API versions
func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
groups, err := client.ServerGroups() groups, err := client.ServerGroups()
@ -87,3 +124,14 @@ func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet
versions := metav1.ExtractGroupVersions(groups) versions := metav1.ExtractGroupVersions(groups)
return chartutil.NewVersionSet(versions...), nil 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)
}
}

@ -26,10 +26,10 @@ import (
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/pkg/chart" "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"
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller/environment"
) )
var verbose = flag.Bool("test.log", false, "enable test logging") var verbose = flag.Bool("test.log", false, "enable test logging")
@ -39,7 +39,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
return &Configuration{ return &Configuration{
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(), Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) { Log: func(format string, v ...interface{}) {
t.Helper() t.Helper()
@ -176,12 +176,12 @@ func namedReleaseStub(name string, status release.Status) *release.Release {
func newHookFailingKubeClient() *hookFailingKubeClient { func newHookFailingKubeClient() *hookFailingKubeClient {
return &hookFailingKubeClient{ return &hookFailingKubeClient{
PrintingKubeClient: environment.PrintingKubeClient{Out: ioutil.Discard}, PrintingKubeClient: kube.PrintingKubeClient{Out: ioutil.Discard},
} }
} }
type hookFailingKubeClient struct { type hookFailingKubeClient struct {
environment.PrintingKubeClient kube.PrintingKubeClient
} }
func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error {

@ -0,0 +1,195 @@
/*
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"
"github.com/spf13/pflag"
"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{}
}
func (d *Dependency) AddBuildFlags(f *pflag.FlagSet) {
f.BoolVar(&d.Verify, "verify", false, "verify the packages against signatures")
f.StringVar(&d.Keyring, "keyring", defaultKeyring(), "keyring containing public keys")
}
func (d *Dependency) AddUpdateFlags(f *pflag.FlagSet) {
d.AddBuildFlags(f)
f.BoolVar(&d.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
}
// 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)
}
}
}

@ -0,0 +1,48 @@
/*
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/spf13/pflag"
"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)
}
func (g *Get) AddFlags(f *pflag.FlagSet) {
f.IntVar(&g.Version, "revision", 0, "get the named release with revision")
}

@ -0,0 +1,74 @@
/*
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"
"github.com/spf13/pflag"
"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
}
func (g *GetValues) AddFlags(f *pflag.FlagSet) {
f.IntVar(&g.Version, "revision", 0, "get the named release with revision")
f.BoolVarP(&g.AllValues, "all", "a", false, "dump all (computed) values")
}

@ -0,0 +1,186 @@
/*
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"
"github.com/spf13/pflag"
"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 (h *History) AddFlags(f *pflag.FlagSet) {
f.StringVarP(&h.OutputFormat, "output", "o", Table.String(), "prints the output in the specified format (json|table|yaml)")
f.IntVar(&h.Max, "max", 256, "maximum number of revision to include in history")
}
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)
}

@ -20,19 +20,33 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/url"
"os"
"path" "path"
"path/filepath"
"sort" "sort"
"strings" "strings"
"text/template"
"time" "time"
"github.com/Masterminds/sprig"
"github.com/ghodss/yaml"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/pflag"
"k8s.io/client-go/util/homedir"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/cli"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/engine" "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/hooks"
"k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/strvals"
"k8s.io/helm/pkg/version" "k8s.io/helm/pkg/version"
) )
@ -53,15 +67,39 @@ const notesFileSuffix = "NOTES.txt"
type Install struct { type Install struct {
cfg *Configuration cfg *Configuration
DryRun bool ChartPathOptions
DisableHooks bool ValueOptions
Replace bool
Wait bool DryRun bool
Devel bool DisableHooks bool
DepUp bool Replace bool
Timeout int64 Wait bool
Namespace string Devel bool
ReleaseName string 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. // NewInstall creates a new Install object with the given configuration.
@ -74,7 +112,7 @@ func NewInstall(cfg *Configuration) *Install {
// Run executes the installation // Run executes the installation
// //
// If DryRun is set to true, this will prepare the release, but not install it // 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 { if err := i.availableName(); err != nil {
return nil, err return nil, err
} }
@ -85,12 +123,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel
Name: i.ReleaseName, Name: i.ReleaseName,
IsInstall: true, IsInstall: true,
} }
valuesToRender, err := chartutil.ToRenderValues(chrt, rawValues, options, caps) valuesToRender, err := chartutil.ToRenderValues(chrt, i.rawValues, options, caps)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rel := i.createRelease(chrt, rawValues) rel := i.createRelease(chrt, i.rawValues)
var manifestDoc *bytes.Buffer var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.renderResources(chrt, valuesToRender, caps.APIVersions) rel.Hooks, manifestDoc, rel.Info.Notes, err = i.renderResources(chrt, valuesToRender, caps.APIVersions)
// Even for errors, attach this if available // Even for errors, attach this if available
@ -100,12 +138,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel
// Check error from render // Check error from render
if err != nil { if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) 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 return rel, err
} }
// Mark this release as in-progress // 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 { if err := i.validateManifest(manifestDoc); err != nil {
return rel, err return rel, err
} }
@ -257,20 +295,20 @@ func (i *Install) replaceRelease(rel *release.Release) error {
// renderResources renders the templates in a chart // 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) { func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
hooks := []*release.Hook{} hooks := []*release.Hook{}
buf := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
if ch.Metadata.KubeVersion != "" { if ch.Metadata.KubeVersion != "" {
cap, _ := values["Capabilities"].(*chartutil.Capabilities) cap, _ := values["Capabilities"].(*chartutil.Capabilities)
gitVersion := cap.KubeVersion.String() gitVersion := cap.KubeVersion.String()
k8sVersion := strings.Split(gitVersion, "+")[0] k8sVersion := strings.Split(gitVersion, "+")[0]
if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { 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) files, err := engine.Render(ch, values)
if err != nil { 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, // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
@ -301,22 +339,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 // We return the files as a big blob of data to help the user debug parser
// errors. // errors.
b := bytes.NewBuffer(nil)
for name, content := range files { for name, content := range files {
if len(strings.TrimSpace(content)) == 0 { if len(strings.TrimSpace(content)) == 0 {
continue continue
} }
b.WriteString("\n---\n# Source: " + name + "\n") fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
b.WriteString(content)
} }
return hooks, b, "", err return hooks, b, "", err
} }
// Aggregate all valid manifests into one big doc. // Aggregate all valid manifests into one big doc.
b := bytes.NewBuffer(nil)
for _, m := range manifests { for _, m := range manifests {
b.WriteString("\n---\n# Source: " + m.Name + "\n") fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
b.WriteString(m.Content)
} }
return hooks, b, notes, nil return hooks, b, notes, nil
@ -346,7 +380,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error {
sort.Sort(hookByWeight(executingHooks)) sort.Sort(hookByWeight(executingHooks))
for _, h := range 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 return err
} }
@ -360,7 +394,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error {
if err := i.cfg.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil { 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 // 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 // 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
} }
return err return err
@ -370,7 +404,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 // 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 // under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks { 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 return err
} }
h.LastRun = time.Now() h.LastRun = time.Now()
@ -379,17 +413,6 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error {
return nil 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 // deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
// FIXME: Can we refactor this out? // FIXME: Can we refactor this out?
var deletePolices = map[string]release.HookDeletePolicy{ var deletePolices = map[string]release.HookDeletePolicy{
@ -424,3 +447,281 @@ func (x hookByWeight) Less(i, j int) bool {
} }
return x[i].Weight < x[j].Weight 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 (i *Install) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&i.DryRun, "dry-run", false, "simulate an install")
f.BoolVar(&i.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&i.Replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production")
f.Int64Var(&i.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&i.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(&i.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)")
f.StringVar(&i.NameTemplate, "name-template", "", "specify template used to name the release")
f.BoolVar(&i.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&i.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart")
i.ValueOptions.AddFlags(f)
i.ChartPathOptions.AddFlags(f)
}
func (v *ValueOptions) AddFlags(f *pflag.FlagSet) {
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)")
}
func (c *ChartPathOptions) AddFlags(f *pflag.FlagSet) {
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")
}
// 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 filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
}
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, &currentMap); 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
}

@ -18,13 +18,21 @@ package action
import ( import (
"fmt" "fmt"
"reflect"
"regexp"
"testing" "testing"
"github.com/stretchr/testify/assert" "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 { func installAction(t *testing.T) *Install {
config := actionConfigFixture(t) config := actionConfigFixture(t)
instAction := NewInstall(config) instAction := NewInstall(config)
@ -34,12 +42,11 @@ func installAction(t *testing.T) *Install {
return instAction return instAction
} }
var mockEmptyVals = func() map[string]interface{} { return map[string]interface{}{} }
func TestInstallRelease(t *testing.T) { func TestInstallRelease(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
res, err := instAction.Run(buildChart(), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -63,7 +70,8 @@ func TestInstallRelease(t *testing.T) {
func TestInstallRelease_NoName(t *testing.T) { func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "" instAction.ReleaseName = ""
_, err := instAction.Run(buildChart(), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
_, err := instAction.Run(buildChart())
if err == nil { if err == nil {
t.Fatal("expected failure when no name is specified") t.Fatal("expected failure when no name is specified")
} }
@ -74,7 +82,8 @@ func TestInstallRelease_WithNotes(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" 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 { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -100,7 +109,8 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" 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 { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -118,11 +128,8 @@ func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart( instAction.rawValues = map[string]interface{}{}
withNotes("parent"), res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))))
withDependency(withNotes("child"))),
mockEmptyVals(),
)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -138,7 +145,8 @@ func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = true instAction.DryRun = true
res, err := instAction.Run(buildChart(withSampleTemplates()), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()))
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -163,7 +171,8 @@ func TestInstallRelease_NoHooks(t *testing.T) {
instAction.ReleaseName = "no-hooks" instAction.ReleaseName = "no-hooks"
instAction.cfg.Releases.Create(releaseStub()) instAction.cfg.Releases.Create(releaseStub())
res, err := instAction.Run(buildChart(), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
@ -177,7 +186,8 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
instAction.ReleaseName = "failed-hooks" instAction.ReleaseName = "failed-hooks"
instAction.cfg.KubeClient = newHookFailingKubeClient() instAction.cfg.KubeClient = newHookFailingKubeClient()
res, err := instAction.Run(buildChart(), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.Error(err) is.Error(err)
is.Contains(res.Info.Description, "failed post-install") is.Contains(res.Info.Description, "failed post-install")
is.Equal(res.Info.Status, release.StatusFailed) is.Equal(res.Info.Status, release.StatusFailed)
@ -193,7 +203,8 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
instAction.cfg.Releases.Create(rel) instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name instAction.ReleaseName = rel.Name
res, err := instAction.Run(buildChart(), mockEmptyVals()) instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.NoError(err) is.NoError(err)
// This should have been auto-incremented // This should have been auto-incremented
@ -208,12 +219,138 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
func TestInstallRelease_KubeVersion(t *testing.T) { func TestInstallRelease_KubeVersion(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(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) is.NoError(err)
// This should fail for a few hundred years // This should fail for a few hundred years
instAction.ReleaseName = "should-fail" 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.Error(err)
is.Contains(err.Error(), "chart requires kubernetesVersion") 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)
}
}

@ -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 (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"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
}
func (l *Lint) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&l.Strict, "strict", false, "fail on lint warnings")
l.ValueOptions.AddFlags(f)
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package action
import ( import (
"testing" "testing"
@ -24,11 +24,11 @@ var (
values = make(map[string]interface{}) values = make(map[string]interface{})
namespace = "testNamespace" namespace = "testNamespace"
strict = false strict = false
archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz" archivedChartPath = "../../cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz"
archivedChartPathWithHyphens = "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz" archivedChartPathWithHyphens = "../../cmd/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz"
invalidArchivedChartPath = "testdata/testcharts/invalidcompressedchart0.1.0.tgz" invalidArchivedChartPath = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz"
chartDirPath = "testdata/testcharts/decompressedchart/" chartDirPath = "../../cmd/helm/testdata/testcharts/decompressedchart/"
chartMissingManifest = "testdata/testcharts/chart-missing-manifest" chartMissingManifest = "../../cmd/helm/testdata/testcharts/chart-missing-manifest"
) )
func TestLintChart(t *testing.T) { func TestLintChart(t *testing.T) {

@ -17,9 +17,13 @@ limitations under the License.
package action package action
import ( import (
"fmt"
"regexp" "regexp"
"k8s.io/helm/pkg/hapi/release" "github.com/gosuri/uitable"
"github.com/spf13/pflag"
"k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/releaseutil"
) )
@ -96,6 +100,8 @@ const (
// //
// It provides, for example, the implementation of 'helm list'. // It provides, for example, the implementation of 'helm list'.
type List struct { type List struct {
cfg *Configuration
// All ignores the limit/offset // All ignores the limit/offset
All bool All bool
// AllNamespaces searches across namespaces // AllNamespaces searches across namespaces
@ -112,9 +118,16 @@ type List struct {
// Offset is the starting index for the Run() call // Offset is the starting index for the Run() call
Offset int Offset int
// Filter is a filter that is applied to the results // Filter is a filter that is applied to the results
Filter string Filter string
Short bool
cfg *Configuration ByDate bool
SortDesc bool
Uninstalled bool
Superseded bool
Uninstalling bool
Deployed bool
Failed bool
Pending bool
} }
// NewList constructs a new *List // NewList constructs a new *List
@ -125,21 +138,42 @@ func NewList(cfg *Configuration) *List {
} }
} }
func (l *List) AddFlags(f *pflag.FlagSet) {
f.BoolVarP(&l.Short, "short", "q", false, "output short (quiet) listing format")
f.BoolVarP(&l.ByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&l.SortDesc, "reverse", "r", false, "reverse the sort order")
f.BoolVarP(&l.All, "all", "a", false, "show all releases, not just the ones marked deployed")
f.BoolVar(&l.Uninstalled, "uninstalled", false, "show uninstalled releases")
f.BoolVar(&l.Superseded, "superseded", false, "show superseded releases")
f.BoolVar(&l.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled")
f.BoolVar(&l.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
f.BoolVar(&l.Failed, "failed", false, "show failed releases")
f.BoolVar(&l.Pending, "pending", false, "show pending releases")
f.BoolVar(&l.AllNamespaces, "all-namespaces", false, "list releases across all namespaces")
f.IntVarP(&l.Limit, "max", "m", 256, "maximum number of releases to fetch")
f.IntVarP(&l.Offset, "offset", "o", 0, "next release name in the list, used to offset from start value")
f.StringVarP(&l.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results")
}
func (l *List) SetConfiguration(cfg *Configuration) {
l.cfg = cfg
}
// Run executes the list command, returning a set of matches. // 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 var filter *regexp.Regexp
if a.Filter != "" { if l.Filter != "" {
var err error var err error
filter, err = regexp.Compile(a.Filter) filter, err = regexp.Compile(l.Filter)
if err != nil { if err != nil {
return nil, err 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 // Skip anything that the mask doesn't cover
currentStatus := a.StateMask.FromName(rel.Info.Status.String()) currentStatus := l.StateMask.FromName(rel.Info.Status.String())
if a.StateMask&currentStatus == 0 { if l.StateMask&currentStatus == 0 {
return false return false
} }
@ -155,30 +189,30 @@ func (a *List) Run() ([]*release.Release, error) {
} }
// Unfortunately, we have to sort before truncating, which can incur substantial overhead // Unfortunately, we have to sort before truncating, which can incur substantial overhead
a.sort(results) l.sort(results)
// Guard on offset // Guard on offset
if a.Offset >= len(results) { if l.Offset >= len(results) {
return []*release.Release{}, nil return []*release.Release{}, nil
} }
// Calculate the limit and offset, and then truncate results if necessary. // Calculate the limit and offset, and then truncate results if necessary.
limit := len(results) limit := len(results)
if a.Limit > 0 && a.Limit < limit { if l.Limit > 0 && l.Limit < limit {
limit = a.Limit limit = l.Limit
} }
last := a.Offset + limit last := l.Offset + limit
if l := len(results); l < last { if l := len(results); l < last {
last = l last = l
} }
results = results[a.Offset:last] results = results[l.Offset:last]
return results, err return results, err
} }
// sort is an in-place sort where order is based on the value of a.Sort // sort is an in-place sort where order is based on the value of a.Sort
func (a *List) sort(rels []*release.Release) { func (l *List) sort(rels []*release.Release) {
switch a.Sort { switch l.Sort {
case ByDate: case ByDate:
releaseutil.SortByDate(rels) releaseutil.SortByDate(rels)
case ByNameDesc: case ByNameDesc:
@ -187,3 +221,53 @@ func (a *List) sort(rels []*release.Release) {
releaseutil.SortByName(rels) 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()
}

@ -21,7 +21,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/helm/pkg/hapi/release" "k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
) )

@ -0,0 +1,162 @@
/*
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"
"github.com/spf13/pflag"
"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 (p *Package) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&p.Sign, "sign", false, "use a PGP private key to sign this package")
f.StringVar(&p.Key, "key", "", "name of the key to use when signing. Used if --sign is true")
f.StringVar(&p.Keyring, "keyring", defaultKeyring(), "location of a public keyring")
f.StringVar(&p.Version, "version", "", "set the version on the chart to this semver version")
f.StringVar(&p.AppVersion, "app-version", "", "set the appVersion on the chart to this version")
f.StringVarP(&p.Destination, "destination", "d", ".", "location to write the chart.")
f.BoolVarP(&p.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`)
p.ValueOptions.AddFlags(f)
}
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
}

@ -14,27 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package tiller package action
import ( import (
"testing" "testing"
"k8s.io/helm/pkg/hapi" "k8s.io/helm/pkg/chart"
) )
func TestGetReleaseContent(t *testing.T) { func TestSetVersion(t *testing.T) {
rs := rsFixture(t) c := &chart.Chart{
rel := releaseStub() Metadata: &chart.Metadata{
if err := rs.Releases.Create(rel); err != nil { Name: "prow",
t.Fatalf("Could not store mock release: %s", err) 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 c.Metadata.Version != expect {
if err != nil { t.Errorf("Expected %q, got %q", expect, c.Metadata.Version)
t.Errorf("Error getting release content: %s", err)
} }
if res.Chart.Name() != rel.Chart.Name() { if err := setVersion(c, "monkeyface"); err == nil {
t.Errorf("Expected %q, got %q", rel.Chart.Name(), res.Chart.Name()) t.Error("Expected bogus version to return an error.")
} }
} }

@ -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()
}

@ -0,0 +1,131 @@
/*
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"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"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
Out io.Writer // TODO: refactor this out of pkg/action
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) error {
c := downloader.ChartDownloader{
HelmHome: p.Settings.Home,
Out: p.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 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 err
}
chartRef = chartURL
}
saved, v, err := c.DownloadTo(chartRef, p.Version, dest)
if err != nil {
return err
}
if p.Verify {
fmt.Fprintf(p.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 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
}
func (p *Pull) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&p.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&p.Untar, "untar", false, "if set to true, will untar the chart after downloading it")
f.BoolVar(&p.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification")
f.StringVar(&p.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded")
f.StringVarP(&p.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this")
p.ChartPathOptions.AddFlags(f)
}

@ -0,0 +1,98 @@
/*
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/pkg/errors"
"github.com/spf13/pflag"
"k8s.io/helm/pkg/release"
reltesting "k8s.io/helm/pkg/releasetesting"
)
// 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,
}
}
func (r *ReleaseTesting) AddFlags(f *pflag.FlagSet) {
f.Int64Var(&r.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&r.Cleanup, "cleanup", false, "delete test pods upon completion")
}
// 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(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 := r.cfg.Releases.Last(name)
if err != nil {
errc <- err
return nil, errc
}
ch := make(chan *release.TestReleaseResponse, 1)
testEnv := &reltesting.Environment{
Namespace: rel.Namespace,
KubeClient: r.cfg.KubeClient,
Timeout: r.Timeout,
Messages: ch,
}
r.cfg.Log("running tests for release %s", rel.Name)
tSuite := reltesting.NewTestSuite(rel)
go func() {
defer close(errc)
defer close(ch)
if err := tSuite.Run(testEnv); err != nil {
errc <- errors.Wrapf(err, "error running test suite for %s", rel.Name)
return
}
rel.Info.LastTestSuiteRun = &release.TestSuite{
StartedAt: tSuite.StartedAt,
CompletedAt: tSuite.CompletedAt,
Results: tSuite.Results,
}
if r.Cleanup {
testEnv.DeleteTestPods(tSuite.TestManifests)
}
if err := r.cfg.Releases.Update(rel); err != nil {
r.cfg.Log("test: Failed to store updated release: %s", err)
}
}()
return ch, errc
}

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package tiller package action
import ( import (
"bytes" "bytes"
"strings" "strings"
"k8s.io/helm/pkg/kube" "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 // resourcePolicyAnno is the annotation name for a resource policy
@ -33,9 +33,9 @@ const resourcePolicyAnno = "helm.sh/resource-policy"
// during an uninstallRelease action. // during an uninstallRelease action.
const keepPolicy = "keep" const keepPolicy = "keep"
func filterManifestsToKeep(manifests []Manifest) ([]Manifest, []Manifest) { func filterManifestsToKeep(manifests []releaseutil.Manifest) ([]releaseutil.Manifest, []releaseutil.Manifest) {
remaining := []Manifest{} remaining := []releaseutil.Manifest{}
keep := []Manifest{} keep := []releaseutil.Manifest{}
for _, m := range manifests { for _, m := range manifests {
if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 { 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 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 var message string
for _, m := range manifests { for _, m := range manifests {
// check if m is in fact present from k8s client's POV. // check if m is in fact present from k8s client's POV.

@ -0,0 +1,255 @@
/*
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"
"github.com/spf13/pflag"
"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,
}
}
func (r *Rollback) AddFlags(f *pflag.FlagSet) {
f.IntVarP(&r.Version, "version", "v", 0, "revision number to rollback to (default: rollback to previous release)")
f.BoolVar(&r.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&r.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.BoolVar(&r.Force, "force", false, "force resource update through delete/recreate if needed")
f.BoolVar(&r.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.Int64Var(&r.Timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&r.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")
}
// 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
}

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

Loading…
Cancel
Save