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