github.com/sdbaiguanghe/helm@v2.16.7+incompatible/pkg/chartutil/requirements.go (about)

     1  /*
     2  Copyright The Helm Authors.
     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 match 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  	// Generated 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, cpath string) {
   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(cpath + 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  	return doProcessRequirementsEnabled(c, v, "")
   251  }
   252  
   253  func doProcessRequirementsEnabled(c *chart.Chart, v *chart.Config, path string) error {
   254  	reqs, err := LoadRequirements(c)
   255  	if err != nil {
   256  		// if not just missing requirements file, return error
   257  		if nerr, ok := err.(ErrNoRequirementsFile); !ok {
   258  			return nerr
   259  		}
   260  
   261  		// no requirements to process
   262  		return nil
   263  	}
   264  
   265  	var chartDependencies []*chart.Chart
   266  	// If any dependency is not a part of requirements.yaml
   267  	// then this should be added to chartDependencies.
   268  	// However, if the dependency is already specified in requirements.yaml
   269  	// we should not add it, as it would be anyways processed from requirements.yaml
   270  
   271  	for _, existingDependency := range c.Dependencies {
   272  		var dependencyFound bool
   273  		for _, req := range reqs.Dependencies {
   274  			if existingDependency.Metadata.Name == req.Name && version.IsCompatibleRange(req.Version, existingDependency.Metadata.Version) {
   275  				dependencyFound = true
   276  				break
   277  			}
   278  		}
   279  		if !dependencyFound {
   280  			chartDependencies = append(chartDependencies, existingDependency)
   281  		}
   282  	}
   283  
   284  	for _, req := range reqs.Dependencies {
   285  		if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil {
   286  			chartDependencies = append(chartDependencies, chartDependency)
   287  		}
   288  		if req.Alias != "" {
   289  			req.Name = req.Alias
   290  		}
   291  	}
   292  	c.Dependencies = chartDependencies
   293  
   294  	// set all to true
   295  	for _, lr := range reqs.Dependencies {
   296  		lr.Enabled = true
   297  	}
   298  	cvals, err := CoalesceValues(c, v)
   299  	if err != nil {
   300  		return err
   301  	}
   302  	// convert our values back into config
   303  	yvals, err := cvals.YAML()
   304  	if err != nil {
   305  		return err
   306  	}
   307  	cc := chart.Config{Raw: yvals}
   308  	// flag dependencies as enabled/disabled
   309  	ProcessRequirementsTags(reqs, cvals)
   310  	ProcessRequirementsConditions(reqs, cvals, path)
   311  	// make a map of charts to remove
   312  	rm := map[string]bool{}
   313  	for _, r := range reqs.Dependencies {
   314  		if !r.Enabled {
   315  			// remove disabled chart
   316  			rm[r.Name] = true
   317  		}
   318  	}
   319  	// don't keep disabled charts in new slice
   320  	cd := []*chart.Chart{}
   321  	copy(cd, c.Dependencies[:0])
   322  	for _, n := range c.Dependencies {
   323  		if _, ok := rm[n.Metadata.Name]; !ok {
   324  			cd = append(cd, n)
   325  		}
   326  
   327  	}
   328  	// recursively call self to process sub dependencies
   329  	for _, t := range cd {
   330  		subpath := path + t.Metadata.Name + "."
   331  		err := doProcessRequirementsEnabled(t, &cc, subpath)
   332  		// if its not just missing requirements file, return error
   333  		if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
   334  			return nerr
   335  		}
   336  	}
   337  	c.Dependencies = cd
   338  
   339  	return nil
   340  }
   341  
   342  // pathToMap creates a nested map given a YAML path in dot notation.
   343  func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
   344  	if path == "." {
   345  		return data
   346  	}
   347  	ap := strings.Split(path, ".")
   348  	if len(ap) == 0 {
   349  		return nil
   350  	}
   351  	n := []map[string]interface{}{}
   352  	// created nested map for each key, adding to slice
   353  	for _, v := range ap {
   354  		nm := make(map[string]interface{})
   355  		nm[v] = make(map[string]interface{})
   356  		n = append(n, nm)
   357  	}
   358  	// find the last key (map) and set our data
   359  	for i, d := range n {
   360  		for k := range d {
   361  			z := i + 1
   362  			if z == len(n) {
   363  				n[i][k] = data
   364  				break
   365  			}
   366  			n[i][k] = n[z]
   367  		}
   368  	}
   369  
   370  	return n[0]
   371  }
   372  
   373  // getParents returns a slice of parent charts in reverse order.
   374  func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
   375  	if len(out) == 0 {
   376  		out = []*chart.Chart{c}
   377  	}
   378  	for _, ch := range c.Dependencies {
   379  		if len(ch.Dependencies) > 0 {
   380  			out = append(out, ch)
   381  			out = getParents(ch, out)
   382  		}
   383  	}
   384  
   385  	return out
   386  }
   387  
   388  // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
   389  func processImportValues(c *chart.Chart) error {
   390  	reqs, err := LoadRequirements(c)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	// combine chart values and empty config to get Values
   395  	cvals, err := CoalesceValues(c, &chart.Config{})
   396  	if err != nil {
   397  		return err
   398  	}
   399  	b := make(map[string]interface{}, 0)
   400  	// import values from each dependency if specified in import-values
   401  	for _, r := range reqs.Dependencies {
   402  		// only process raw requirement that is found in chart's dependencies (enabled)
   403  		found := false
   404  		name := r.Name
   405  		for _, v := range c.Dependencies {
   406  			if v.Metadata.Name == r.Name {
   407  				found = true
   408  			}
   409  			if v.Metadata.Name == r.Alias {
   410  				found = true
   411  				name = r.Alias
   412  			}
   413  		}
   414  		if !found {
   415  			continue
   416  		}
   417  		if len(r.ImportValues) > 0 {
   418  			var outiv []interface{}
   419  			for _, riv := range r.ImportValues {
   420  				nm := make(map[string]string, 0)
   421  				switch iv := riv.(type) {
   422  				case map[string]interface{}:
   423  					nm["child"] = iv["child"].(string)
   424  					nm["parent"] = iv["parent"].(string)
   425  				case string:
   426  					nm["child"] = "exports." + iv
   427  					nm["parent"] = "."
   428  				}
   429  
   430  				outiv = append(outiv, nm)
   431  				s := name + "." + nm["child"]
   432  				// get child table
   433  				vv, err := cvals.Table(s)
   434  				if err != nil {
   435  					log.Printf("Warning: ImportValues missing table: %v", err)
   436  					continue
   437  				}
   438  				// create value map from child to be merged into parent
   439  				vm := pathToMap(nm["parent"], vv.AsMap())
   440  				b = coalesceTables(b, vm, c.Metadata.Name)
   441  
   442  			}
   443  			// set our formatted import values
   444  			r.ImportValues = outiv
   445  		}
   446  	}
   447  	b = coalesceTables(b, cvals, c.Metadata.Name)
   448  	y, err := yaml.Marshal(b)
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	// set the new values
   454  	c.Values = &chart.Config{Raw: string(y)}
   455  
   456  	return nil
   457  }
   458  
   459  // ProcessRequirementsImportValues imports specified chart values from child to parent.
   460  func ProcessRequirementsImportValues(c *chart.Chart) error {
   461  	pc := getParents(c, nil)
   462  	for i := len(pc) - 1; i >= 0; i-- {
   463  		processImportValues(pc[i])
   464  	}
   465  
   466  	return nil
   467  }