github.com/amundsenjunior/helm@v2.8.0-rc.1.0.20180119233529-2b92431476e1+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  	"k8s.io/helm/pkg/version"
    27  )
    28  
    29  const (
    30  	requirementsName = "requirements.yaml"
    31  	lockfileName     = "requirements.lock"
    32  )
    33  
    34  var (
    35  	// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
    36  	ErrRequirementsNotFound = errors.New(requirementsName + " not found")
    37  	// ErrLockfileNotFound indicates that a requirements.lock is not found.
    38  	ErrLockfileNotFound = errors.New(lockfileName + " not found")
    39  )
    40  
    41  // Dependency describes a chart upon which another chart depends.
    42  //
    43  // Dependencies can be used to express developer intent, or to capture the state
    44  // of a chart.
    45  type Dependency struct {
    46  	// Name is the name of the dependency.
    47  	//
    48  	// This must mach the name in the dependency's Chart.yaml.
    49  	Name string `json:"name"`
    50  	// Version is the version (range) of this chart.
    51  	//
    52  	// A lock file will always produce a single version, while a dependency
    53  	// may contain a semantic version range.
    54  	Version string `json:"version,omitempty"`
    55  	// The URL to the repository.
    56  	//
    57  	// Appending `index.yaml` to this string should result in a URL that can be
    58  	// used to fetch the repository index.
    59  	Repository string `json:"repository"`
    60  	// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
    61  	Condition string `json:"condition,omitempty"`
    62  	// Tags can be used to group charts for enabling/disabling together
    63  	Tags []string `json:"tags,omitempty"`
    64  	// Enabled bool determines if chart should be loaded
    65  	Enabled bool `json:"enabled,omitempty"`
    66  	// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
    67  	// string or pair of child/parent sublist items.
    68  	ImportValues []interface{} `json:"import-values,omitempty"`
    69  	// Alias usable alias to be used for the chart
    70  	Alias string `json:"alias,omitempty"`
    71  }
    72  
    73  // ErrNoRequirementsFile to detect error condition
    74  type ErrNoRequirementsFile error
    75  
    76  // Requirements is a list of requirements for a chart.
    77  //
    78  // Requirements are charts upon which this chart depends. This expresses
    79  // developer intent.
    80  type Requirements struct {
    81  	Dependencies []*Dependency `json:"dependencies"`
    82  }
    83  
    84  // RequirementsLock is a lock file for requirements.
    85  //
    86  // It represents the state that the dependencies should be in.
    87  type RequirementsLock struct {
    88  	// Genderated is the date the lock file was last generated.
    89  	Generated time.Time `json:"generated"`
    90  	// Digest is a hash of the requirements file used to generate it.
    91  	Digest string `json:"digest"`
    92  	// Dependencies is the list of dependencies that this lock file has locked.
    93  	Dependencies []*Dependency `json:"dependencies"`
    94  }
    95  
    96  // LoadRequirements loads a requirements file from an in-memory chart.
    97  func LoadRequirements(c *chart.Chart) (*Requirements, error) {
    98  	var data []byte
    99  	for _, f := range c.Files {
   100  		if f.TypeUrl == requirementsName {
   101  			data = f.Value
   102  		}
   103  	}
   104  	if len(data) == 0 {
   105  		return nil, ErrRequirementsNotFound
   106  	}
   107  	r := &Requirements{}
   108  	return r, yaml.Unmarshal(data, r)
   109  }
   110  
   111  // LoadRequirementsLock loads a requirements lock file.
   112  func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
   113  	var data []byte
   114  	for _, f := range c.Files {
   115  		if f.TypeUrl == lockfileName {
   116  			data = f.Value
   117  		}
   118  	}
   119  	if len(data) == 0 {
   120  		return nil, ErrLockfileNotFound
   121  	}
   122  	r := &RequirementsLock{}
   123  	return r, yaml.Unmarshal(data, r)
   124  }
   125  
   126  // ProcessRequirementsConditions disables charts based on condition path value in values
   127  func ProcessRequirementsConditions(reqs *Requirements, cvals Values) {
   128  	var cond string
   129  	var conds []string
   130  	if reqs == nil || len(reqs.Dependencies) == 0 {
   131  		return
   132  	}
   133  	for _, r := range reqs.Dependencies {
   134  		var hasTrue, hasFalse bool
   135  		cond = string(r.Condition)
   136  		// check for list
   137  		if len(cond) > 0 {
   138  			if strings.Contains(cond, ",") {
   139  				conds = strings.Split(strings.TrimSpace(cond), ",")
   140  			} else {
   141  				conds = []string{strings.TrimSpace(cond)}
   142  			}
   143  			for _, c := range conds {
   144  				if len(c) > 0 {
   145  					// retrieve value
   146  					vv, err := cvals.PathValue(c)
   147  					if err == nil {
   148  						// if not bool, warn
   149  						if bv, ok := vv.(bool); ok {
   150  							if bv {
   151  								hasTrue = true
   152  							} else {
   153  								hasFalse = true
   154  							}
   155  						} else {
   156  							log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
   157  						}
   158  					} else if _, ok := err.(ErrNoValue); !ok {
   159  						// this is a real error
   160  						log.Printf("Warning: PathValue returned error %v", err)
   161  
   162  					}
   163  					if vv != nil {
   164  						// got first value, break loop
   165  						break
   166  					}
   167  				}
   168  			}
   169  			if !hasTrue && hasFalse {
   170  				r.Enabled = false
   171  			} else if hasTrue {
   172  				r.Enabled = true
   173  
   174  			}
   175  		}
   176  
   177  	}
   178  
   179  }
   180  
   181  // ProcessRequirementsTags disables charts based on tags in values
   182  func ProcessRequirementsTags(reqs *Requirements, cvals Values) {
   183  	vt, err := cvals.Table("tags")
   184  	if err != nil {
   185  		return
   186  
   187  	}
   188  	if reqs == nil || len(reqs.Dependencies) == 0 {
   189  		return
   190  	}
   191  	for _, r := range reqs.Dependencies {
   192  		if len(r.Tags) > 0 {
   193  			tags := r.Tags
   194  
   195  			var hasTrue, hasFalse bool
   196  			for _, k := range tags {
   197  				if b, ok := vt[k]; ok {
   198  					// if not bool, warn
   199  					if bv, ok := b.(bool); ok {
   200  						if bv {
   201  							hasTrue = true
   202  						} else {
   203  							hasFalse = true
   204  						}
   205  					} else {
   206  						log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name)
   207  					}
   208  				}
   209  			}
   210  			if !hasTrue && hasFalse {
   211  				r.Enabled = false
   212  			} else if hasTrue || !hasTrue && !hasFalse {
   213  				r.Enabled = true
   214  
   215  			}
   216  
   217  		}
   218  	}
   219  
   220  }
   221  
   222  func getAliasDependency(charts []*chart.Chart, aliasChart *Dependency) *chart.Chart {
   223  	var chartFound chart.Chart
   224  	for _, existingChart := range charts {
   225  		if existingChart == nil {
   226  			continue
   227  		}
   228  		if existingChart.Metadata == nil {
   229  			continue
   230  		}
   231  		if existingChart.Metadata.Name != aliasChart.Name {
   232  			continue
   233  		}
   234  		if !version.IsCompatibleRange(aliasChart.Version, existingChart.Metadata.Version) {
   235  			continue
   236  		}
   237  		chartFound = *existingChart
   238  		newMetadata := *existingChart.Metadata
   239  		if aliasChart.Alias != "" {
   240  			newMetadata.Name = aliasChart.Alias
   241  		}
   242  		chartFound.Metadata = &newMetadata
   243  		return &chartFound
   244  	}
   245  	return nil
   246  }
   247  
   248  // ProcessRequirementsEnabled removes disabled charts from dependencies
   249  func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
   250  	reqs, err := LoadRequirements(c)
   251  	if err != nil {
   252  		// if not just missing requirements file, return error
   253  		if nerr, ok := err.(ErrNoRequirementsFile); !ok {
   254  			return nerr
   255  		}
   256  
   257  		// no requirements to process
   258  		return nil
   259  	}
   260  
   261  	var chartDependencies []*chart.Chart
   262  	// If any dependency is not a part of requirements.yaml
   263  	// then this should be added to chartDependencies.
   264  	// However, if the dependency is already specified in requirements.yaml
   265  	// we should not add it, as it would be anyways processed from requirements.yaml
   266  
   267  	for _, existingDependency := range c.Dependencies {
   268  		var dependencyFound bool
   269  		for _, req := range reqs.Dependencies {
   270  			if existingDependency.Metadata.Name == req.Name && version.IsCompatibleRange(req.Version, existingDependency.Metadata.Version) {
   271  				dependencyFound = true
   272  				break
   273  			}
   274  		}
   275  		if !dependencyFound {
   276  			chartDependencies = append(chartDependencies, existingDependency)
   277  		}
   278  	}
   279  
   280  	for _, req := range reqs.Dependencies {
   281  		if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil {
   282  			chartDependencies = append(chartDependencies, chartDependency)
   283  		}
   284  		if req.Alias != "" {
   285  			req.Name = req.Alias
   286  		}
   287  	}
   288  	c.Dependencies = chartDependencies
   289  
   290  	// set all to true
   291  	for _, lr := range reqs.Dependencies {
   292  		lr.Enabled = true
   293  	}
   294  	cvals, err := CoalesceValues(c, v)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	// convert our values back into config
   299  	yvals, err := cvals.YAML()
   300  	if err != nil {
   301  		return err
   302  	}
   303  	cc := chart.Config{Raw: yvals}
   304  	// flag dependencies as enabled/disabled
   305  	ProcessRequirementsTags(reqs, cvals)
   306  	ProcessRequirementsConditions(reqs, cvals)
   307  	// make a map of charts to remove
   308  	rm := map[string]bool{}
   309  	for _, r := range reqs.Dependencies {
   310  		if !r.Enabled {
   311  			// remove disabled chart
   312  			rm[r.Name] = true
   313  		}
   314  	}
   315  	// don't keep disabled charts in new slice
   316  	cd := []*chart.Chart{}
   317  	copy(cd, c.Dependencies[:0])
   318  	for _, n := range c.Dependencies {
   319  		if _, ok := rm[n.Metadata.Name]; !ok {
   320  			cd = append(cd, n)
   321  		}
   322  
   323  	}
   324  	// recursively call self to process sub dependencies
   325  	for _, t := range cd {
   326  		err := ProcessRequirementsEnabled(t, &cc)
   327  		// if its not just missing requirements file, return error
   328  		if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
   329  			return nerr
   330  		}
   331  	}
   332  	c.Dependencies = cd
   333  
   334  	return nil
   335  }
   336  
   337  // pathToMap creates a nested map given a YAML path in dot notation.
   338  func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
   339  	if path == "." {
   340  		return data
   341  	}
   342  	ap := strings.Split(path, ".")
   343  	if len(ap) == 0 {
   344  		return nil
   345  	}
   346  	n := []map[string]interface{}{}
   347  	// created nested map for each key, adding to slice
   348  	for _, v := range ap {
   349  		nm := make(map[string]interface{})
   350  		nm[v] = make(map[string]interface{})
   351  		n = append(n, nm)
   352  	}
   353  	// find the last key (map) and set our data
   354  	for i, d := range n {
   355  		for k := range d {
   356  			z := i + 1
   357  			if z == len(n) {
   358  				n[i][k] = data
   359  				break
   360  			}
   361  			n[i][k] = n[z]
   362  		}
   363  	}
   364  
   365  	return n[0]
   366  }
   367  
   368  // getParents returns a slice of parent charts in reverse order.
   369  func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
   370  	if len(out) == 0 {
   371  		out = []*chart.Chart{c}
   372  	}
   373  	for _, ch := range c.Dependencies {
   374  		if len(ch.Dependencies) > 0 {
   375  			out = append(out, ch)
   376  			out = getParents(ch, out)
   377  		}
   378  	}
   379  
   380  	return out
   381  }
   382  
   383  // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
   384  func processImportValues(c *chart.Chart) error {
   385  	reqs, err := LoadRequirements(c)
   386  	if err != nil {
   387  		return err
   388  	}
   389  	// combine chart values and empty config to get Values
   390  	cvals, err := CoalesceValues(c, &chart.Config{})
   391  	if err != nil {
   392  		return err
   393  	}
   394  	b := make(map[string]interface{}, 0)
   395  	// import values from each dependency if specified in import-values
   396  	for _, r := range reqs.Dependencies {
   397  		if len(r.ImportValues) > 0 {
   398  			var outiv []interface{}
   399  			for _, riv := range r.ImportValues {
   400  				switch iv := riv.(type) {
   401  				case map[string]interface{}:
   402  					nm := map[string]string{
   403  						"child":  iv["child"].(string),
   404  						"parent": iv["parent"].(string),
   405  					}
   406  					outiv = append(outiv, nm)
   407  					s := r.Name + "." + nm["child"]
   408  					// get child table
   409  					vv, err := cvals.Table(s)
   410  					if err != nil {
   411  						log.Printf("Warning: ImportValues missing table: %v", err)
   412  						continue
   413  					}
   414  					// create value map from child to be merged into parent
   415  					vm := pathToMap(nm["parent"], vv.AsMap())
   416  					b = coalesceTables(cvals, vm)
   417  				case string:
   418  					nm := map[string]string{
   419  						"child":  "exports." + iv,
   420  						"parent": ".",
   421  					}
   422  					outiv = append(outiv, nm)
   423  					s := r.Name + "." + nm["child"]
   424  					vm, err := cvals.Table(s)
   425  					if err != nil {
   426  						log.Printf("Warning: ImportValues missing table: %v", err)
   427  						continue
   428  					}
   429  					b = coalesceTables(b, vm.AsMap())
   430  				}
   431  			}
   432  			// set our formatted import values
   433  			r.ImportValues = outiv
   434  		}
   435  	}
   436  	b = coalesceTables(b, cvals)
   437  	y, err := yaml.Marshal(b)
   438  	if err != nil {
   439  		return err
   440  	}
   441  
   442  	// set the new values
   443  	c.Values = &chart.Config{Raw: string(y)}
   444  
   445  	return nil
   446  }
   447  
   448  // ProcessRequirementsImportValues imports specified chart values from child to parent.
   449  func ProcessRequirementsImportValues(c *chart.Chart) error {
   450  	pc := getParents(c, nil)
   451  	for i := len(pc) - 1; i >= 0; i-- {
   452  		processImportValues(pc[i])
   453  	}
   454  
   455  	return nil
   456  }