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"
// Run a create
if _, err := executeCommand(nil, "create "+cname); err != nil {
if _, _, err := executeActionCommand("create " + cname); err != nil {
t.Errorf("Failed to run create: %s", err)
return
}
@ -86,7 +86,7 @@ func TestCreateStarterCmd(t *testing.T) {
defer testChdir(t, tdir)()
// Run a create
if _, err := executeCommand(nil, fmt.Sprintf("--home='%s' create --starter=starterchart %s", hh.String(), cname)); err != nil {
if _, _, err := executeActionCommand(fmt.Sprintf("--home='%s' create --starter=starterchart %s", hh.String(), cname)); err != nil {
t.Errorf("Failed to run create: %s", err)
return
}

@ -16,18 +16,13 @@ limitations under the License.
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/Masterminds/semver"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/action"
)
const dependencyDesc = `
@ -103,14 +98,8 @@ func newDependencyCmd(out io.Writer) *cobra.Command {
return cmd
}
type dependencyLisOptions struct {
chartpath string
}
func newDependencyListCmd(out io.Writer) *cobra.Command {
o := &dependencyLisOptions{
chartpath: ".",
}
client := action.NewDependency()
cmd := &cobra.Command{
Use: "list CHART",
@ -119,151 +108,12 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
Long: dependencyListDesc,
Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 {
o.chartpath = filepath.Clean(args[0])
chartpath = filepath.Clean(args[0])
}
return o.run(out)
return client.List(chartpath, out)
},
}
return cmd
}
func (o *dependencyLisOptions) run(out io.Writer) error {
c, err := loader.Load(o.chartpath)
if err != nil {
return err
}
if c.Metadata.Dependencies == nil {
fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(o.chartpath, "charts"))
return nil
}
o.printDependencies(out, c.Metadata.Dependencies)
fmt.Fprintln(out)
o.printMissing(out, c.Metadata.Dependencies)
return nil
}
func (o *dependencyLisOptions) dependencyStatus(dep *chart.Dependency) string {
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
archives, err := filepath.Glob(filepath.Join(o.chartpath, "charts", filename))
if err != nil {
return "bad pattern"
} else if len(archives) > 1 {
return "too many matches"
} else if len(archives) == 1 {
archive := archives[0]
if _, err := os.Stat(archive); err == nil {
c, err := loader.Load(archive)
if err != nil {
return "corrupt"
}
if c.Name() != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return "invalid version"
}
v, err := semver.NewVersion(c.Metadata.Version)
if err != nil {
return "invalid version"
}
if constraint.Check(v) {
return "ok"
}
return "wrong version"
}
return "ok"
}
}
folder := filepath.Join(o.chartpath, "charts", dep.Name)
if fi, err := os.Stat(folder); err != nil {
return "missing"
} else if !fi.IsDir() {
return "mispackaged"
}
c, err := loader.Load(folder)
if err != nil {
return "corrupt"
}
if c.Name() != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return "invalid version"
}
v, err := semver.NewVersion(c.Metadata.Version)
if err != nil {
return "invalid version"
}
if constraint.Check(v) {
return "unpacked"
}
return "wrong version"
}
return "unpacked"
}
// printDependencies prints all of the dependencies in the yaml file.
func (o *dependencyLisOptions) printDependencies(out io.Writer, reqs []*chart.Dependency) {
table := uitable.New()
table.MaxColWidth = 80
table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS")
for _, row := range reqs {
table.AddRow(row.Name, row.Version, row.Repository, o.dependencyStatus(row))
}
fmt.Fprintln(out, table)
}
// printMissing prints warnings about charts that are present on disk, but are
// not in Charts.yaml.
func (o *dependencyLisOptions) printMissing(out io.Writer, reqs []*chart.Dependency) {
folder := filepath.Join(o.chartpath, "charts/*")
files, err := filepath.Glob(folder)
if err != nil {
fmt.Fprintln(out, err)
return
}
for _, f := range files {
fi, err := os.Stat(f)
if err != nil {
fmt.Fprintf(out, "Warning: %s\n", err)
}
// Skip anything that is not a directory and not a tgz file.
if !fi.IsDir() && filepath.Ext(f) != ".tgz" {
continue
}
c, err := loader.Load(f)
if err != nil {
fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f)
continue
}
found := false
for _, d := range reqs {
if d.Name == c.Name() {
found = true
break
}
}
if !found {
fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f)
}
}
}

@ -17,10 +17,12 @@ package main
import (
"io"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
)
@ -36,17 +38,8 @@ If no lock file is found, 'helm dependency build' will mirror the behavior
of 'helm dependency update'.
`
type dependencyBuildOptions struct {
keyring string // --keyring
verify bool // --verify
chartpath string
}
func newDependencyBuildCmd(out io.Writer) *cobra.Command {
o := &dependencyBuildOptions{
chartpath: ".",
}
client := action.NewDependency()
cmd := &cobra.Command{
Use: "build CHART",
@ -54,31 +47,28 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
Long: dependencyBuildDesc,
Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 {
o.chartpath = args[0]
chartpath = filepath.Clean(args[0])
}
man := &downloader.Manager{
Out: out,
ChartPath: chartpath,
HelmHome: settings.Home,
Keyring: client.Keyring,
Getters: getter.All(settings),
}
if client.Verify {
man.Verify = downloader.VerifyIfPossible
}
return o.run(out)
if settings.Debug {
man.Debug = true
}
return man.Build()
},
}
f := cmd.Flags()
f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures")
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys")
client.AddBuildFlags(cmd.Flags())
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))
out, err := executeCommand(nil, cmd)
_, out, err := executeActionCommand(cmd)
// In the first pass, we basically want the same results as an update.
if err != nil {
@ -72,7 +72,7 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Fatal(err)
}
out, err = executeCommand(nil, cmd)
_, out, err = executeActionCommand(cmd)
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)

@ -22,9 +22,9 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
)
const dependencyUpDesc = `
@ -42,23 +42,9 @@ reason, an update command will not remove charts unless they are (a) present
in the Chart.yaml file, but (b) at the wrong version.
`
// dependencyUpdateOptions describes a 'helm dependency update'
type dependencyUpdateOptions struct {
keyring string // --keyring
skipRefresh bool // --skip-refresh
verify bool // --verify
// args
chartpath string
helmhome helmpath.Home
}
// newDependencyUpdateCmd creates a new dependency update command.
func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
o := &dependencyUpdateOptions{
chartpath: ".",
}
client := action.NewDependency()
cmd := &cobra.Command{
Use: "update CHART",
@ -67,37 +53,29 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
Long: dependencyUpDesc,
Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chartpath := "."
if len(args) > 0 {
o.chartpath = filepath.Clean(args[0])
chartpath = filepath.Clean(args[0])
}
man := &downloader.Manager{
Out: out,
ChartPath: chartpath,
HelmHome: settings.Home,
Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings),
}
if client.Verify {
man.Verify = downloader.VerifyAlways
}
o.helmhome = settings.Home
return o.run(out)
if settings.Debug {
man.Debug = true
}
return man.Update()
},
}
f := cmd.Flags()
f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures")
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys")
f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
client.AddUpdateFlags(cmd.Flags())
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
import (
"bytes"
"fmt"
"io/ioutil"
"os"
@ -52,7 +51,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err)
}
out, err := executeCommand(nil, fmt.Sprintf("--home='%s' dependency update '%s'", hh.String(), hh.Path(chartname)))
_, out, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update '%s'", hh.String(), hh.Path(chartname)))
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
@ -95,7 +94,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err)
}
out, err = executeCommand(nil, fmt.Sprintf("--home='%s' dependency update '%s'", hh, hh.Path(chartname)))
_, out, err = executeActionCommand(fmt.Sprintf("--home='%s' dependency update '%s'", hh, hh.Path(chartname)))
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
@ -133,7 +132,7 @@ func TestDependencyUpdateCmd_SkipRefresh(t *testing.T) {
t.Fatal(err)
}
out, err := executeCommand(nil, fmt.Sprintf("--home='%s' dependency update --skip-refresh %s", hh, hh.Path(chartname)))
_, out, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update --skip-refresh %s", hh, hh.Path(chartname)))
if err == nil {
t.Fatal("Expected failure to find the repo with skipRefresh")
}
@ -164,13 +163,8 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
t.Fatal(err)
}
out := bytes.NewBuffer(nil)
o := &dependencyUpdateOptions{}
o.helmhome = hh
o.chartpath = hh.Path(chartname)
if err := o.run(out); err != nil {
output := out.String()
_, output, err := executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname)))
if err != nil {
t.Logf("Output: %s", output)
t.Fatal(err)
}
@ -178,14 +172,14 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
// Chart repo is down
srv.Stop()
if err := o.run(out); err == nil {
output := out.String()
_, output, err = executeActionCommand(fmt.Sprintf("--home='%s' dependency update %s", hh, hh.Path(chartname)))
if err == nil {
t.Logf("Output: %s", output)
t.Fatal("Expected error, got nil")
}
// Make sure charts dir still has dependencies
files, err := ioutil.ReadDir(filepath.Join(o.chartpath, "charts"))
files, err := ioutil.ReadDir(filepath.Join(hh.Path(chartname), "charts"))
if err != nil {
t.Fatal(err)
}
@ -201,7 +195,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
}
// Make sure tmpcharts is deleted
if _, err := os.Stat(filepath.Join(o.chartpath, "tmpcharts")); !os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(hh.Path(chartname), "tmpcharts")); !os.IsNotExist(err) {
t.Fatalf("tmpcharts dir still exists")
}
}

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

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

