github.com/umeshredd/helm@v3.0.0-alpha.1+incompatible/pkg/chartutil/values.go (about)

     1  /*
     2  Copyright The Helm Authors.
     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  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"log"
    25  	"strings"
    26  
    27  	"github.com/ghodss/yaml"
    28  	"github.com/pkg/errors"
    29  	"github.com/xeipuuv/gojsonschema"
    30  
    31  	"helm.sh/helm/pkg/chart"
    32  )
    33  
    34  // ErrNoTable indicates that a chart does not have a matching table.
    35  type ErrNoTable string
    36  
    37  func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e) }
    38  
    39  // ErrNoValue indicates that Values does not contain a key with a value
    40  type ErrNoValue string
    41  
    42  func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e) }
    43  
    44  // GlobalKey is the name of the Values key that is used for storing global vars.
    45  const GlobalKey = "global"
    46  
    47  // Values represents a collection of chart values.
    48  type Values map[string]interface{}
    49  
    50  // YAML encodes the Values into a YAML string.
    51  func (v Values) YAML() (string, error) {
    52  	b, err := yaml.Marshal(v)
    53  	return string(b), err
    54  }
    55  
    56  // Table gets a table (YAML subsection) from a Values object.
    57  //
    58  // The table is returned as a Values.
    59  //
    60  // Compound table names may be specified with dots:
    61  //
    62  //	foo.bar
    63  //
    64  // The above will be evaluated as "The table bar inside the table
    65  // foo".
    66  //
    67  // An ErrNoTable is returned if the table does not exist.
    68  func (v Values) Table(name string) (Values, error) {
    69  	table := v
    70  	var err error
    71  
    72  	for _, n := range parsePath(name) {
    73  		if table, err = tableLookup(table, n); err != nil {
    74  			break
    75  		}
    76  	}
    77  	return table, err
    78  }
    79  
    80  // AsMap is a utility function for converting Values to a map[string]interface{}.
    81  //
    82  // It protects against nil map panics.
    83  func (v Values) AsMap() map[string]interface{} {
    84  	if v == nil || len(v) == 0 {
    85  		return map[string]interface{}{}
    86  	}
    87  	return v
    88  }
    89  
    90  // Encode writes serialized Values information to the given io.Writer.
    91  func (v Values) Encode(w io.Writer) error {
    92  	//return yaml.NewEncoder(w).Encode(v)
    93  	out, err := yaml.Marshal(v)
    94  	if err != nil {
    95  		return err
    96  	}
    97  	_, err = w.Write(out)
    98  	return err
    99  }
   100  
   101  func tableLookup(v Values, simple string) (Values, error) {
   102  	v2, ok := v[simple]
   103  	if !ok {
   104  		return v, ErrNoTable(simple)
   105  	}
   106  	if vv, ok := v2.(map[string]interface{}); ok {
   107  		return vv, nil
   108  	}
   109  
   110  	// This catches a case where a value is of type Values, but doesn't (for some
   111  	// reason) match the map[string]interface{}. This has been observed in the
   112  	// wild, and might be a result of a nil map of type Values.
   113  	if vv, ok := v2.(Values); ok {
   114  		return vv, nil
   115  	}
   116  
   117  	return Values{}, ErrNoTable(simple)
   118  }
   119  
   120  // ReadValues will parse YAML byte data into a Values.
   121  func ReadValues(data []byte) (vals Values, err error) {
   122  	err = yaml.Unmarshal(data, &vals)
   123  	if len(vals) == 0 {
   124  		vals = Values{}
   125  	}
   126  	return vals, err
   127  }
   128  
   129  // ReadValuesFile will parse a YAML file into a map of values.
   130  func ReadValuesFile(filename string) (Values, error) {
   131  	data, err := ioutil.ReadFile(filename)
   132  	if err != nil {
   133  		return map[string]interface{}{}, err
   134  	}
   135  	return ReadValues(data)
   136  }
   137  
   138  // ValidateAgainstSchema checks that values does not violate the structure laid out in schema
   139  func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
   140  	var sb strings.Builder
   141  	if chrt.Schema != nil {
   142  		err := ValidateAgainstSingleSchema(values, chrt.Schema)
   143  		if err != nil {
   144  			sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
   145  			sb.WriteString(err.Error())
   146  		}
   147  	}
   148  
   149  	// For each dependency, recurively call this function with the coalesced values
   150  	for _, subchrt := range chrt.Dependencies() {
   151  		subchrtValues := values[subchrt.Name()].(map[string]interface{})
   152  		if err := ValidateAgainstSchema(subchrt, subchrtValues); err != nil {
   153  			sb.WriteString(err.Error())
   154  		}
   155  	}
   156  
   157  	if sb.Len() > 0 {
   158  		return errors.New(sb.String())
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
   165  func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error {
   166  	valuesData, err := yaml.Marshal(values)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	valuesJSON, err := yaml.YAMLToJSON(valuesData)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	if bytes.Equal(valuesJSON, []byte("null")) {
   175  		valuesJSON = []byte("{}")
   176  	}
   177  	schemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
   178  	valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
   179  
   180  	result, err := gojsonschema.Validate(schemaLoader, valuesLoader)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	if !result.Valid() {
   186  		var sb strings.Builder
   187  		for _, desc := range result.Errors() {
   188  			sb.WriteString(fmt.Sprintf("- %s\n", desc))
   189  		}
   190  		return errors.New(sb.String())
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // CoalesceValues coalesces all of the values in a chart (and its subcharts).
   197  //
   198  // Values are coalesced together using the following rules:
   199  //
   200  //	- Values in a higher level chart always override values in a lower-level
   201  //		dependency chart
   202  //	- Scalar values and arrays are replaced, maps are merged
   203  //	- A chart has access to all of the variables for it, as well as all of
   204  //		the values destined for its dependencies.
   205  func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
   206  	if vals == nil {
   207  		vals = make(map[string]interface{})
   208  	}
   209  	if _, err := coalesce(chrt, vals); err != nil {
   210  		return vals, err
   211  	}
   212  	return coalesceDeps(chrt, vals)
   213  }
   214  
   215  // coalesce coalesces the dest values and the chart values, giving priority to the dest values.
   216  //
   217  // This is a helper function for CoalesceValues.
   218  func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
   219  	coalesceValues(ch, dest)
   220  	return coalesceDeps(ch, dest)
   221  }
   222  
   223  // coalesceDeps coalesces the dependencies of the given chart.
   224  func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
   225  	for _, subchart := range chrt.Dependencies() {
   226  		if c, ok := dest[subchart.Name()]; !ok {
   227  			// If dest doesn't already have the key, create it.
   228  			dest[subchart.Name()] = make(map[string]interface{})
   229  		} else if !istable(c) {
   230  			return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c)
   231  		}
   232  		if dv, ok := dest[subchart.Name()]; ok {
   233  			dvmap := dv.(map[string]interface{})
   234  
   235  			// Get globals out of dest and merge them into dvmap.
   236  			coalesceGlobals(dvmap, dest)
   237  
   238  			// Now coalesce the rest of the values.
   239  			var err error
   240  			dest[subchart.Name()], err = coalesce(subchart, dvmap)
   241  			if err != nil {
   242  				return dest, err
   243  			}
   244  		}
   245  	}
   246  	return dest, nil
   247  }
   248  
   249  // coalesceGlobals copies the globals out of src and merges them into dest.
   250  //
   251  // For convenience, returns dest.
   252  func coalesceGlobals(dest, src map[string]interface{}) {
   253  	var dg, sg map[string]interface{}
   254  
   255  	if destglob, ok := dest[GlobalKey]; !ok {
   256  		dg = make(map[string]interface{})
   257  	} else if dg, ok = destglob.(map[string]interface{}); !ok {
   258  		log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
   259  		return
   260  	}
   261  
   262  	if srcglob, ok := src[GlobalKey]; !ok {
   263  		sg = make(map[string]interface{})
   264  	} else if sg, ok = srcglob.(map[string]interface{}); !ok {
   265  		log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey)
   266  		return
   267  	}
   268  
   269  	// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
   270  	// reverses that decision. It may somehow be possible to introduce a loop
   271  	// here, but I haven't found a way. So for the time being, let's allow
   272  	// tables in globals.
   273  	for key, val := range sg {
   274  		if istable(val) {
   275  			vv := copyMap(val.(map[string]interface{}))
   276  			if destv, ok := dg[key]; !ok {
   277  				// Here there is no merge. We're just adding.
   278  				dg[key] = vv
   279  			} else {
   280  				if destvmap, ok := destv.(map[string]interface{}); !ok {
   281  					log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
   282  				} else {
   283  					// Basically, we reverse order of coalesce here to merge
   284  					// top-down.
   285  					CoalesceTables(vv, destvmap)
   286  					dg[key] = vv
   287  					continue
   288  				}
   289  			}
   290  		} else if dv, ok := dg[key]; ok && istable(dv) {
   291  			// It's not clear if this condition can actually ever trigger.
   292  			log.Printf("key %s is table. Skipping", key)
   293  			continue
   294  		}
   295  		// TODO: Do we need to do any additional checking on the value?
   296  		dg[key] = val
   297  	}
   298  	dest[GlobalKey] = dg
   299  }
   300  
   301  func copyMap(src map[string]interface{}) map[string]interface{} {
   302  	m := make(map[string]interface{}, len(src))
   303  	for k, v := range src {
   304  		m[k] = v
   305  	}
   306  	return m
   307  }
   308  
   309  // coalesceValues builds up a values map for a particular chart.
   310  //
   311  // Values in v will override the values in the chart.
   312  func coalesceValues(c *chart.Chart, v map[string]interface{}) {
   313  	for key, val := range c.Values {
   314  		if value, ok := v[key]; ok {
   315  			if value == nil {
   316  				// When the YAML value is null, we remove the value's key.
   317  				// This allows Helm's various sources of values (value files or --set) to
   318  				// remove incompatible keys from any previous chart, file, or set values.
   319  				delete(v, key)
   320  			} else if dest, ok := value.(map[string]interface{}); ok {
   321  				// if v[key] is a table, merge nv's val table into v[key].
   322  				src, ok := val.(map[string]interface{})
   323  				if !ok {
   324  					log.Printf("warning: skipped value for %s: Not a table.", key)
   325  					continue
   326  				}
   327  				// Because v has higher precedence than nv, dest values override src
   328  				// values.
   329  				CoalesceTables(dest, src)
   330  			}
   331  		} else {
   332  			// If the key is not in v, copy it from nv.
   333  			v[key] = val
   334  		}
   335  	}
   336  }
   337  
   338  // CoalesceTables merges a source map into a destination map.
   339  //
   340  // dest is considered authoritative.
   341  func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
   342  	if dst == nil || src == nil {
   343  		return src
   344  	}
   345  	// Because dest has higher precedence than src, dest values override src
   346  	// values.
   347  	for key, val := range src {
   348  		if istable(val) {
   349  			switch innerdst, ok := dst[key]; {
   350  			case !ok:
   351  				dst[key] = val
   352  			case istable(innerdst):
   353  				CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
   354  			default:
   355  				log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
   356  			}
   357  		} else if dv, ok := dst[key]; ok && istable(dv) {
   358  			log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val)
   359  		} else if !ok { // <- ok is still in scope from preceding conditional.
   360  			dst[key] = val
   361  		}
   362  	}
   363  	return dst
   364  }
   365  
   366  // ReleaseOptions represents the additional release options needed
   367  // for the composition of the final values struct
   368  type ReleaseOptions struct {
   369  	Name      string
   370  	Namespace string
   371  	IsUpgrade bool
   372  	IsInstall bool
   373  }
   374  
   375  // ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
   376  //
   377  // This takes both ReleaseOptions and Capabilities to merge into the render values.
   378  func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) {
   379  	if caps == nil {
   380  		caps = DefaultCapabilities
   381  	}
   382  	top := map[string]interface{}{
   383  		"Chart":        chrt.Metadata,
   384  		"Capabilities": caps,
   385  		"Release": map[string]interface{}{
   386  			"Name":      options.Name,
   387  			"Namespace": options.Namespace,
   388  			"IsUpgrade": options.IsUpgrade,
   389  			"IsInstall": options.IsInstall,
   390  			"Service":   "Helm",
   391  		},
   392  	}
   393  
   394  	vals, err := CoalesceValues(chrt, chrtVals)
   395  	if err != nil {
   396  		return top, err
   397  	}
   398  
   399  	if err := ValidateAgainstSchema(chrt, vals); err != nil {
   400  		errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s"
   401  		return top, fmt.Errorf(errFmt, err.Error())
   402  	}
   403  
   404  	top["Values"] = vals
   405  	return top, nil
   406  }
   407  
   408  // istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
   409  func istable(v interface{}) bool {
   410  	_, ok := v.(map[string]interface{})
   411  	return ok
   412  }
   413  
   414  // PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
   415  // The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
   416  // Given the following YAML data the value at path "chapter.one.title" is "Loomings".
   417  //
   418  //	chapter:
   419  //	  one:
   420  //	    title: "Loomings"
   421  func (v Values) PathValue(path string) (interface{}, error) {
   422  	if path == "" {
   423  		return nil, errors.New("YAML path cannot be empty")
   424  	}
   425  	return v.pathValue(parsePath(path))
   426  }
   427  
   428  func (v Values) pathValue(path []string) (interface{}, error) {
   429  	if len(path) == 1 {
   430  		// if exists must be root key not table
   431  		if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
   432  			return v[path[0]], nil
   433  		}
   434  		return nil, ErrNoValue(path[0])
   435  	}
   436  
   437  	key, path := path[len(path)-1], path[:len(path)-1]
   438  	// get our table for table path
   439  	t, err := v.Table(joinPath(path...))
   440  	if err != nil {
   441  		return nil, ErrNoValue(key)
   442  	}
   443  	// check table for key and ensure value is not a table
   444  	if k, ok := t[key]; ok && !istable(k) {
   445  		return k, nil
   446  	}
   447  	return nil, ErrNoValue(key)
   448  }
   449  
   450  func parsePath(key string) []string { return strings.Split(key, ".") }
   451  
   452  func joinPath(path ...string) string { return strings.Join(path, ".") }