diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 04ca70235..7f1686050 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -24,6 +24,7 @@ import ( "path/filepath" "strings" + "github.com/ghodss/yaml" "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" @@ -34,9 +35,21 @@ import ( const installDesc = ` This command installs a chart archive. -The install argument must be either a relative -path to a chart directory or the name of a -chart in the current working directory. +The install argument must be either a relative path to a chart directory or the +name of a chart in the current working directory. + +To override values in a chart, use either the '--values' flag and pass in a file +or use the '--set' flag and pass configuration from the command line. + + $ helm install -f myvalues.yaml redis + +or + + $ helm install --set name=prod redis + +To check the generated manifests of a release without installing the chart, +the '--debug' and '--dry-run' flags can be combined. This will still require a +round-trip to the Tiller server. ` type installCmd struct { @@ -48,12 +61,14 @@ type installCmd struct { disableHooks bool out io.Writer client helm.Interface + values *values } func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { inst := &installCmd{ out: out, client: c, + values: new(values), } cmd := &cobra.Command{ @@ -76,12 +91,13 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&inst.valuesFile, "values", "f", "", "path to a values YAML file") - f.StringVarP(&inst.name, "name", "n", "", "the release name. If unspecified, it will autogenerate one for you.") + f.StringVarP(&inst.valuesFile, "values", "f", "", "specify values in a YAML file") + f.StringVarP(&inst.name, "name", "n", "", "the release name. If unspecified, it will autogenerate one for you") // TODO use kubeconfig default f.StringVar(&inst.namespace, "namespace", "default", "the namespace to install the release into") f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install") + f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") return cmd } @@ -106,6 +122,9 @@ func (i *installCmd) run() error { } func (i *installCmd) vals() ([]byte, error) { + if len(i.values.pairs) > 0 { + return i.values.yaml() + } if i.valuesFile == "" { return []byte{}, nil } @@ -128,6 +147,62 @@ func (i *installCmd) printRelease(rel *release.Release) { } } +// values represents the command-line value pairs +type values struct { + pairs map[string]interface{} +} + +func (v *values) yaml() ([]byte, error) { + return yaml.Marshal(v.pairs) +} + +func (v *values) String() string { + out, _ := v.yaml() + return string(out) +} + +func (v *values) Type() string { + // Added to pflags.Value interface, but not documented there. + return "struct" +} + +func (v *values) Set(data string) error { + v.pairs = map[string]interface{}{} + + items := strings.Split(data, ",") + for _, item := range items { + n, val := splitPair(item) + names := strings.Split(n, ".") + ln := len(names) + current := &v.pairs + for i := 0; i < ln; i++ { + if i+1 == ln { + // We're at the last element. Set it. + (*current)[names[i]] = val + } else { + // + if e, ok := (*current)[names[i]]; !ok { + m := map[string]interface{}{} + (*current)[names[i]] = m + current = &m + } else if m, ok := e.(map[string]interface{}); ok { + current = &m + } + } + } + } + fmt.Print(v.pairs) + return nil +} + +func splitPair(item string) (name string, value interface{}) { + pair := strings.SplitN(item, "=", 2) + if len(pair) == 1 { + return pair[0], true + } + return pair[0], pair[1] +} + // 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. diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 3b1554ced..c8f9e53bb 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "fmt" "io" "strings" "testing" @@ -42,6 +43,14 @@ func TestInstall(t *testing.T) { expected: "juno", resp: releaseMock("juno"), }, + // Install, values from cli + { + name: "install with values", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--set foo=bar", " "), + resp: releaseMock("virgil"), + expected: "virgil", + }, // Install, no charts { name: "install with no chart specified", @@ -54,3 +63,45 @@ func TestInstall(t *testing.T) { return newInstallCmd(c, out) }) } + +func TestValues(t *testing.T) { + args := "sailor=sinbad,good,port.source=baghdad,port.destination=basrah" + vobj := new(values) + vobj.Set(args) + + if vobj.Type() != "struct" { + t.Fatalf("Expected Type to be struct, got %s", vobj.Type()) + } + + vals := vobj.pairs + if fmt.Sprint(vals["good"]) != "true" { + t.Errorf("Expected good to be true. Got %v", vals["good"]) + } + + port := vals["port"].(map[string]interface{}) + + if fmt.Sprint(port["source"]) != "baghdad" { + t.Errorf("Expected source to be baghdad. Got %s", port["source"]) + } + if fmt.Sprint(port["destination"]) != "basrah" { + t.Errorf("Expected source to be baghdad. Got %s", port["source"]) + } + + y := `good: true +port: + destination: basrah + source: baghdad +sailor: sinbad +` + out, err := vobj.yaml() + if err != nil { + t.Fatal(err) + } + if string(out) != y { + t.Errorf("Expected YAML to be \n%s\nGot\n%s\n", y, out) + } + + if vobj.String() != y { + t.Errorf("Expected String() to be \n%s\nGot\n%s\n", y, out) + } +}