From 215bd6a45c0a30d2ebf24fe3e125ae157f7f5d8d Mon Sep 17 00:00:00 2001 From: Justin Scott Date: Thu, 15 Jun 2017 14:18:03 -0700 Subject: [PATCH] Checkpoint commit. Contains merge problem with set values map and deployment map. Add --set and tests for --set and --node-selectors. Add Options.Values and ValuesMap() test by setting image version: helm init --dry-run --debug --set spec.template.spec.containers[0].image=gcr.io/kubernetes-helm/tiller:v2.4.2 --set spec.replicas=2 -o yaml --- cmd/helm/init.go | 20 ++-- cmd/helm/installer/install.go | 147 +++++++++++++++++++++++++++++ cmd/helm/installer/install_test.go | 107 +++++++++++++++++++++ cmd/helm/installer/options.go | 16 ++++ 4 files changed, 283 insertions(+), 7 deletions(-) diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 2c3148ec9..8ac95741a 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -122,7 +122,8 @@ func newInitCmd(out io.Writer) *cobra.Command { f.StringVar(&i.serviceAccount, "service-account", "", "name of service account") f.StringVar(&i.opts.NodeSelectors, "node-selectors", "", "labels to select which node tiller lands on") - f.StringVarP(&i.opts.Output, "output", "o", "", "skip installation and output tiller's manifest in specified format") + f.StringVarP(&i.opts.Output, "output", "o", "", "skip installation and output tiller's manifest in specified format (json,yaml)") + f.StringArrayVar(&i.opts.Values, "set", []string{}, "set values for the Tiller Deployment manifest (can specify multiple or separate values with commas: key1=val1,key2=val2)") return cmd } @@ -188,13 +189,14 @@ func (i *initCmd) run() error { return err } if len(i.opts.Output) > 0 { + + var body string + var err error + if body, err = installer.DeploymentManifest(&i.opts); err != nil { + return err + } switch i.opts.Output { case "json": - var body string - var err error - if body, err = installer.DeploymentManifest(&i.opts); err != nil { - return err - } jsonb, err := yaml.ToJSON([]byte(body)) if err != nil { return err @@ -202,7 +204,11 @@ func (i *initCmd) run() error { jsons := string(jsonb) jsons = "{\"apiVersion\":\"extensions/v1beta1\",\"kind\":\"Deployment\"," + jsons[1:] fmt.Fprint(i.out, jsons) - + return nil + case "yaml": + if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil { + return err + } return nil default: return fmt.Errorf("Unknown output format: %s", i.opts.Output) diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 3c9b51b16..4b2941d34 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -21,7 +21,10 @@ import ( "strings" + "log" + "github.com/ghodss/yaml" + "github.com/imdario/mergo" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -30,6 +33,7 @@ import ( extensionsclient "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/extensions/v1beta1" + "k8s.io/helm/pkg/chartutil" ) // Install uses kubernetes client to install tiller. @@ -133,6 +137,98 @@ func parseNodeSelectors(labels string) map[string]string { return nodeSelectors } +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} +func coalesceTables(dst, src map[string]interface{}) map[string]interface{} { + // Because dest has higher precedence than src, dest values override src + // values. + for key, val := range src { + if istable(val) { + if innerdst, ok := dst[key]; !ok { + dst[key] = val + } else if istable(innerdst) { + coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) + } else { + log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) + } + continue + } else if dv, ok := dst[key]; ok && istable(dv) { + log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) + continue + } else if !ok { // <- ok is still in scope from preceding conditional. + dst[key] = val + continue + } + } + return dst +} + +// pathToMap creates a nested map given a YAML path in dot notation. +func pathToMap(path string, data map[string]interface{}) map[string]interface{} { + if path == "." { + return data + } + ap := strings.Split(path, ".") + if len(ap) == 0 { + return nil + } + n := []map[string]interface{}{} + // created nested map for each key, adding to slice + for _, v := range ap { + nm := make(map[string]interface{}) + nm[v] = make(map[string]interface{}) + n = append(n, nm) + } + // find the last key (map) and set our data + for i, d := range n { + for k := range d { + z := i + 1 + if z == len(n) { + n[i][k] = data + break + } + n[i][k] = n[z] + } + } + + return n[0] +} + +// Merges source and destination map, preferring values from the source map +func mergeValues(dest map[string]interface{}, 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 + } + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = nextMap + 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 generateDeployment(opts *Options) *v1beta1.Deployment { labels := generateLabels(map[string]string{"name": "tiller"}) nodeSelectors := map[string]string{} @@ -222,6 +318,57 @@ func generateDeployment(opts *Options) *v1beta1.Deployment { }, }) } + // if --set values were specified, ultimately convert values and deployment to maps, + // merge them and convert back to Deployment + if len(opts.Values) > 0 { + // base deployment struct + var dd v1beta1.Deployment + // get YAML from original deployment + dy, err := yaml.Marshal(d) + if err != nil { + log.Fatalf("Error marshalling base Tiller Deployment to YAML: %+v", err) + } + // convert deployment YAML to values + dv, err := chartutil.ReadValues(dy) + if err != nil { + log.Fatalf("Error converting Deployment manifest to Values: %+v ", err) + } + setMap, err := opts.valuesMap() + // transform our set map back into YAML + setS, err := yaml.Marshal(setMap) + + if err != nil { + log.Fatalf("Error marshalling set map to YAML: %+v ", err) + } + // transform our YAML into Values + setV, err := chartutil.ReadValues(setS) + + //log.Fatal(setV) + if err != nil { + log.Fatalf("Error reading Values from input: %+v ", err) + } + // merge original deployment map and set map + //finalM := coalesceTables(dv.AsMap(), setV.AsMap()) + //finalM := mergeValues(dv.AsMap(), setV.AsMap()) + dm := dv.AsMap() + sm := setV.AsMap() + err = mergo.Merge(&sm, dm) + if err != nil { + log.Fatal(err) + } + log.Fatal(sm) //for other merges use finalM above + finalY, err := yaml.Marshal(dm) + if err != nil { + log.Fatalf("Error marshalling merged map to YAML: %+v ", err) + } + + // convert merged values back into deployment + err = yaml.Unmarshal([]byte(finalY), &dd) + if err != nil { + log.Fatalf("Error unmarshalling Values to Deployment manifest: %+v ", err) + } + d = &dd + } return d } diff --git a/cmd/helm/installer/install_test.go b/cmd/helm/installer/install_test.go index e1e94d7e5..dd10795f3 100644 --- a/cmd/helm/installer/install_test.go +++ b/cmd/helm/installer/install_test.go @@ -30,6 +30,7 @@ import ( "k8s.io/client-go/pkg/apis/extensions/v1beta1" testcore "k8s.io/client-go/testing" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/version" ) @@ -419,3 +420,109 @@ func tlsTestFile(t *testing.T, path string) string { } return path } +func TestDeploymentManifest_WithNodeSelectors(t *testing.T) { + //note 'beta.kubernetes.io/os": "linux"' is a default nodeSelector in helm init's deployment manifest + tests := []struct { + opts Options + name string + expect map[string]interface{} + }{ + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller"}, + "nodeSelector app=tiller", + map[string]interface{}{"app": "tiller", "beta.kubernetes.io/os": "linux"}, + }, + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,helm=rocks"}, + "nodeSelector app=tiller, helm=rocks", + map[string]interface{}{"app": "tiller", "helm": "rocks", "beta.kubernetes.io/os": "linux"}, + }, + // note: nodeSelector key and value are strings + { + Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,minCoolness=1"}, + "nodeSelector app=tiller, helm=rocks", + map[string]interface{}{"app": "tiller", "minCoolness": "1", "beta.kubernetes.io/os": "linux"}, + }, + } + for _, tt := range tests { + o, err := DeploymentManifest(&tt.opts) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + + var d v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &d); err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + // verify environment variable in deployment reflect the use of tls being enabled. + got := d.Spec.Template.Spec.NodeSelector + for k, v := range tt.expect { + if got[k] != v { + t.Errorf("%s: expected nodeselector value %q, got %q", tt.name, tt.expect, got) + } + } + } +} +func TestDeploymentManifest_WithSetValues(t *testing.T) { + tests := []struct { + opts Options + name string + expectPath string + expect interface{} + }{ + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec.nodeselector=app=tiller"}}, + "setValues spec.template.spec.nodeSelector=app=tiller", + "spec.template.spec.nodeSelector.app", + "tiller", + }, + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.replicas=2"}}, + "setValues spec.replicas=2", + "spec.replicas", + 2, + }, + { + Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec=activedeadlineseconds=120"}}, + "setValues spec.template.spec=activedeadlineseconds=120", + "spec.template.spec.activeDeadlineSeconds", + 120, + }, + } + for _, tt := range tests { + o, err := DeploymentManifest(&tt.opts) + if err != nil { + t.Fatalf("%s: error %q", tt.name, err) + } + values, err := chartutil.ReadValues([]byte(o)) + if err != nil { + t.Errorf("Error converting Deployment manifest to Values: %s", err) + } + // path value + pv, err := values.PathValue(tt.expectPath) + if err != nil { + t.Errorf("Error retrieving path value from Deployment Values: %s", err) + } + + // convert our expected value to match the result type for comparison + ev := tt.expect + switch pvt := pv.(type) { + case float64: + floatType := reflect.TypeOf(float64(0)) + v := reflect.ValueOf(ev) + v = reflect.Indirect(v) + if !v.Type().ConvertibleTo(floatType) { + t.Fatalf("Error converting expected value %v to float64", v.Type()) + } + fv := v.Convert(floatType) + if fv.Float() != pvt { + t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) + } + + default: + if pv != tt.expect { + t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) + } + } + } +} diff --git a/cmd/helm/installer/options.go b/cmd/helm/installer/options.go index 354812dc7..9e825a695 100644 --- a/cmd/helm/installer/options.go +++ b/cmd/helm/installer/options.go @@ -20,6 +20,7 @@ import ( "fmt" "k8s.io/client-go/pkg/api/v1" + "k8s.io/helm/pkg/strvals" "k8s.io/helm/pkg/version" ) @@ -77,6 +78,9 @@ type Options struct { // Output dumps the Tiller manifest in the specified format (e.g. json) but skips Helm/Tiller installation Output string + + // Set merges additional values into the Tiller Deployment manifest + Values []string } func (opts *Options) selectImage() string { @@ -98,3 +102,15 @@ func (opts *Options) pullPolicy() v1.PullPolicy { } func (opts *Options) tls() bool { return opts.EnableTLS || opts.VerifyTLS } + +// valuesMap returns user set values in map format +func (opts *Options) valuesMap() (map[string]interface{}, error) { + m := map[string]interface{}{} + for _, skv := range opts.Values { + err := strvals.ParseInto(skv, m) + if err != nil { + return nil, err + } + } + return m, nil +}