@ -19,8 +19,7 @@ package main
import (
"testing"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/release"
)
func TestGetHooks(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetHooks(t *testing.T) {
name: "get hooks with release",
cmd: "get hooks aeneas",
golden: "output/get-hooks.txt",
rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})},
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, {
name: "get hooks without args",
cmd: "get hooks",

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

@ -19,8 +19,7 @@ package main
import (
"testing"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/release"
)
func TestGetManifest(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetManifest(t *testing.T) {
name: "get manifest with release",
cmd: "get manifest juno",
golden: "output/get-manifest.txt",
rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"})},
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "juno"})},
}, {
name: "get manifest without args",
cmd: "get manifest",

@ -19,8 +19,7 @@ package main
import (
"testing"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/release"
)
func TestGetCmd(t *testing.T) {
@ -28,7 +27,7 @@ func TestGetCmd(t *testing.T) {
name: "get with a release",
cmd: "get thomas-guide",
golden: "output/get-release.txt",
rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})},
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})},
}, {
name: "get requires release name arg",
cmd: "get",

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

@ -27,15 +27,14 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/helm/environment"
"k8s.io/helm/pkg/cli"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
)
var (
settings environment.EnvSettings
settings cli.EnvSettings
config genericclioptions.RESTClientGetter
configOnce sync.Once
)
@ -52,45 +51,13 @@ func logf(format string, v ...interface{}) {
}
func main() {
cmd := newRootCmd(nil, newActionConfig(false), os.Stdout, os.Args[1:])
cmd := newRootCmd(newActionConfig(false), os.Stdout, os.Args[1:])
if err := cmd.Execute(); err != nil {
logf("%+v", err)
os.Exit(1)
}
}
// ensureHelmClient returns a new helm client impl. if h is not nil.
func ensureHelmClient(h helm.Interface, allNamespaces bool) helm.Interface {
if h != nil {
return h
}
return newClient(allNamespaces)
}
func newClient(allNamespaces bool) helm.Interface {
kc := kube.New(kubeConfig())
kc.Log = logf
clientset, err := kc.KubernetesClientSet()
if err != nil {
// TODO return error
log.Fatal(err)
}
var namespace string
if !allNamespaces {
namespace = getNamespace()
}
// TODO add other backends
d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
d.Log = logf
return helm.NewClient(
helm.KubeClient(kc),
helm.Driver(d),
helm.Discovery(clientset.Discovery()),
)
}
func newActionConfig(allNamespaces bool) *action.Configuration {
kc := kube.New(kubeConfig())
kc.Log = logf

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

@ -17,31 +17,15 @@ limitations under the License.
package main
import (
"encoding/json"
"fmt"
"io"
"github.com/ghodss/yaml"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/action"
)
type releaseInfo struct {
Revision int `json:"revision"`
Updated string `json:"updated"`
Status string `json:"status"`
Chart string `json:"chart"`
Description string `json:"description"`
}
type releaseHistory []releaseInfo
var historyHelp = `
History prints historical revisions for a given release.
@ -58,18 +42,8 @@ The historical release set is printed as a formatted table, e.g:
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 Upgraded successfully
`
type historyOptions struct {
colWidth uint // --col-width
max int // --max
outputFormat string // --output
release string
client helm.Interface
}
func newHistoryCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &historyOptions{client: c}
func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewHistory(cfg)
cmd := &cobra.Command{
Use: "history RELEASE_NAME",
@ -78,94 +52,16 @@ func newHistoryCmd(c helm.Interface, out io.Writer) *cobra.Command {
Aliases: []string{"hist"},
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
o.client = ensureHelmClient(o.client, false)
o.release = args[0]
return o.run(out)
history, err := client.Run(args[0])
if err != nil {
return err
}
fmt.Fprintln(out, history)
return nil
},
}
f := cmd.Flags()
f.IntVar(&o.max, "max", 256, "maximum number of revision to include in history")
f.UintVar(&o.colWidth, "col-width", 60, "specifies the max column width of output")
f.StringVarP(&o.outputFormat, "output", "o", "table", "prints the output in the specified format (json|table|yaml)")
client.AddFlags(cmd.Flags())
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 (
"testing"
rpb "k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/release"
)
func TestHistoryCmd(t *testing.T) {
mk := func(name string, vers int, status rpb.Status) *rpb.Release {
return helm.ReleaseMock(&helm.MockReleaseOptions{
mk := func(name string, vers int, status release.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
Status: status,
@ -35,35 +34,35 @@ func TestHistoryCmd(t *testing.T) {
tests := []cmdTestCase{{
name: "get history for release",
cmd: "history angry-bird",
rels: []*rpb.Release{
mk("angry-bird", 4, rpb.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded),
mk("angry-bird", 2, rpb.StatusSuperseded),
mk("angry-bird", 1, rpb.StatusSuperseded),
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 2, release.StatusSuperseded),
mk("angry-bird", 1, release.StatusSuperseded),
},
golden: "output/history.txt",
}, {
name: "get history with max limit set",
cmd: "history angry-bird --max 2",
rels: []*rpb.Release{
mk("angry-bird", 4, rpb.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded),
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
},
golden: "output/history-limit.txt",
}, {
name: "get history with yaml output format",
cmd: "history angry-bird --output yaml",
rels: []*rpb.Release{
mk("angry-bird", 4, rpb.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded),
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
},
golden: "output/history.yaml",
}, {
name: "get history with json output format",
cmd: "history angry-bird --output json",
rels: []*rpb.Release{
mk("angry-bird", 4, rpb.StatusDeployed),
mk("angry-bird", 3, rpb.StatusSuperseded),
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
},
golden: "output/history.json",
}}

@ -29,7 +29,7 @@ import (
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/plugin"
"k8s.io/helm/pkg/plugin/installer"
"k8s.io/helm/pkg/repo"

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

@ -17,29 +17,18 @@ limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"text/tabwriter"
"text/template"
"time"
"github.com/Masterminds/sprig"
"github.com/pkg/errors"
"k8s.io/helm/pkg/release"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
)
const installDesc = `
@ -104,268 +93,87 @@ To see the list of chart repositories, use 'helm repo list'. To search for
charts in a repository, use 'helm search'.
`
type installOptions struct {
name string // arg 0
dryRun bool // --dry-run
disableHooks bool // --disable-hooks
replace bool // --replace
nameTemplate string // --name-template
timeout int64 // --timeout
wait bool // --wait
devel bool // --devel
depUp bool // --dep-up
chartPath string // arg 1
generateName bool // --generate-name
valuesOptions
chartPathOptions
cfg *action.Configuration
// LEGACY: Here until we get upgrade converted
client helm.Interface
}
func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &installOptions{cfg: cfg}
client := action.NewInstall(cfg)
cmd := &cobra.Command{
Use: "install [NAME] [CHART]",
Short: "install a chart",
Long: installDesc,
Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
debug("Original chart version: %q", o.version)
if o.version == "" && o.devel {
debug("setting version to >0.0.0-0")
o.version = ">0.0.0-0"
}
name, chart, err := o.nameAndChart(args)
if err != nil {
return err
}
o.name = name // FIXME
cp, err := o.locateChart(chart)
RunE: func(_ *cobra.Command, args []string) error {
rel, err := runInstall(args, client, out)
if err != nil {
return err
}
o.chartPath = cp
return o.run(out)
action.PrintRelease(out, rel)
return nil
},
}
f := cmd.Flags()
f.BoolVarP(&o.generateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)")
f.BoolVar(&o.dryRun, "dry-run", false, "simulate an install")
f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&o.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production")
f.StringVar(&o.nameTemplate, "name-template", "", "specify template used to name the release")
f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&o.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&o.depUp, "dep-up", false, "run helm dependency update before installing the chart")
o.valuesOptions.addFlags(f)
o.chartPathOptions.addFlags(f)
client.AddFlags(cmd.Flags())
return cmd
}
// nameAndChart returns the name of the release and the chart that should be used.
//
// This will read the flags and handle name generation if necessary.
func (o *installOptions) nameAndChart(args []string) (string, string, error) {
flagsNotSet := func() error {
if o.generateName {
return errors.New("cannot set --generate-name and also specify a name")
}
if o.nameTemplate != "" {
return errors.New("cannot set --name-template and also specify a name")
}
return nil
}
if len(args) == 2 {
return args[0], args[1], flagsNotSet()
}
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")
func runInstall(args []string, client *action.Install, out io.Writer) (*release.Release, error) {
debug("Original chart version: %q", client.Version)
if client.Version == "" && client.Devel {
debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0"
}
base := filepath.Base(args[0])
if base == "." || base == "" {
base = "chart"
name, chart, err := client.NameAndChart(args)
if err != nil {
return nil, err
}
newName := fmt.Sprintf("%s-%d", base, time.Now().Unix())
return newName, args[0], nil
}
client.ReleaseName = name
func (o *installOptions) run(out io.Writer) error {
debug("CHART PATH: %s\n", o.chartPath)
rawVals, err := o.mergedValues()
cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil {
return err
return nil, err
}
// If template is specified, try to run the template.
if o.nameTemplate != "" {
o.name, err = templateName(o.nameTemplate)
if err != nil {
return err
}
// Print the final name so the user knows what the final name of the release is.
fmt.Fprintf(out, "FINAL NAME: %s\n", o.name)
debug("CHART PATH: %s\n", cp)
if err := client.ValueOptions.MergeValues(settings); err != nil {
return nil, err
}
// Check chart dependencies to make sure all are present in /charts
chartRequested, err := loader.Load(o.chartPath)
chartRequested, err := loader.Load(cp)
if err != nil {
return err
return nil, err
}
validInstallableChart, err := chartutil.IsChartInstallable(chartRequested)
if !validInstallableChart {
return err
return nil, err
}
if req := chartRequested.Metadata.Dependencies; req != nil {
// If checkDependencies returns an error, we have unfulfilled dependencies.
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
if err := checkDependencies(chartRequested, req); err != nil {
if o.depUp {
if err := action.CheckDependencies(chartRequested, req); err != nil {
if client.DependencyUpdate {
man := &downloader.Manager{
Out: out,
ChartPath: o.chartPath,
ChartPath: cp,
HelmHome: settings.Home,
Keyring: o.keyring,
Keyring: client.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: getter.All(settings),
}
if err := man.Update(); err != nil {
return err
return nil, err
}
} else {
return err
}
}
}
inst := action.NewInstall(o.cfg)
inst.DryRun = o.dryRun
inst.DisableHooks = o.disableHooks
inst.Replace = o.replace
inst.Wait = o.wait
inst.Devel = o.devel
inst.Timeout = o.timeout
inst.Namespace = getNamespace()
inst.ReleaseName = o.name
rel, err := inst.Run(chartRequested, rawVals)
if err != nil {
return err
}
o.printRelease(out, rel)
return nil
}
// printRelease prints info about a release
func (o *installOptions) printRelease(out io.Writer, rel *release.Release) {
if rel == nil {
return
}
fmt.Fprintf(out, "NAME: %s\n", rel.Name)
if settings.Debug {
printRelease(out, rel)
}
if !rel.Info.LastDeployed.IsZero() {
fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed)
}
fmt.Fprintf(out, "NAMESPACE: %s\n", rel.Namespace)
fmt.Fprintf(out, "STATUS: %s\n", rel.Info.Status.String())
fmt.Fprintf(out, "\n")
if len(rel.Info.Resources) > 0 {
re := regexp.MustCompile(" +")
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent)
fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(rel.Info.Resources, "\t"))
w.Flush()
}
if rel.Info.LastTestSuiteRun != nil {
lastRun := rel.Info.LastTestSuiteRun
fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n",
fmt.Sprintf("Last Started: %s", lastRun.StartedAt),
fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt),
formatTestResults(lastRun.Results))
}
if len(rel.Info.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", rel.Info.Notes)
}
}
// Merges source and destination map, preferring values from the source map
func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = v
continue
}
nextMap, ok := v.(map[string]interface{})
// If it isn't another map, overwrite the value
if !ok {
dest[k] = v
continue
}
// Edge case: If the key exists in the destination, but isn't a map
destMap, isMap := dest[k].(map[string]interface{})
// If the source map has a map for this key, prefer it
if !isMap {
dest[k] = v
continue
}
// If we got to this point, it is a map in both, so merge them
dest[k] = mergeValues(destMap, nextMap)
}
return dest
}
func templateName(nameTemplate string) (string, error) {
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil {
return "", err
}
var b bytes.Buffer
err = t.Execute(&b, nil)
return b.String(), err
}
func checkDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
var missing []string
OUTER:
for _, r := range reqs {
for _, d := range ch.Dependencies() {
if d.Name() == r.Name {
continue OUTER
return nil, err
}
}
missing = append(missing, r.Name)
}
if len(missing) > 0 {
return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
}
return nil
client.Namespace = getNamespace()
return client.Run(chartRequested)
}

