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