github.com/sgoings/helm@v2.0.0-alpha.2.0.20170406211108-734e92851ac3+incompatible/pkg/chartutil/requirements.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package chartutil
    17  
    18  import (
    19  	"errors"
    20  	"log"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/ghodss/yaml"
    25  	"k8s.io/helm/pkg/proto/hapi/chart"
    26  )
    27  
    28  const (
    29  	requirementsName = "requirements.yaml"
    30  	lockfileName     = "requirements.lock"
    31  )
    32  
    33  var (
    34  	// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
    35  	ErrRequirementsNotFound = errors.New(requirementsName + " not found")
    36  	// ErrLockfileNotFound indicates that a requirements.lock is not found.
    37  	ErrLockfileNotFound = errors.New(lockfileName + " not found")
    38  )
    39  
    40  // Dependency describes a chart upon which another chart depends.
    41  //
    42  // Dependencies can be used to express developer intent, or to capture the state
    43  // of a chart.
    44  type Dependency struct {
    45  	// Name is the name of the dependency.
    46  	//
    47  	// This must mach the name in the dependency's Chart.yaml.
    48  	Name string `json:"name"`
    49  	// Version is the version (range) of this chart.
    50  	//
    51  	// A lock file will always produce a single version, while a dependency
    52  	// may contain a semantic version range.
    53  	Version string `json:"version,omitempty"`
    54  	// The URL to the repository.
    55  	//
    56  	// Appending `index.yaml` to this string should result in a URL that can be
    57  	// used to fetch the repository index.
    58  	Repository string `json:"repository"`
    59  	// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
    60  	Condition string `json:"condition"`
    61  	// Tags can be used to group charts for enabling/disabling together
    62  	Tags []string `json:"tags"`
    63  	// Enabled bool determines if chart should be loaded
    64  	Enabled bool `json:"enabled"`
    65  	// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
    66  	// string or pair of child/parent sublist items.
    67  	ImportValues []interface{} `json:"import-values"`
    68  }
    69  
    70  // ErrNoRequirementsFile to detect error condition
    71  type ErrNoRequirementsFile error
    72  
    73  // Requirements is a list of requirements for a chart.
    74  //
    75  // Requirements are charts upon which this chart depends. This expresses
    76  // developer intent.
    77  type Requirements struct {
    78  	Dependencies []*Dependency `json:"dependencies"`
    79  }
    80  
    81  // RequirementsLock is a lock file for requirements.
    82  //
    83  // It represents the state that the dependencies should be in.
    84  type RequirementsLock struct {
    85  	// Genderated is the date the lock file was last generated.
    86  	Generated time.Time `json:"generated"`
    87  	// Digest is a hash of the requirements file used to generate it.
    88  	Digest string `json:"digest"`
    89  	// Dependencies is the list of dependencies that this lock file has locked.
    90  	Dependencies []*Dependency `json:"dependencies"`
    91  }
    92  
    93  // LoadRequirements loads a requirements file from an in-memory chart.
    94  func LoadRequirements(c *chart.Chart) (*Requirements, error) {
    95  	var data []byte
    96  	for _, f := range c.Files {
    97  		if f.TypeUrl == requirementsName {
    98  			data = f.Value
    99  		}
   100  	}
   101  	if len(data) == 0 {
   102  		return nil, ErrRequirementsNotFound
   103  	}
   104  	r := &Requirements{}
   105  	return r, yaml.Unmarshal(data, r)
   106  }
   107  
   108  // LoadRequirementsLock loads a requirements lock file.
   109  func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
   110  	var data []byte
   111  	for _, f := range c.Files {
   112  		if f.TypeUrl == lockfileName {
   113  			data = f.Value
   114  		}
   115  	}
   116  	if len(data) == 0 {
   117  		return nil, ErrLockfileNotFound
   118  	}
   119  	r := &RequirementsLock{}
   120  	return r, yaml.Unmarshal(data, r)
   121  }
   122  
   123  // ProcessRequirementsConditions disables charts based on condition path value in values
   124  func ProcessRequirementsConditions(reqs *Requirements, cvals Values) {
   125  	var cond string
   126  	var conds []string
   127  	if reqs == nil || len(reqs.Dependencies) == 0 {
   128  		return
   129  	}
   130  	for _, r := range reqs.Dependencies {
   131  		var hasTrue, hasFalse bool
   132  		cond = string(r.Condition)
   133  		// check for list
   134  		if len(cond) > 0 {
   135  			if strings.Contains(cond, ",") {
   136  				conds = strings.Split(strings.TrimSpace(cond), ",")
   137  			} else {
   138  				conds = []string{strings.TrimSpace(cond)}
   139  			}
   140  			for _, c := range conds {
   141  				if len(c) > 0 {
   142  					// retrieve value
   143  					vv, err := cvals.PathValue(c)
   144  					if err == nil {
   145  						// if not bool, warn
   146  						if bv, ok := vv.(bool); ok {
   147  							if bv {
   148  								hasTrue = true
   149  							} else {
   150  								hasFalse = true
   151  							}
   152  						} else {
   153  							log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
   154  						}
   155  					} else if _, ok := err.(ErrNoValue); !ok {
   156  						// this is a real error
   157  						log.Printf("Warning: PathValue returned error %v", err)
   158  
   159  					}
   160  					if vv != nil {
   161  						// got first value, break loop
   162  						break
   163  					}
   164  				}
   165  			}
   166  			if !hasTrue && hasFalse {
   167  				r.Enabled = false
   168  			} else if hasTrue {
   169  				r.Enabled = true
   170  
   171  			}
   172  		}
   173  
   174  	}
   175  
   176  }
   177  
   178  // ProcessRequirementsTags disables charts based on tags in values
   179  func ProcessRequirementsTags(reqs *Requirements, cvals Values) {
   180  	vt, err := cvals.Table("tags")
   181  	if err != nil {
   182  		return
   183  
   184  	}
   185  	if reqs == nil || len(reqs.Dependencies) == 0 {
   186  		return
   187  	}
   188  	for _, r := range reqs.Dependencies {
   189  		if len(r.Tags) > 0 {
   190  			tags := r.Tags
   191  
   192  			var hasTrue, hasFalse bool
   193  			for _, k := range tags {
   194  				if b, ok := vt[k]; ok {
   195  					// if not bool, warn
   196  					if bv, ok := b.(bool); ok {
   197  						if bv {
   198  							hasTrue = true
   199  						} else {
   200  							hasFalse = true
   201  						}
   202  					} else {
   203  						log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name)
   204  					}
   205  				}
   206  			}
   207  			if !hasTrue && hasFalse {
   208  				r.Enabled = false
   209  			} else if hasTrue || !hasTrue && !hasFalse {
   210  				r.Enabled = true
   211  
   212  			}
   213  
   214  		}
   215  	}
   216  
   217  }
   218  
   219  // ProcessRequirementsEnabled removes disabled charts from dependencies
   220  func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
   221  	reqs, err := LoadRequirements(c)
   222  	if err != nil {
   223  		// if not just missing requirements file, return error
   224  		if nerr, ok := err.(ErrNoRequirementsFile); !ok {
   225  			return nerr
   226  		}
   227  
   228  		// no requirements to process
   229  		return nil
   230  	}
   231  	// set all to true
   232  	for _, lr := range reqs.Dependencies {
   233  		lr.Enabled = true
   234  	}
   235  	cvals, err := CoalesceValues(c, v)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	// convert our values back into config
   240  	yvals, err := cvals.YAML()
   241  	if err != nil {
   242  		return err
   243  	}
   244  	cc := chart.Config{Raw: yvals}
   245  	// flag dependencies as enabled/disabled
   246  	ProcessRequirementsTags(reqs, cvals)
   247  	ProcessRequirementsConditions(reqs, cvals)
   248  	// make a map of charts to remove
   249  	rm := map[string]bool{}
   250  	for _, r := range reqs.Dependencies {
   251  		if !r.Enabled {
   252  			// remove disabled chart
   253  			rm[r.Name] = true
   254  		}
   255  	}
   256  	// don't keep disabled charts in new slice
   257  	cd := []*chart.Chart{}
   258  	copy(cd, c.Dependencies[:0])
   259  	for _, n := range c.Dependencies {
   260  		if _, ok := rm[n.Metadata.Name]; !ok {
   261  			cd = append(cd, n)
   262  		}
   263  
   264  	}
   265  	// recursively call self to process sub dependencies
   266  	for _, t := range cd {
   267  		err := ProcessRequirementsEnabled(t, &cc)
   268  		// if its not just missing requirements file, return error
   269  		if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
   270  			return nerr
   271  		}
   272  	}
   273  	c.Dependencies = cd
   274  
   275  	return nil
   276  }
   277  
   278  // pathToMap creates a nested map given a YAML path in dot notation.
   279  func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
   280  	if path == "." {
   281  		return data
   282  	}
   283  	ap := strings.Split(path, ".")
   284  	if len(ap) == 0 {
   285  		return nil
   286  	}
   287  	n := []map[string]interface{}{}
   288  	// created nested map for each key, adding to slice
   289  	for _, v := range ap {
   290  		nm := make(map[string]interface{})
   291  		nm[v] = make(map[string]interface{})
   292  		n = append(n, nm)
   293  	}
   294  	// find the last key (map) and set our data
   295  	for i, d := range n {
   296  		for k := range d {
   297  			z := i + 1
   298  			if z == len(n) {
   299  				n[i][k] = data
   300  				break
   301  			}
   302  			n[i][k] = n[z]
   303  		}
   304  	}
   305  
   306  	return n[0]
   307  }
   308  
   309  // getParents returns a slice of parent charts in reverse order.
   310  func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
   311  	if len(out) == 0 {
   312  		out = []*chart.Chart{c}
   313  	}
   314  	for _, ch := range c.Dependencies {
   315  		if len(ch.Dependencies) > 0 {
   316  			out = append(out, ch)
   317  			out = getParents(ch, out)
   318  		}
   319  	}
   320  
   321  	return out
   322  }
   323  
   324  // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
   325  func processImportValues(c *chart.Chart, v *chart.Config) error {
   326  	reqs, err := LoadRequirements(c)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	// combine chart values and its dependencies' values
   331  	cvals, err := CoalesceValues(c, v)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	nv := v.GetValues()
   336  	b := make(map[string]interface{}, len(nv))
   337  	// convert values to map
   338  	for kk, vvv := range nv {
   339  		b[kk] = vvv
   340  	}
   341  	// import values from each dependency if specified in import-values
   342  	for _, r := range reqs.Dependencies {
   343  		if len(r.ImportValues) > 0 {
   344  			var outiv []interface{}
   345  			for _, riv := range r.ImportValues {
   346  				switch iv := riv.(type) {
   347  				case map[string]interface{}:
   348  					nm := map[string]string{
   349  						"child":  iv["child"].(string),
   350  						"parent": iv["parent"].(string),
   351  					}
   352  					outiv = append(outiv, nm)
   353  					s := r.Name + "." + nm["child"]
   354  					// get child table
   355  					vv, err := cvals.Table(s)
   356  					if err != nil {
   357  						log.Printf("Warning: ImportValues missing table: %v", err)
   358  						continue
   359  					}
   360  					// create value map from child to be merged into parent
   361  					vm := pathToMap(nm["parent"], vv.AsMap())
   362  					b = coalesceTables(cvals, vm)
   363  				case string:
   364  					nm := map[string]string{
   365  						"child":  "exports." + iv,
   366  						"parent": ".",
   367  					}
   368  					outiv = append(outiv, nm)
   369  					s := r.Name + "." + nm["child"]
   370  					vm, err := cvals.Table(s)
   371  					if err != nil {
   372  						log.Printf("Warning: ImportValues missing table: %v", err)
   373  						continue
   374  					}
   375  					b = coalesceTables(b, vm.AsMap())
   376  				}
   377  			}
   378  			// set our formatted import values
   379  			r.ImportValues = outiv
   380  		}
   381  	}
   382  	b = coalesceTables(b, cvals)
   383  	y, err := yaml.Marshal(b)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	// set the new values
   388  	c.Values.Raw = string(y)
   389  
   390  	return nil
   391  }
   392  
   393  // ProcessRequirementsImportValues imports specified chart values from child to parent.
   394  func ProcessRequirementsImportValues(c *chart.Chart, v *chart.Config) error {
   395  	pc := getParents(c, nil)
   396  	for i := len(pc) - 1; i >= 0; i-- {
   397  		processImportValues(pc[i], v)
   398  	}
   399  
   400  	return nil
   401  }