@ -17,8 +17,6 @@ limitations under the License.
package main
import (
"reflect"
"regexp"
"testing"
)
@ -142,133 +140,3 @@ func TestInstall(t *testing.T) {
runTestActionCmd(t, tests)
}
type nameTemplateTestCase struct {
tpl string
expected string
expectedErrorStr string
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
// Just a straight up nop please
{
tpl: "foobar",
expected: "foobar",
expectedErrorStr: "",
},
// Random numbers at the end for fun & profit
{
tpl: "foobar-{{randNumeric 6}}",
expected: "foobar-[0-9]{6}$",
expectedErrorStr: "",
},
// Random numbers in the middle for fun & profit
{
tpl: "foobar-{{randNumeric 4}}-baz",
expected: "foobar-[0-9]{4}-baz$",
expectedErrorStr: "",
},
// No such function
{
tpl: "foobar-{{randInt}}",
expected: "",
expectedErrorStr: "function \"randInt\" not defined",
},
// Invalid template
{
tpl: "foobar-{{",
expected: "",
expectedErrorStr: "unexpected unclosed action",
},
}
for _, tc := range testCases {
n, err := templateName(tc.tpl)
if err != nil {
if tc.expectedErrorStr == "" {
t.Errorf("Was not expecting error, but got: %v", err)
continue
}
re, compErr := regexp.Compile(tc.expectedErrorStr)
if compErr != nil {
t.Errorf("Expected error string failed to compile: %v", compErr)
continue
}
if !re.MatchString(err.Error()) {
t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err)
continue
}
}
if err == nil && tc.expectedErrorStr != "" {
t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr)
}
if tc.expected != "" {
re, err := regexp.Compile(tc.expected)
if err != nil {
t.Errorf("Expected string failed to compile: %v", err)
continue
}
if !re.MatchString(n) {
t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n)
}
}
}
}
func TestMergeValues(t *testing.T) {
nestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
"testing": "fun",
}
testMap := mergeValues(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
testMap = mergeValues(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
testMap = mergeValues(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
testMap = mergeValues(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
}

@ -19,19 +19,12 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/strvals"
"k8s.io/helm/pkg/action"
)
var longLintHelp = `
@ -43,159 +36,41 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio
or recommendation, it will emit [WARNING] messages.
`
type lintOptions struct {
strict bool
paths []string
valuesOptions
}
func newLintCmd(out io.Writer) *cobra.Command {
o := &lintOptions{paths: []string{"."}}
client := action.NewLint()
cmd := &cobra.Command{
Use: "lint PATH",
Short: "examines a chart for possible issues",
Long: longLintHelp,
RunE: func(cmd *cobra.Command, args []string) error {
paths := []string{"."}
if len(args) > 0 {
o.paths = args
paths = args
}
return o.run(out)
},
}
fs := cmd.Flags()
fs.BoolVar(&o.strict, "strict", false, "fail on lint warnings")
o.valuesOptions.addFlags(fs)
return cmd
}
var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)")
func (o *lintOptions) run(out io.Writer) error {
var lowestTolerance int
if o.strict {
lowestTolerance = support.WarningSev
} else {
lowestTolerance = support.ErrorSev
}
// Get the raw values
rvals, err := o.vals()
if err != nil {
return err
}
var total int
var failures int
for _, path := range o.paths {
if linter, err := lintChart(path, rvals, getNamespace(), o.strict); err != nil {
fmt.Println("==> Skipping", path)
fmt.Println(err)
if err == errLintNoChart {
failures = failures + 1
client.Namespace = getNamespace()
if err := client.ValueOptions.MergeValues(settings); err != nil {
return err
}
} else {
fmt.Println("==> Linting", path)
if len(linter.Messages) == 0 {
fmt.Println("Lint OK")
result := client.Run(paths)
var message strings.Builder
fmt.Fprintf(&message, "%d chart(s) linted, %d chart(s) failed\n", result.TotalChartsLinted, len(result.Errors))
for _, err := range result.Errors {
fmt.Fprintf(&message, "\t%s\n", err)
}
for _, msg := range linter.Messages {
fmt.Println(msg)
for _, msg := range result.Messages {
fmt.Fprintf(&message, "\t%s\n", msg)
}
total = total + 1
if linter.HighestSeverity >= lowestTolerance {
failures = failures + 1
if len(result.Errors) > 0 {
return errors.New(message.String())
}
}
fmt.Println("")
}
msg := fmt.Sprintf("%d chart(s) linted", total)
if failures > 0 {
return errors.Errorf("%s, %d chart(s) failed", msg, failures)
}
fmt.Fprintf(out, "%s, no failures\n", msg)
return nil
}
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {
var chartPath string
linter := support.Linter{}
if strings.HasSuffix(path, ".tgz") {
tempDir, err := ioutil.TempDir("", "helm-lint")
if err != nil {
return linter, err
}
defer os.RemoveAll(tempDir)
file, err := os.Open(path)
if err != nil {
return linter, err
}
defer file.Close()
if err = chartutil.Expand(tempDir, file); err != nil {
return linter, err
}
lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-")
if lastHyphenIndex <= 0 {
return linter, errors.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path))
}
base := filepath.Base(path)[:lastHyphenIndex]
chartPath = filepath.Join(tempDir, base)
} else {
chartPath = path
}
// Guard: Error out of this is not a chart.
if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil {
return linter, errLintNoChart
}
return lint.All(chartPath, vals, namespace, strict), nil
}
func (o *lintOptions) vals() (map[string]interface{}, error) {
base := map[string]interface{}{}
// User specified a values files via -f/--values
for _, filePath := range o.valueFiles {
currentMap := map[string]interface{}{}
bytes, err := ioutil.ReadFile(filePath)
if err != nil {
return base, err
}
if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
return base, errors.Wrapf(err, "failed to parse %s", filePath)
}
// Merge with the previous map
base = mergeValues(base, currentMap)
fmt.Fprintf(out, message.String())
return nil
},
}
// User specified a value via --set
for _, value := range o.values {
if err := strvals.ParseInto(value, base); err != nil {
return base, errors.Wrap(err, "failed parsing --set data")
}
}
client.AddFlags(cmd.Flags())
// User specified a value via --set-string
for _, value := range o.stringValues {
if err := strvals.ParseIntoString(value, base); err != nil {
return base, errors.Wrap(err, "failed parsing --set-string data")
}
}
return base, nil
return cmd
}

@ -19,14 +19,11 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release"
)
var listHelp = `
@ -39,11 +36,11 @@ By default, it lists only releases that are deployed or failed. Flags like
By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date.
If an argument is provided, it will be treated as a filter. Filters are
If the --filter flag is provided, it will be treated as a filter. Filters are
regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned.
$ helm list 'ara[a-z]+'
$ helm list --filter 'ara[a-z]+'
NAME UPDATED CHART
maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0
@ -56,143 +53,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.
`
type listOptions struct {
// flags
all bool // --all
allNamespaces bool // --all-namespaces
byDate bool // --date
colWidth uint // --col-width
uninstalled bool // --uninstalled
uninstalling bool // --uninstalling
deployed bool // --deployed
failed bool // --failed
limit int // --max
offset int // --offset
pending bool // --pending
short bool // --short
sortDesc bool // --reverse
superseded bool // --superseded
filter string
}
func newListCmd(actionConfig *action.Configuration, out io.Writer) *cobra.Command {
o := &listOptions{}
func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewList(cfg)
cmd := &cobra.Command{
Use: "list [FILTER]",
Use: "list",
Short: "list releases",
Long: listHelp,
Aliases: []string{"ls"},
Args: require.MaximumNArgs(1),
Args: require.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
o.filter = strings.Join(args, " ")
}
if o.allNamespaces {
actionConfig = newActionConfig(true)
}
lister := action.NewList(actionConfig)
lister.All = o.limit == -1
lister.AllNamespaces = o.allNamespaces
lister.Limit = o.limit
lister.Offset = o.offset
lister.Filter = o.filter
// Set StateMask
lister.StateMask = o.setStateMask()
// Set sorter
lister.Sort = action.ByNameAsc
if o.sortDesc {
lister.Sort = action.ByNameDesc
}
if o.byDate {
lister.Sort = action.ByDate
if client.AllNamespaces {
client.SetConfiguration(newActionConfig(true))
}
client.All = client.Limit == -1
client.SetStateMask()
results, err := lister.Run()
results, err := client.Run()
if o.short {
if client.Short {
for _, res := range results {
fmt.Fprintln(out, res.Name)
}
return err
}
fmt.Fprintln(out, formatList(results, 90))
fmt.Fprintln(out, action.FormatList(results))
return err
},
}
f := cmd.Flags()
f.BoolVarP(&o.short, "short", "q", false, "output short (quiet) listing format")
f.BoolVarP(&o.byDate, "date", "d", false, "sort by release date")
f.BoolVarP(&o.sortDesc, "reverse", "r", false, "reverse the sort order")
f.IntVarP(&o.limit, "max", "m", 256, "maximum number of releases to fetch")
f.IntVarP(&o.offset, "offset", "o", 0, "next release name in the list, used to offset from start value")
f.BoolVarP(&o.all, "all", "a", false, "show all releases, not just the ones marked deployed")
f.BoolVar(&o.uninstalled, "uninstalled", false, "show uninstalled releases")
f.BoolVar(&o.superseded, "superseded", false, "show superseded releases")
f.BoolVar(&o.uninstalling, "uninstalling", false, "show releases that are currently being uninstalled")
f.BoolVar(&o.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
f.BoolVar(&o.failed, "failed", false, "show failed releases")
f.BoolVar(&o.pending, "pending", false, "show pending releases")
f.UintVar(&o.colWidth, "col-width", 60, "specifies the max column width of output")
f.BoolVar(&o.allNamespaces, "all-namespaces", false, "list releases across all namespaces")
client.AddFlags(cmd.Flags())
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"
"io"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"github.com/Masterminds/semver"
"k8s.io/helm/pkg/action"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/provenance"
)
const packageDesc = `
@ -49,186 +42,60 @@ Chart.yaml file, and (if found) build the current directory into a chart.
Versioned chart archives are used by Helm package repositories.
`
type packageOptions struct {
appVersion string // --app-version
dependencyUpdate bool // --dependency-update
destination string // --destination
key string // --key
keyring string // --keyring
sign bool // --sign
version string // --version
valuesOptions
path string
home helmpath.Home
}
func newPackageCmd(out io.Writer) *cobra.Command {
o := &packageOptions{}
client := action.NewPackage()
cmd := &cobra.Command{
Use: "package [CHART_PATH] [...]",
Short: "package a chart directory into a chart archive",
Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error {
o.home = settings.Home
if len(args) == 0 {
return errors.Errorf("need at least one argument, the path to the chart")
}
if o.sign {
if o.key == "" {
if client.Sign {
if client.Key == "" {
return errors.New("--key is required for signing a package")
}
if o.keyring == "" {
if client.Keyring == "" {
return errors.New("--keyring is required for signing a package")
}
}
if err := client.ValueOptions.MergeValues(settings); err != nil {
return err
}
for i := 0; i < len(args); i++ {
o.path = args[i]
if err := o.run(out); err != nil {
path, err := filepath.Abs(args[i])
if err != nil {
return err
}
if client.DependencyUpdate {
downloadManager := &downloader.Manager{
Out: ioutil.Discard,
ChartPath: path,
HelmHome: settings.Home,
Keyring: client.Keyring,
Getters: getter.All(settings),
Debug: settings.Debug,
}
if err := downloadManager.Update(); err != nil {
return err
}
}
p, err := client.Run(path)
if err != nil {
return err
}
fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p)
}
return nil
},
}
f := cmd.Flags()
f.BoolVar(&o.sign, "sign", false, "use a PGP private key to sign this package")
f.StringVar(&o.key, "key", "", "name of the key to use when signing. Used if --sign is true")
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "location of a public keyring")
f.StringVar(&o.version, "version", "", "set the version on the chart to this semver version")
f.StringVar(&o.appVersion, "app-version", "", "set the appVersion on the chart to this version")
f.StringVarP(&o.destination, "destination", "d", ".", "location to write the chart.")
f.BoolVarP(&o.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`)
o.valuesOptions.addFlags(f)
client.AddFlags(cmd.Flags())
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/loader"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
)
func TestSetVersion(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "prow",
Version: "0.0.1",
},
}
expect := "1.2.3-beta.5"
if err := setVersion(c, expect); err != nil {
t.Fatal(err)
}
if c.Metadata.Version != expect {
t.Errorf("Expected %q, got %q", expect, c.Metadata.Version)
}
if err := setVersion(c, "monkeyface"); err == nil {
t.Error("Expected bogus version to return an error.")
}
}
func TestPackage(t *testing.T) {
statExe := "stat"
statFileMsg := "no such file or directory"

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

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

@ -24,7 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/plugin"
)

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

@ -24,7 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/plugin"
"k8s.io/helm/pkg/plugin/installer"
)

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

@ -17,20 +17,12 @@ limitations under the License.
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/action"
)
const pullDesc = `
@ -48,20 +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.
`
type pullOptions struct {
destdir string // --destination
devel bool // --devel
untar bool // --untar
untardir string // --untardir
verifyLater bool // --prov
chartRef string
chartPathOptions
}
func newPullCmd(out io.Writer) *cobra.Command {
o := &pullOptions{}
client := action.NewPull()
client.Out = out
cmd := &cobra.Command{
Use: "pull [chart URL | repo/chartname] [...]",
@ -70,14 +51,14 @@ func newPullCmd(out io.Writer) *cobra.Command {
Long: pullDesc,
Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if o.version == "" && o.devel {
client.Settings = settings
if client.Version == "" && client.Devel {
debug("setting version to >0.0.0-0")
o.version = ">0.0.0-0"
client.Version = ">0.0.0-0"
}
for i := 0; i < len(args); i++ {
o.chartRef = args[i]
if err := o.run(out); err != nil {
if err := client.Run(args[i]); err != nil {
return err
}
}
@ -85,80 +66,7 @@ func newPullCmd(out io.Writer) *cobra.Command {
},
}
f := cmd.Flags()
f.BoolVar(&o.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&o.untar, "untar", false, "if set to true, will untar the chart after downloading it")
f.BoolVar(&o.verifyLater, "prov", false, "fetch the provenance file, but don't perform verification")
f.StringVar(&o.untardir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded")
f.StringVarP(&o.destdir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this")
o.chartPathOptions.addFlags(f)
client.AddFlags(cmd.Flags())
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)
cmd := strings.Join(append(tt.args, "-d", "'"+outdir+"'", "--home", "'"+hh.String()+"'"), " ")
out, err := executeCommand(nil, "fetch "+cmd)
_, out, err := executeActionCommand("fetch " + cmd)
if err != nil {
if tt.wantError {
continue

@ -24,8 +24,8 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/release"
)
const releaseTestDesc = `
@ -35,15 +35,8 @@ The argument this command takes is the name of a deployed release.
The tests to be run are defined in the chart that was installed.
`
type releaseTestOptions struct {
name string
client helm.Interface
timeout int64
cleanup bool
}
func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &releaseTestOptions{client: c}
func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewReleaseTesting(cfg)
cmd := &cobra.Command{
Use: "test [RELEASE]",
@ -51,47 +44,35 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: releaseTestDesc,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
o.name = args[0]
o.client = ensureHelmClient(o.client, false)
return o.run(out)
c, errc := client.Run(args[0])
testErr := &testErr{}
for {
select {
case err := <-errc:
if err == nil && testErr.failed > 0 {
return testErr.Error()
}
return err
case res, ok := <-c:
if !ok {
break
}
if res.Status == release.TestRunFailure {
testErr.failed++
}
fmt.Fprintf(out, res.Msg+"\n")
}
}
},
}
f := cmd.Flags()
f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&o.cleanup, "cleanup", false, "delete test pods upon completion")
client.AddFlags(cmd.Flags())
return cmd
}
func (o *releaseTestOptions) run(out io.Writer) (err error) {
c, errc := o.client.RunReleaseTest(
o.name,
helm.ReleaseTestTimeout(o.timeout),
helm.ReleaseTestCleanup(o.cleanup),
)
testErr := &testErr{}
for {
select {
case err := <-errc:
if err == nil && testErr.failed > 0 {
return testErr.Error()
}
return err
case res, ok := <-c:
if !ok {
break
}
if res.Status == release.TestRunFailure {
testErr.failed++
}
fmt.Fprintf(out, res.Msg+"\n")
}
}
}
type testErr struct {
failed int
}

@ -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/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo"
)

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

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

@ -26,7 +26,7 @@ import (
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo"
)

@ -24,7 +24,7 @@ import (
"testing"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
)

@ -19,13 +19,11 @@ package main
import (
"fmt"
"io"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/action"
)
const rollbackDesc = `
@ -36,20 +34,8 @@ second is a revision (version) number. To see revision numbers, run
'helm history RELEASE'.
`
type rollbackOptions struct {
name string
revision int
dryRun bool
recreate bool
force bool
disableHooks bool
client helm.Interface
timeout int64
wait bool
}
func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &rollbackOptions{client: c}
func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewRollback(cfg)
cmd := &cobra.Command{
Use: "rollback [RELEASE] [REVISION]",
@ -57,45 +43,18 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: rollbackDesc,
Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
o.name = args[0]
v64, err := strconv.ParseInt(args[1], 10, 32)
_, err := client.Run(args[0])
if err != nil {
return errors.Wrapf(err, "invalid revision number '%q'", args[1])
return err
}
o.revision = int(v64)
o.client = ensureHelmClient(o.client, false)
return o.run(out)
fmt.Fprintf(out, "Rollback was a success! Happy Helming!\n")
return nil
},
}
f := cmd.Flags()
f.BoolVar(&o.dryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&o.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.BoolVar(&o.force, "force", false, "force resource update through delete/recreate if needed")
f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&o.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout")
client.AddFlags(cmd.Flags())
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 (
"testing"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/release"
)
func TestRollbackCmd(t *testing.T) {
rels := []*release.Release{
{
Name: "funny-honey",
Info: &release.Info{Status: release.StatusSuperseded},
Chart: &chart.Chart{},
Version: 1,
},
{
Name: "funny-honey",
Info: &release.Info{Status: release.StatusDeployed},
Chart: &chart.Chart{},
Version: 2,
},
}
tests := []cmdTestCase{{
name: "rollback a release",
cmd: "rollback funny-honey 1",
golden: "output/rollback.txt",
rels: rels,
}, {
name: "rollback a release with timeout",
cmd: "rollback funny-honey 1 --timeout 120",
golden: "output/rollback-timeout.txt",
rels: rels,
}, {
name: "rollback a release with wait",
cmd: "rollback funny-honey 1 --wait",
golden: "output/rollback-wait.txt",
rels: rels,
}, {
name: "rollback a release without revision",
cmd: "rollback funny-honey",
golden: "output/rollback-no-args.txt",
rels: rels,
wantError: true,
}}
runTestCmd(t, tests)

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

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

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

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

@ -18,20 +18,14 @@ package main
import (
"encoding/json"
"fmt"
"io"
"regexp"
"text/tabwriter"
"github.com/ghodss/yaml"
"github.com/gosuri/uitable"
"github.com/gosuri/uitable/util/strutil"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/action"
)
var statusHelp = `
@ -45,15 +39,8 @@ The status consists of:
- additional notes provided by the chart
`
type statusOptions struct {
release string
client helm.Interface
version int
outfmt string
}
func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &statusOptions{client: client}
func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewStatus(cfg)
cmd := &cobra.Command{
Use: "status RELEASE_NAME",
@ -61,88 +48,42 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command {
Long: statusHelp,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
o.release = args[0]
o.client = ensureHelmClient(o.client, false)
return o.run(out)
rel, err := client.Run(args[0])
if err != nil {
return err
}
outfmt, err := action.ParseOutputFormat(client.OutputFormat)
// We treat an invalid format type as the default
if err != nil && err != action.ErrInvalidFormatType {
return err
}
switch outfmt {
case "":
action.PrintRelease(out, rel)
return nil
case action.JSON:
data, err := json.Marshal(rel)
if err != nil {
return errors.Wrap(err, "failed to Marshal JSON output")
}
out.Write(data)
return nil
case action.YAML:
data, err := yaml.Marshal(rel)
if err != nil {
return errors.Wrap(err, "failed to Marshal YAML output")
}
out.Write(data)
return nil
default:
return errors.Errorf("unknown output format %q", outfmt)
}
},
}
cmd.PersistentFlags().IntVar(&o.version, "revision", 0, "if set, display the status of the named release with revision")
cmd.PersistentFlags().StringVarP(&o.outfmt, "output", "o", "", "output the status in the specified format (json or yaml)")
client.AddFlags(cmd.PersistentFlags())
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"
"time"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/release"
)
func TestStatusCmd(t *testing.T) {
releasesMockWithStatus := func(info *release.Info) []*release.Release {
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "flummoxed-chickadee",
Info: info,
Name: "flummoxed-chickadee",
Info: info,
Chart: &chart.Chart{},
}}
}

@ -19,39 +19,23 @@ package main
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"io/ioutil"
"strings"
"time"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/engine"
"k8s.io/helm/pkg/hapi/release"
util "k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/tiller"
)
const defaultDirectoryPermission = 0755
var (
whitespaceRegex = regexp.MustCompile(`^\s*$`)
// defaultKubeVersion is the default value of --kube-version flag
defaultKubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor)
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
)
const templateDesc = `
Render chart templates locally and display the output.
This does not require Tiller. However, any values that would normally be
This does not require Helm. However, any values that would normally be
looked up or retrieved in-cluster will be faked locally. Additionally, none
of the server-side testing of chart validity (e.g. whether an API is supported)
is done.
@ -61,232 +45,38 @@ To render just one template in a chart, use '-x':
$ helm template mychart -x templates/deployment.yaml
`
type templateOptions struct {
nameTemplate string // --name-template
showNotes bool // --notes
releaseName string // --name
renderFiles []string // --execute
kubeVersion string // --kube-version
outputDir string // --output-dir
valuesOptions
chartPath string
}
func newTemplateCmd(out io.Writer) *cobra.Command {
o := &templateOptions{}
customConfig := &action.Configuration{
// Add mock objects in here so it doesn't use Kube API server
Releases: storage.Init(driver.NewMemory()),
KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) {
fmt.Fprintf(out, format, v...)
},
}
client := action.NewInstall(customConfig)
cmd := &cobra.Command{
Use: "template CHART",
Short: fmt.Sprintf("locally render templates"),
Long: templateDesc,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// verify chart path exists
if _, err := os.Stat(args[0]); err == nil {
if o.chartPath, err = filepath.Abs(args[0]); err != nil {
return err
}
} else {
RunE: func(_ *cobra.Command, args []string) error {
client.DryRun = true
client.ReleaseName = "RELEASE-NAME"
client.Replace = true // Skip the name check
rel, err := runInstall(args, client, out)
if err != nil {
return err
}
return o.run(out)
fmt.Fprintln(out, strings.TrimSpace(rel.Manifest))
return nil
},
}
f := cmd.Flags()
f.BoolVar(&o.showNotes, "notes", false, "show the computed NOTES.txt file as well")
f.StringVarP(&o.releaseName, "name", "", "RELEASE-NAME", "release name")
f.StringArrayVarP(&o.renderFiles, "execute", "x", []string{}, "only execute the given templates")
f.StringVar(&o.nameTemplate, "name-template", "", "specify template used to name the release")
f.StringVar(&o.kubeVersion, "kube-version", defaultKubeVersion, "kubernetes version used as Capabilities.KubeVersion.Major/Minor")
f.StringVar(&o.outputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
o.valuesOptions.addFlags(f)
client.AddFlags(cmd.Flags())
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"
func TestTemplateCmd(t *testing.T) {
absChartPath, err := filepath.Abs(chartPath)
if err != nil {
t.Fatal(err)
}
tests := []cmdTestCase{
{
name: "check name",
@ -37,24 +33,9 @@ func TestTemplateCmd(t *testing.T) {
},
{
name: "check set name",
cmd: fmt.Sprintf("template '%s' -x '%s' --set service.name=apache", chartPath, filepath.Join("templates", "service.yaml")),
cmd: fmt.Sprintf("template '%s' --set service.name=apache", chartPath),
golden: "output/template-set.txt",
},
{
name: "check execute absolute",
cmd: fmt.Sprintf("template '%s' -x '%s' --set service.name=apache", chartPath, filepath.Join(absChartPath, "templates", "service.yaml")),
golden: "output/template-absolute.txt",
},
{
name: "check release name",
cmd: fmt.Sprintf("template '%s' --name test", chartPath),
golden: "output/template-name.txt",
},
{
name: "check notes",
cmd: fmt.Sprintf("template '%s' --notes", chartPath),
golden: "output/template-notes.txt",
},
{
name: "check values files",
cmd: fmt.Sprintf("template '%s' --values '%s'", chartPath, filepath.Join(chartPath, "/charts/subchartA/values.yaml")),
@ -65,11 +46,6 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" }}-baz'`, chartPath),
golden: "output/template-name-template.txt",
},
{
name: "check kube version",
cmd: fmt.Sprintf("template '%s' --kube-version 1.6", chartPath),
golden: "output/template-kube-version.txt",
},
{
name: "check no args",
cmd: "template",

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

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

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

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

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

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

@ -1,4 +1,38 @@
---
# Source: subchart1/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app: subcharta
---
# Source: subchart1/charts/subchartb/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchartb
labels:
chart: "subchartb-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app: subchartb
---
# Source: subchart1/templates/service.yaml
apiVersion: v1
kind: Service
@ -19,4 +53,3 @@ spec:
name: apache
selector:
app: subchart1

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

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

@ -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!
NAME: crazy-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=crazy-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "zany-bunny" has been upgraded. Happy Helming!
NAME: zany-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=zany-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "crazy-bunny" has been upgraded. Happy Helming!
NAME: crazy-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=crazy-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -1,5 +1,11 @@
Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default
STATUS: deployed
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods -l "app=testUpgradeChart,release=funny-bunny" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/action"
)
const uninstallDesc = `
@ -34,20 +34,8 @@ Use the '--dry-run' flag to see which releases will be uninstalled without actua
uninstalling them.
`
type uninstallOptions struct {
disableHooks bool // --no-hooks
dryRun bool // --dry-run
purge bool // --purge
timeout int64 // --timeout
// args
name string
client helm.Interface
}
func newUninstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &uninstallOptions{client: c}
func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewUninstall(cfg)
cmd := &cobra.Command{
Use: "uninstall RELEASE_NAME [...]",
@ -57,40 +45,23 @@ func newUninstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: uninstallDesc,
Args: require.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
o.client = ensureHelmClient(o.client, false)
for i := 0; i < len(args); i++ {
o.name = args[i]
if err := o.run(out); err != nil {
res, err := client.Run(args[0])
if err != nil {
return err
}
if res != nil && res.Info != "" {
fmt.Fprintln(out, res.Info)
}
fmt.Fprintf(out, "release \"%s\" uninstalled\n", o.name)
fmt.Fprintf(out, "release \"%s\" uninstalled\n", args[i])
}
return nil
},
}
f := cmd.Flags()
f.BoolVar(&o.dryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&o.disableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.BoolVar(&o.purge, "purge", false, "remove the release from the store and make its name free for later use")
f.Int64Var(&o.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
client.AddFlags(cmd.Flags())
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 (
"testing"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/release"
)
func TestUninstall(t *testing.T) {
rels := []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}
tests := []cmdTestCase{
{
name: "basic uninstall",
cmd: "uninstall aeneas",
golden: "output/uninstall.txt",
rels: rels,
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{
name: "uninstall with timeout",
cmd: "uninstall aeneas --timeout 120",
golden: "output/uninstall-timeout.txt",
rels: rels,
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{
name: "uninstall without hooks",
cmd: "uninstall aeneas --no-hooks",
golden: "output/uninstall-no-hooks.txt",
rels: rels,
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{
name: "purge",
cmd: "uninstall aeneas --purge",
golden: "output/uninstall-purge.txt",
rels: rels,
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{
name: "uninstall without release",

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

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

@ -21,7 +21,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/action"
)
const verifyDesc = `
@ -35,13 +35,8 @@ This command can be used to verify a local chart. Several other commands provide
the 'helm package --sign' command.
`
type verifyOptions struct {
keyring string
chartfile string
}
func newVerifyCmd(out io.Writer) *cobra.Command {
o := &verifyOptions{}
client := action.NewVerify()
cmd := &cobra.Command{
Use: "verify PATH",
@ -49,18 +44,11 @@ func newVerifyCmd(out io.Writer) *cobra.Command {
Long: verifyDesc,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
o.chartfile = args[0]
return o.run(out)
return client.Run(args[0])
},
}
f := cmd.Flags()
f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys")
client.AddFlags(cmd.Flags())
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 {
t.Run(tt.name, func(t *testing.T) {
out, err := executeCommand(nil, tt.cmd)
_, out, err := executeActionCommand(tt.cmd)
if tt.wantError {
if err == nil {
t.Errorf("Expected error, but got none: %q", out)

@ -10,11 +10,11 @@ Helm can be installed either from source, or from pre-built binary releases.
### From the Binary Releases
Every [release](https://github.com/helm/helm/releases) of Helm
Every [release](https://github.com/helm/releases) of Helm
provides binary releases for a variety of OSes. These binary versions
can be manually downloaded and installed.
1. Download your [desired version](https://github.com/helm/helm/releases)
1. Download your [desired version](https://github.com/helm/releases)
2. Unpack it (`tar -zxvf helm-v2.0.0-linux-amd64.tgz`)
3. Find the `helm` binary in the unpacked directory, and move it to its
desired destination (`mv linux-amd64/helm /usr/local/bin/helm`)

@ -12,7 +12,7 @@ The following prerequisites are required for a successful and properly secured u
### Install Kubernetes or have access to a cluster
- You must have Kubernetes installed. For the latest release of Helm, we recommend the latest stable release of Kubernetes, which in most cases is the second-latest minor release.
- You must have Kubernetes installed. For the latest release of Helm, we recommend the latest stable release of Kubernetes, which in most cases is the second-latest minor release.
- You should also have a local configured copy of `kubectl`.
NOTE: Kubernetes versions prior to 1.6 have limited or no support for role-based access controls (RBAC).

@ -75,7 +75,7 @@ func compare(actual []byte, filename string) error {
return errors.Wrapf(err, "unable to read testdata %s", filename)
}
if !bytes.Equal(expected, actual) {
return errors.Errorf("does not match golden file %s\n\nWANT:\n%s\n\nGOT:\n%s\n", filename, expected, actual)
return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'\n", filename, expected, actual)
}
return nil
}

@ -17,15 +17,18 @@ limitations under the License.
package action
import (
"regexp"
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/registry"
"k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/tiller/environment"
)
// Timestamper is a function capable of producing a timestamp.Timestamper.
@ -34,6 +37,28 @@ import (
// though, so that timestamps are predictable.
var Timestamper = time.Now
var (
// errMissingChart indicates that a chart was not provided.
errMissingChart = errors.New("no chart provided")
// errMissingRelease indicates that a release (name) was not provided.
errMissingRelease = errors.New("no release provided")
// errInvalidRevision indicates that an invalid release revision number was provided.
errInvalidRevision = errors.New("invalid release revision")
//errInvalidName indicates that an invalid release name was provided
errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
)
// ValidName is a regular expression for names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// (([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?
//
// We modified that. First, we added start and end delimiters. Second, we changed
// the final ? to + to require that the pattern match at least once. This modification
// prevents an empty string from matching.
var ValidName = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$")
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// Discovery contains a discovery client
@ -43,7 +68,7 @@ type Configuration struct {
Releases *storage.Storage
// KubeClient is a Kubernetes API client.
KubeClient environment.KubeClient
KubeClient kube.KubernetesClient
// RegistryClient is a client for working with registries
RegistryClient *registry.Client
@ -69,6 +94,18 @@ func (c *Configuration) Now() time.Time {
return Timestamper()
}
func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) {
if err := validateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
}
if version <= 0 {
return c.Releases.Last(name)
}
return c.Releases.Get(name, version)
}
// GetVersionSet retrieves a set of available k8s API versions
func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
groups, err := client.ServerGroups()
@ -87,3 +124,14 @@ func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet
versions := metav1.ExtractGroupVersions(groups)
return chartutil.NewVersionSet(versions...), nil
}
// recordRelease with an update operation in case reuse has been set.
func (c *Configuration) recordRelease(r *release.Release, reuse bool) {
if reuse {
if err := c.Releases.Update(r); err != nil {
c.Log("warning: Failed to update release %s: %s", r.Name, err)
}
} else if err := c.Releases.Create(r); err != nil {
c.Log("warning: Failed to record release %s: %s", r.Name, err)
}
}

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

@ -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"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"k8s.io/client-go/util/homedir"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/cli"
"k8s.io/helm/pkg/downloader"
"k8s.io/helm/pkg/engine"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/getter"
"k8s.io/helm/pkg/hooks"
"k8s.io/helm/pkg/release"
"k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/strvals"
"k8s.io/helm/pkg/version"
)
@ -53,15 +67,39 @@ const notesFileSuffix = "NOTES.txt"
type Install struct {
cfg *Configuration
DryRun bool
DisableHooks bool
Replace bool
Wait bool
Devel bool
DepUp bool
Timeout int64
Namespace string
ReleaseName string
ChartPathOptions
ValueOptions
DryRun bool
DisableHooks bool
Replace bool
Wait bool
Devel bool
DependencyUpdate bool
Timeout int64
Namespace string
ReleaseName string
GenerateName bool
NameTemplate string
}
type ValueOptions struct {
ValueFiles []string
StringValues []string
Values []string
rawValues map[string]interface{}
}
type ChartPathOptions struct {
CaFile string // --ca-file
CertFile string // --cert-file
KeyFile string // --key-file
Keyring string // --keyring
Password string // --password
RepoURL string // --repo
Username string // --username
Verify bool // --verify
Version string // --version
}
// NewInstall creates a new Install object with the given configuration.
@ -74,7 +112,7 @@ func NewInstall(cfg *Configuration) *Install {
// Run executes the installation
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*release.Release, error) {
func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
if err := i.availableName(); err != nil {
return nil, err
}
@ -85,12 +123,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel
Name: i.ReleaseName,
IsInstall: true,
}
valuesToRender, err := chartutil.ToRenderValues(chrt, rawValues, options, caps)
valuesToRender, err := chartutil.ToRenderValues(chrt, i.rawValues, options, caps)
if err != nil {
return nil, err
}
rel := i.createRelease(chrt, rawValues)
rel := i.createRelease(chrt, i.rawValues)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.renderResources(chrt, valuesToRender, caps.APIVersions)
// Even for errors, attach this if available
@ -100,12 +138,12 @@ func (i *Install) Run(chrt *chart.Chart, rawValues map[string]interface{}) (*rel
// Check error from render
if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
rel.Version = 0 // Why do we do this?
// Return a release with partial data so that the client can show debugging information.
return rel, err
}
// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Intiial install underway")
rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
if err := i.validateManifest(manifestDoc); err != nil {
return rel, err
}
@ -257,20 +295,20 @@ func (i *Install) replaceRelease(rel *release.Release) error {
// renderResources renders the templates in a chart
func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
hooks := []*release.Hook{}
buf := bytes.NewBuffer(nil)
b := bytes.NewBuffer(nil)
if ch.Metadata.KubeVersion != "" {
cap, _ := values["Capabilities"].(*chartutil.Capabilities)
gitVersion := cap.KubeVersion.String()
k8sVersion := strings.Split(gitVersion, "+")[0]
if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
return hooks, buf, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
return hooks, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
}
}
files, err := engine.Render(ch, values)
if err != nil {
return hooks, buf, "", err
return hooks, b, "", err
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
@ -301,22 +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
// errors.
b := bytes.NewBuffer(nil)
for name, content := range files {
if len(strings.TrimSpace(content)) == 0 {
continue
}
b.WriteString("\n---\n# Source: " + name + "\n")
b.WriteString(content)
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
}
return hooks, b, "", err
}
// Aggregate all valid manifests into one big doc.
b := bytes.NewBuffer(nil)
for _, m := range manifests {
b.WriteString("\n---\n# Source: " + m.Name + "\n")
b.WriteString(m.Content)
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
}
return hooks, b, notes, nil
@ -346,7 +380,7 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error {
sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks {
if err := i.deleteHookByPolicy(h, hooks.BeforeHookCreation, hook); err != nil {
if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.BeforeHookCreation, hook); err != nil {
return err
}
@ -360,7 +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 a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := i.deleteHookByPolicy(h, hooks.HookFailed, hook); err != nil {
if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.HookFailed, hook); err != nil {
return err
}
return err
@ -370,7 +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
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := i.deleteHookByPolicy(h, hooks.HookSucceeded, hook); err != nil {
if err := deleteHookByPolicy(i.cfg, i.Namespace, h, hooks.HookSucceeded, hook); err != nil {
return err
}
h.LastRun = time.Now()
@ -379,17 +413,6 @@ func (i *Install) execHook(hs []*release.Hook, hook string) error {
return nil
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (i *Install) deleteHookByPolicy(h *release.Hook, policy, hook string) error {
b := bytes.NewBufferString(h.Manifest)
if hookHasDeletePolicy(h, policy) {
if errHookDelete := i.cfg.KubeClient.Delete(i.Namespace, b); errHookDelete != nil {
return errHookDelete
}
}
return nil
}
// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
// FIXME: Can we refactor this out?
var deletePolices = map[string]release.HookDeletePolicy{
@ -424,3 +447,281 @@ func (x hookByWeight) Less(i, j int) bool {
}
return x[i].Weight < x[j].Weight
}
// NameAndChart returns the name and chart that should be used.
//
// This will read the flags and handle name generation if necessary.
func (i *Install) NameAndChart(args []string) (string, string, error) {
flagsNotSet := func() error {
if i.GenerateName {
return errors.New("cannot set --generate-name and also specify a name")
}
if i.NameTemplate != "" {
return errors.New("cannot set --name-template and also specify a name")
}
return nil
}
if len(args) == 2 {
return args[0], args[1], flagsNotSet()
}
if i.NameTemplate != "" {
name, err := TemplateName(i.NameTemplate)
return name, args[0], err
}
if i.ReleaseName != "" {
return i.ReleaseName, args[0], nil
}
if !i.GenerateName {
return "", args[0], errors.New("must either provide a name or specify --generate-name")
}
base := filepath.Base(args[0])
if base == "." || base == "" {
base = "chart"
}
return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
}
func (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 (
"fmt"
"reflect"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/release"
)
type nameTemplateTestCase struct {
tpl string
expected string
expectedErrorStr string
}
func installAction(t *testing.T) *Install {
config := actionConfigFixture(t)
instAction := NewInstall(config)
@ -34,12 +42,11 @@ func installAction(t *testing.T) *Install {
return instAction
}
var mockEmptyVals = func() map[string]interface{} { return map[string]interface{}{} }
func TestInstallRelease(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
res, err := instAction.Run(buildChart(), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -63,7 +70,8 @@ func TestInstallRelease(t *testing.T) {
func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = ""
_, err := instAction.Run(buildChart(), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
_, err := instAction.Run(buildChart())
if err == nil {
t.Fatal("expected failure when no name is specified")
}
@ -74,7 +82,8 @@ func TestInstallRelease_WithNotes(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(withNotes("note here")), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("note here")))
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -100,7 +109,8 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")))
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -118,11 +128,8 @@ func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(
withNotes("parent"),
withDependency(withNotes("child"))),
mockEmptyVals(),
)
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))))
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -138,7 +145,8 @@ func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
res, err := instAction.Run(buildChart(withSampleTemplates()), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()))
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -163,7 +171,8 @@ func TestInstallRelease_NoHooks(t *testing.T) {
instAction.ReleaseName = "no-hooks"
instAction.cfg.Releases.Create(releaseStub())
res, err := instAction.Run(buildChart(), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
if err != nil {
t.Fatalf("Failed install: %s", err)
}
@ -177,7 +186,8 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
instAction.ReleaseName = "failed-hooks"
instAction.cfg.KubeClient = newHookFailingKubeClient()
res, err := instAction.Run(buildChart(), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.Error(err)
is.Contains(res.Info.Description, "failed post-install")
is.Equal(res.Info.Status, release.StatusFailed)
@ -193,7 +203,8 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name
res, err := instAction.Run(buildChart(), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.NoError(err)
// This should have been auto-incremented
@ -208,12 +219,138 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
func TestInstallRelease_KubeVersion(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
_, err := instAction.Run(buildChart(withKube(">=0.0.0")), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
_, err := instAction.Run(buildChart(withKube(">=0.0.0")))
is.NoError(err)
// This should fail for a few hundred years
instAction.ReleaseName = "should-fail"
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), mockEmptyVals())
instAction.rawValues = map[string]interface{}{}
_, err = instAction.Run(buildChart(withKube(">=99.0.0")))
is.Error(err)
is.Contains(err.Error(), "chart requires kubernetesVersion")
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
// Just a straight up nop please
{
tpl: "foobar",
expected: "foobar",
expectedErrorStr: "",
},
// Random numbers at the end for fun & profit
{
tpl: "foobar-{{randNumeric 6}}",
expected: "foobar-[0-9]{6}$",
expectedErrorStr: "",
},
// Random numbers in the middle for fun & profit
{
tpl: "foobar-{{randNumeric 4}}-baz",
expected: "foobar-[0-9]{4}-baz$",
expectedErrorStr: "",
},
// No such function
{
tpl: "foobar-{{randInt}}",
expected: "",
expectedErrorStr: "function \"randInt\" not defined",
},
// Invalid template
{
tpl: "foobar-{{",
expected: "",
expectedErrorStr: "unexpected unclosed action",
},
}
for _, tc := range testCases {
n, err := TemplateName(tc.tpl)
if err != nil {
if tc.expectedErrorStr == "" {
t.Errorf("Was not expecting error, but got: %v", err)
continue
}
re, compErr := regexp.Compile(tc.expectedErrorStr)
if compErr != nil {
t.Errorf("Expected error string failed to compile: %v", compErr)
continue
}
if !re.MatchString(err.Error()) {
t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err)
continue
}
}
if err == nil && tc.expectedErrorStr != "" {
t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr)
}
if tc.expected != "" {
re, err := regexp.Compile(tc.expected)
if err != nil {
t.Errorf("Expected string failed to compile: %v", err)
continue
}
if !re.MatchString(n) {
t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n)
}
}
}
}
func TestMergeValues(t *testing.T) {
nestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
"testing": "fun",
}
testMap := MergeValues(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
testMap = MergeValues(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
testMap = MergeValues(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
testMap = MergeValues(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
}

@ -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.
*/
package main
package action
import (
"testing"
@ -24,11 +24,11 @@ var (
values = make(map[string]interface{})
namespace = "testNamespace"
strict = false
archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz"
archivedChartPathWithHyphens = "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz"
invalidArchivedChartPath = "testdata/testcharts/invalidcompressedchart0.1.0.tgz"
chartDirPath = "testdata/testcharts/decompressedchart/"
chartMissingManifest = "testdata/testcharts/chart-missing-manifest"
archivedChartPath = "../../cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz"
archivedChartPathWithHyphens = "../../cmd/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz"
invalidArchivedChartPath = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz"
chartDirPath = "../../cmd/helm/testdata/testcharts/decompressedchart/"
chartMissingManifest = "../../cmd/helm/testdata/testcharts/chart-missing-manifest"
)
func TestLintChart(t *testing.T) {

@ -17,9 +17,13 @@ limitations under the License.
package action
import (
"fmt"
"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"
)
@ -96,6 +100,8 @@ const (
//
// It provides, for example, the implementation of 'helm list'.
type List struct {
cfg *Configuration
// All ignores the limit/offset
All bool
// AllNamespaces searches across namespaces
@ -112,9 +118,16 @@ type List struct {
// Offset is the starting index for the Run() call
Offset int
// Filter is a filter that is applied to the results
Filter string
cfg *Configuration
Filter string
Short bool
ByDate bool
SortDesc bool
Uninstalled bool
Superseded bool
Uninstalling bool
Deployed bool
Failed bool
Pending bool
}
// NewList constructs a new *List
@ -125,21 +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.
func (a *List) Run() ([]*release.Release, error) {
func (l *List) Run() ([]*release.Release, error) {
var filter *regexp.Regexp
if a.Filter != "" {
if l.Filter != "" {
var err error
filter, err = regexp.Compile(a.Filter)
filter, err = regexp.Compile(l.Filter)
if err != nil {
return nil, err
}
}
results, err := a.cfg.Releases.List(func(rel *release.Release) bool {
results, err := l.cfg.Releases.List(func(rel *release.Release) bool {
// Skip anything that the mask doesn't cover
currentStatus := a.StateMask.FromName(rel.Info.Status.String())
if a.StateMask&currentStatus == 0 {
currentStatus := l.StateMask.FromName(rel.Info.Status.String())
if l.StateMask&currentStatus == 0 {
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
a.sort(results)
l.sort(results)
// Guard on offset
if a.Offset >= len(results) {
if l.Offset >= len(results) {
return []*release.Release{}, nil
}
// Calculate the limit and offset, and then truncate results if necessary.
limit := len(results)
if a.Limit > 0 && a.Limit < limit {
limit = a.Limit
if l.Limit > 0 && l.Limit < limit {
limit = l.Limit
}
last := a.Offset + limit
last := l.Offset + limit
if l := len(results); l < last {
last = l
}
results = results[a.Offset:last]
results = results[l.Offset:last]
return results, err
}
// sort is an in-place sort where order is based on the value of a.Sort
func (a *List) sort(rels []*release.Release) {
switch a.Sort {
func (l *List) sort(rels []*release.Release) {
switch l.Sort {
case ByDate:
releaseutil.SortByDate(rels)
case ByNameDesc:
@ -187,3 +221,53 @@ func (a *List) sort(rels []*release.Release) {
releaseutil.SortByName(rels)
}
}
// setStateMask calculates the state mask based on parameters.
func (l *List) SetStateMask() {
if l.All {
l.StateMask = ListAll
return
}
state := ListStates(0)
if l.Deployed {
state |= ListDeployed
}
if l.Uninstalled {
state |= ListUninstalled
}
if l.Uninstalling {
state |= ListUninstalling
}
if l.Pending {
state |= ListPendingInstall | ListPendingRollback | ListPendingUpgrade
}
if l.Failed {
state |= ListFailed
}
// Apply a default
if state == 0 {
state = ListDeployed | ListFailed
}
l.StateMask = state
}
func FormatList(rels []*release.Release) string {
table := uitable.New()
table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE")
for _, r := range rels {
md := r.Chart.Metadata
c := fmt.Sprintf("%s-%s", md.Name, md.Version)
t := "-"
if tspb := r.Info.LastDeployed; !tspb.IsZero() {
t = tspb.String()
}
s := r.Info.Status.String()
v := r.Version
n := r.Namespace
table.AddRow(r.Name, v, t, s, c, n)
}
return table.String()
}

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

@ -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.
*/
package tiller
package action
import (
"testing"
"k8s.io/helm/pkg/hapi"
"k8s.io/helm/pkg/chart"
)
func TestGetReleaseContent(t *testing.T) {
rs := rsFixture(t)
rel := releaseStub()
if err := rs.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
func TestSetVersion(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "prow",
Version: "0.0.1",
},
}
expect := "1.2.3-beta.5"
if err := setVersion(c, expect); err != nil {
t.Fatal(err)
}
res, err := rs.GetReleaseContent(&hapi.GetReleaseContentRequest{Name: rel.Name, Version: 1})
if err != nil {
t.Errorf("Error getting release content: %s", err)
if c.Metadata.Version != expect {
t.Errorf("Expected %q, got %q", expect, c.Metadata.Version)
}
if res.Chart.Name() != rel.Chart.Name() {
t.Errorf("Expected %q, got %q", rel.Chart.Name(), res.Chart.Name())
if err := setVersion(c, "monkeyface"); err == nil {
t.Error("Expected bogus version to return an error.")
}
}

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

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

Loading…
Cancel
Save