github.com/cloudposse/helm@v2.2.3+incompatible/pkg/chartutil/values.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package chartutil
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"log"
    24  	"strings"
    25  
    26  	"github.com/ghodss/yaml"
    27  	"github.com/golang/protobuf/ptypes/timestamp"
    28  	"k8s.io/helm/pkg/proto/hapi/chart"
    29  )
    30  
    31  // ErrNoTable indicates that a chart does not have a matching table.
    32  type ErrNoTable error
    33  
    34  // ErrNoValue indicates that Values does not contain a key with a value
    35  type ErrNoValue error
    36  
    37  // GlobalKey is the name of the Values key that is used for storing global vars.
    38  const GlobalKey = "global"
    39  
    40  // Values represents a collection of chart values.
    41  type Values map[string]interface{}
    42  
    43  // YAML encodes the Values into a YAML string.
    44  func (v Values) YAML() (string, error) {
    45  	b, err := yaml.Marshal(v)
    46  	return string(b), err
    47  }
    48  
    49  // Table gets a table (YAML subsection) from a Values object.
    50  //
    51  // The table is returned as a Values.
    52  //
    53  // Compound table names may be specified with dots:
    54  //
    55  //	foo.bar
    56  //
    57  // The above will be evaluated as "The table bar inside the table
    58  // foo".
    59  //
    60  // An ErrNoTable is returned if the table does not exist.
    61  func (v Values) Table(name string) (Values, error) {
    62  	names := strings.Split(name, ".")
    63  	table := v
    64  	var err error
    65  
    66  	for _, n := range names {
    67  		table, err = tableLookup(table, n)
    68  		if err != nil {
    69  			return table, err
    70  		}
    71  	}
    72  	return table, err
    73  }
    74  
    75  // AsMap is a utility function for converting Values to a map[string]interface{}.
    76  //
    77  // It protects against nil map panics.
    78  func (v Values) AsMap() map[string]interface{} {
    79  	if v == nil || len(v) == 0 {
    80  		return map[string]interface{}{}
    81  	}
    82  	return v
    83  }
    84  
    85  // Encode writes serialized Values information to the given io.Writer.
    86  func (v Values) Encode(w io.Writer) error {
    87  	//return yaml.NewEncoder(w).Encode(v)
    88  	out, err := yaml.Marshal(v)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	_, err = w.Write(out)
    93  	return err
    94  }
    95  
    96  func tableLookup(v Values, simple string) (Values, error) {
    97  	v2, ok := v[simple]
    98  	if !ok {
    99  		return v, ErrNoTable(fmt.Errorf("no table named %q (%v)", simple, v))
   100  	}
   101  	if vv, ok := v2.(map[string]interface{}); ok {
   102  		return vv, nil
   103  	}
   104  
   105  	// This catches a case where a value is of type Values, but doesn't (for some
   106  	// reason) match the map[string]interface{}. This has been observed in the
   107  	// wild, and might be a result of a nil map of type Values.
   108  	if vv, ok := v2.(Values); ok {
   109  		return vv, nil
   110  	}
   111  
   112  	var e ErrNoTable = fmt.Errorf("no table named %q", simple)
   113  	return map[string]interface{}{}, e
   114  }
   115  
   116  // ReadValues will parse YAML byte data into a Values.
   117  func ReadValues(data []byte) (vals Values, err error) {
   118  	err = yaml.Unmarshal(data, &vals)
   119  	if len(vals) == 0 {
   120  		vals = Values{}
   121  	}
   122  	return
   123  }
   124  
   125  // ReadValuesFile will parse a YAML file into a map of values.
   126  func ReadValuesFile(filename string) (Values, error) {
   127  	data, err := ioutil.ReadFile(filename)
   128  	if err != nil {
   129  		return map[string]interface{}{}, err
   130  	}
   131  	return ReadValues(data)
   132  }
   133  
   134  // CoalesceValues coalesces all of the values in a chart (and its subcharts).
   135  //
   136  // Values are coalesced together using the following rules:
   137  //
   138  //	- Values in a higher level chart always override values in a lower-level
   139  //		dependency chart
   140  //	- Scalar values and arrays are replaced, maps are merged
   141  //	- A chart has access to all of the variables for it, as well as all of
   142  //		the values destined for its dependencies.
   143  func CoalesceValues(chrt *chart.Chart, vals *chart.Config) (Values, error) {
   144  	cvals := Values{}
   145  	// Parse values if not nil. We merge these at the top level because
   146  	// the passed-in values are in the same namespace as the parent chart.
   147  	if vals != nil {
   148  		evals, err := ReadValues([]byte(vals.Raw))
   149  		if err != nil {
   150  			return cvals, err
   151  		}
   152  		cvals, err = coalesce(chrt, evals)
   153  		if err != nil {
   154  			return cvals, err
   155  		}
   156  	}
   157  
   158  	var err error
   159  	cvals, err = coalesceDeps(chrt, cvals)
   160  	return cvals, err
   161  }
   162  
   163  // coalesce coalesces the dest values and the chart values, giving priority to the dest values.
   164  //
   165  // This is a helper function for CoalesceValues.
   166  func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
   167  	var err error
   168  	dest, err = coalesceValues(ch, dest)
   169  	if err != nil {
   170  		return dest, err
   171  	}
   172  	coalesceDeps(ch, dest)
   173  	return dest, nil
   174  }
   175  
   176  // coalesceDeps coalesces the dependencies of the given chart.
   177  func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
   178  	for _, subchart := range chrt.Dependencies {
   179  		if c, ok := dest[subchart.Metadata.Name]; !ok {
   180  			// If dest doesn't already have the key, create it.
   181  			dest[subchart.Metadata.Name] = map[string]interface{}{}
   182  		} else if !istable(c) {
   183  			return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Metadata.Name, c)
   184  		}
   185  		if dv, ok := dest[subchart.Metadata.Name]; ok {
   186  			dvmap := dv.(map[string]interface{})
   187  
   188  			// Get globals out of dest and merge them into dvmap.
   189  			coalesceGlobals(dvmap, dest)
   190  
   191  			var err error
   192  			// Now coalesce the rest of the values.
   193  			dest[subchart.Metadata.Name], err = coalesce(subchart, dvmap)
   194  			if err != nil {
   195  				return dest, err
   196  			}
   197  		}
   198  	}
   199  	return dest, nil
   200  }
   201  
   202  // coalesceGlobals copies the globals out of src and merges them into dest.
   203  //
   204  // For convenience, returns dest.
   205  func coalesceGlobals(dest, src map[string]interface{}) map[string]interface{} {
   206  	var dg, sg map[string]interface{}
   207  
   208  	if destglob, ok := dest[GlobalKey]; !ok {
   209  		dg = map[string]interface{}{}
   210  	} else if dg, ok = destglob.(map[string]interface{}); !ok {
   211  		log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
   212  		return dg
   213  	}
   214  
   215  	if srcglob, ok := src[GlobalKey]; !ok {
   216  		sg = map[string]interface{}{}
   217  	} else if sg, ok = srcglob.(map[string]interface{}); !ok {
   218  		log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey)
   219  		return dg
   220  	}
   221  
   222  	// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
   223  	// reverses that decision. It may somehow be possible to introduce a loop
   224  	// here, but I haven't found a way. So for the time being, let's allow
   225  	// tables in globals.
   226  	for key, val := range sg {
   227  		if istable(val) {
   228  			vv := copyMap(val.(map[string]interface{}))
   229  			if destv, ok := dg[key]; ok {
   230  				if destvmap, ok := destv.(map[string]interface{}); ok {
   231  					// Basically, we reverse order of coalesce here to merge
   232  					// top-down.
   233  					coalesceTables(vv, destvmap)
   234  					dg[key] = vv
   235  					continue
   236  				} else {
   237  					log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
   238  				}
   239  			} else {
   240  				// Here there is no merge. We're just adding.
   241  				dg[key] = vv
   242  			}
   243  		} else if dv, ok := dg[key]; ok && istable(dv) {
   244  			// It's not clear if this condition can actually ever trigger.
   245  			log.Printf("key %s is table. Skipping", key)
   246  			continue
   247  		}
   248  		// TODO: Do we need to do any additional checking on the value?
   249  		dg[key] = val
   250  	}
   251  	dest[GlobalKey] = dg
   252  	return dest
   253  }
   254  
   255  func copyMap(src map[string]interface{}) map[string]interface{} {
   256  	dest := make(map[string]interface{}, len(src))
   257  	for k, v := range src {
   258  		dest[k] = v
   259  	}
   260  	return dest
   261  }
   262  
   263  // coalesceValues builds up a values map for a particular chart.
   264  //
   265  // Values in v will override the values in the chart.
   266  func coalesceValues(c *chart.Chart, v map[string]interface{}) (map[string]interface{}, error) {
   267  	// If there are no values in the chart, we just return the given values
   268  	if c.Values == nil || c.Values.Raw == "" {
   269  		return v, nil
   270  	}
   271  
   272  	nv, err := ReadValues([]byte(c.Values.Raw))
   273  	if err != nil {
   274  		// On error, we return just the overridden values.
   275  		// FIXME: We should log this error. It indicates that the YAML data
   276  		// did not parse.
   277  		return v, fmt.Errorf("error reading default values (%s): %s", c.Values.Raw, err)
   278  	}
   279  
   280  	for key, val := range nv {
   281  		if _, ok := v[key]; !ok {
   282  			// If the key is not in v, copy it from nv.
   283  			v[key] = val
   284  		} else if dest, ok := v[key].(map[string]interface{}); ok {
   285  			// if v[key] is a table, merge nv's val table into v[key].
   286  			src, ok := val.(map[string]interface{})
   287  			if !ok {
   288  				log.Printf("warning: skipped value for %s: Not a table.", key)
   289  				continue
   290  			}
   291  			// Because v has higher precedence than nv, dest values override src
   292  			// values.
   293  			coalesceTables(dest, src)
   294  		}
   295  	}
   296  	return v, nil
   297  }
   298  
   299  // coalesceTables merges a source map into a destination map.
   300  //
   301  // dest is considered authoritative.
   302  func coalesceTables(dst, src map[string]interface{}) map[string]interface{} {
   303  	// Because dest has higher precedence than src, dest values override src
   304  	// values.
   305  	for key, val := range src {
   306  		if istable(val) {
   307  			if innerdst, ok := dst[key]; !ok {
   308  				dst[key] = val
   309  			} else if istable(innerdst) {
   310  				coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
   311  			} else {
   312  				log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
   313  			}
   314  			continue
   315  		} else if dv, ok := dst[key]; ok && istable(dv) {
   316  			log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val)
   317  			continue
   318  		} else if !ok { // <- ok is still in scope from preceding conditional.
   319  			dst[key] = val
   320  			continue
   321  		}
   322  	}
   323  	return dst
   324  }
   325  
   326  // ReleaseOptions represents the additional release options needed
   327  // for the composition of the final values struct
   328  type ReleaseOptions struct {
   329  	Name      string
   330  	Time      *timestamp.Timestamp
   331  	Namespace string
   332  	IsUpgrade bool
   333  	IsInstall bool
   334  	Revision  int
   335  }
   336  
   337  // ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
   338  //
   339  // WARNING: This function is deprecated for Helm > 2.1.99 Use ToRenderValuesCaps() instead. It will
   340  // remain in the codebase to stay SemVer compliant.
   341  //
   342  // In Helm 3.0, this will be changed to accept Capabilities as a fourth parameter.
   343  func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) {
   344  	caps := &Capabilities{APIVersions: DefaultVersionSet}
   345  	return ToRenderValuesCaps(chrt, chrtVals, options, caps)
   346  }
   347  
   348  // ToRenderValuesCaps composes the struct from the data coming from the Releases, Charts and Values files
   349  //
   350  // This takes both ReleaseOptions and Capabilities to merge into the render values.
   351  func ToRenderValuesCaps(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions, caps *Capabilities) (Values, error) {
   352  
   353  	top := map[string]interface{}{
   354  		"Release": map[string]interface{}{
   355  			"Name":      options.Name,
   356  			"Time":      options.Time,
   357  			"Namespace": options.Namespace,
   358  			"IsUpgrade": options.IsUpgrade,
   359  			"IsInstall": options.IsInstall,
   360  			"Revision":  options.Revision,
   361  			"Service":   "Tiller",
   362  		},
   363  		"Chart":        chrt.Metadata,
   364  		"Files":        NewFiles(chrt.Files),
   365  		"Capabilities": caps,
   366  	}
   367  
   368  	vals, err := CoalesceValues(chrt, chrtVals)
   369  	if err != nil {
   370  		return top, err
   371  	}
   372  
   373  	top["Values"] = vals
   374  	return top, nil
   375  }
   376  
   377  // istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
   378  func istable(v interface{}) bool {
   379  	_, ok := v.(map[string]interface{})
   380  	return ok
   381  }
   382  
   383  // PathValue takes a yaml path with . notation and returns the value if exists
   384  func (v Values) PathValue(ypath string) (interface{}, error) {
   385  	if len(ypath) == 0 {
   386  		return nil, fmt.Errorf("yaml path string cannot be zero length")
   387  	}
   388  	yps := strings.Split(ypath, ".")
   389  	if len(yps) == 1 {
   390  		// if exists must be root key not table
   391  		vals := v.AsMap()
   392  		k := yps[0]
   393  		if _, ok := vals[k]; ok && !istable(vals[k]) {
   394  			// key found
   395  			return vals[yps[0]], nil
   396  		}
   397  		// key not found
   398  		return nil, ErrNoValue(fmt.Errorf("%v is not a value", k))
   399  	}
   400  	// join all elements of YAML path except last to get string table path
   401  	ypsLen := len(yps)
   402  	table := yps[:ypsLen-1]
   403  	st := strings.Join(table, ".")
   404  	// get the last element as a string key
   405  	key := yps[ypsLen-1:]
   406  	sk := string(key[0])
   407  	// get our table for table path
   408  	t, err := v.Table(st)
   409  	if err != nil {
   410  		//no table
   411  		return nil, ErrNoValue(fmt.Errorf("%v is not a value", sk))
   412  	}
   413  	// check table for key and ensure value is not a table
   414  	if k, ok := t[sk]; ok && !istable(k) {
   415  		// key found
   416  		return k, nil
   417  	}
   418  
   419  	// key not found
   420  	return nil, ErrNoValue(fmt.Errorf("key not found: %s", sk))
   421  }