github.com/caicloud/helm@v2.5.0+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  	// Alias usable alias to be used for the chart
    69  	Alias string `json:"alias"`
    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  	for _, req := range reqs.Dependencies {
   262  		if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil {
   263  			chartDependencies = append(chartDependencies, chartDependency)
   264  		}
   265  		if req.Alias != "" {
   266  			req.Name = req.Alias
   267  		}
   268  	}
   269  	c.Dependencies = chartDependencies
   270  
   271  	// set all to true
   272  	for _, lr := range reqs.Dependencies {
   273  		lr.Enabled = true
   274  	}
   275  	cvals, err := CoalesceValues(c, v)
   276  	if err != nil {
   277  		return err
   278  	}
   279  	// convert our values back into config
   280  	yvals, err := cvals.YAML()
   281  	if err != nil {
   282  		return err
   283  	}
   284  	cc := chart.Config{Raw: yvals}
   285  	// flag dependencies as enabled/disabled
   286  	ProcessRequirementsTags(reqs, cvals)
   287  	ProcessRequirementsConditions(reqs, cvals)
   288  	// make a map of charts to remove
   289  	rm := map[string]bool{}
   290  	for _, r := range reqs.Dependencies {
   291  		if !r.Enabled {
   292  			// remove disabled chart
   293  			rm[r.Name] = true
   294  		}
   295  	}
   296  	// don't keep disabled charts in new slice
   297  	cd := []*chart.Chart{}
   298  	copy(cd, c.Dependencies[:0])
   299  	for _, n := range c.Dependencies {
   300  		if _, ok := rm[n.Metadata.Name]; !ok {
   301  			cd = append(cd, n)
   302  		}
   303  
   304  	}
   305  	// recursively call self to process sub dependencies
   306  	for _, t := range cd {
   307  		err := ProcessRequirementsEnabled(t, &cc)
   308  		// if its not just missing requirements file, return error
   309  		if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
   310  			return nerr
   311  		}
   312  	}
   313  	c.Dependencies = cd
   314  
   315  	return nil
   316  }
   317  
   318  // pathToMap creates a nested map given a YAML path in dot notation.
   319  func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
   320  	if path == "." {
   321  		return data
   322  	}
   323  	ap := strings.Split(path, ".")
   324  	if len(ap) == 0 {
   325  		return nil
   326  	}
   327  	n := []map[string]interface{}{}
   328  	// created nested map for each key, adding to slice
   329  	for _, v := range ap {
   330  		nm := make(map[string]interface{})
   331  		nm[v] = make(map[string]interface{})
   332  		n = append(n, nm)
   333  	}
   334  	// find the last key (map) and set our data
   335  	for i, d := range n {
   336  		for k := range d {
   337  			z := i + 1
   338  			if z == len(n) {
   339  				n[i][k] = data
   340  				break
   341  			}
   342  			n[i][k] = n[z]
   343  		}
   344  	}
   345  
   346  	return n[0]
   347  }
   348  
   349  // getParents returns a slice of parent charts in reverse order.
   350  func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
   351  	if len(out) == 0 {
   352  		out = []*chart.Chart{c}
   353  	}
   354  	for _, ch := range c.Dependencies {
   355  		if len(ch.Dependencies) > 0 {
   356  			out = append(out, ch)
   357  			out = getParents(ch, out)
   358  		}
   359  	}
   360  
   361  	return out
   362  }
   363  
   364  // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
   365  func processImportValues(c *chart.Chart) error {
   366  	reqs, err := LoadRequirements(c)
   367  	if err != nil {
   368  		return err
   369  	}
   370  	// combine chart values and empty config to get Values
   371  	cvals, err := CoalesceValues(c, &chart.Config{})
   372  	if err != nil {
   373  		return err
   374  	}
   375  	b := make(map[string]interface{}, 0)
   376  	// import values from each dependency if specified in import-values
   377  	for _, r := range reqs.Dependencies {
   378  		if len(r.ImportValues) > 0 {
   379  			var outiv []interface{}
   380  			for _, riv := range r.ImportValues {
   381  				switch iv := riv.(type) {
   382  				case map[string]interface{}:
   383  					nm := map[string]string{
   384  						"child":  iv["child"].(string),
   385  						"parent": iv["parent"].(string),
   386  					}
   387  					outiv = append(outiv, nm)
   388  					s := r.Name + "." + nm["child"]
   389  					// get child table
   390  					vv, err := cvals.Table(s)
   391  					if err != nil {
   392  						log.Printf("Warning: ImportValues missing table: %v", err)
   393  						continue
   394  					}
   395  					// create value map from child to be merged into parent
   396  					vm := pathToMap(nm["parent"], vv.AsMap())
   397  					b = coalesceTables(cvals, vm)
   398  				case string:
   399  					nm := map[string]string{
   400  						"child":  "exports." + iv,
   401  						"parent": ".",
   402  					}
   403  					outiv = append(outiv, nm)
   404  					s := r.Name + "." + nm["child"]
   405  					vm, err := cvals.Table(s)
   406  					if err != nil {
   407  						log.Printf("Warning: ImportValues missing table: %v", err)
   408  						continue
   409  					}
   410  					b = coalesceTables(b, vm.AsMap())
   411  				}
   412  			}
   413  			// set our formatted import values
   414  			r.ImportValues = outiv
   415  		}
   416  	}
   417  	b = coalesceTables(b, cvals)
   418  	y, err := yaml.Marshal(b)
   419  	if err != nil {
   420  		return err
   421  	}
   422  
   423  	// set the new values
   424  	c.Values = &chart.Config{Raw: string(y)}
   425  
   426  	return nil
   427  }
   428  
   429  // ProcessRequirementsImportValues imports specified chart values from child to parent.
   430  func ProcessRequirementsImportValues(c *chart.Chart) error {
   431  	pc := getParents(c, nil)
   432  	for i := len(pc) - 1; i >= 0; i-- {
   433  		processImportValues(pc[i])
   434  	}
   435  
   436  	return nil
   437  }