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
reviewable/pr2557/r3
Justin Scott 8 years ago
parent 505bcd1fcd
commit 215bd6a45c

@ -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 {
switch i.opts.Output {
case "json":
var body string
var err error
if body, err = installer.DeploymentManifest(&i.opts); err != nil {
return err
}
switch i.opts.Output {
case "json":
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)

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

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

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

Loading…
Cancel
Save