github.com/cloudposse/helm@v2.2.3+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  }
    66  
    67  // ErrNoRequirementsFile to detect error condition
    68  type ErrNoRequirementsFile error
    69  
    70  // Requirements is a list of requirements for a chart.
    71  //
    72  // Requirements are charts upon which this chart depends. This expresses
    73  // developer intent.
    74  type Requirements struct {
    75  	Dependencies []*Dependency `json:"dependencies"`
    76  }
    77  
    78  // RequirementsLock is a lock file for requirements.
    79  //
    80  // It represents the state that the dependencies should be in.
    81  type RequirementsLock struct {
    82  	// Genderated is the date the lock file was last generated.
    83  	Generated time.Time `json:"generated"`
    84  	// Digest is a hash of the requirements file used to generate it.
    85  	Digest string `json:"digest"`
    86  	// Dependencies is the list of dependencies that this lock file has locked.
    87  	Dependencies []*Dependency `json:"dependencies"`
    88  }
    89  
    90  // LoadRequirements loads a requirements file from an in-memory chart.
    91  func LoadRequirements(c *chart.Chart) (*Requirements, error) {
    92  	var data []byte
    93  	for _, f := range c.Files {
    94  		if f.TypeUrl == requirementsName {
    95  			data = f.Value
    96  		}
    97  	}
    98  	if len(data) == 0 {
    99  		return nil, ErrRequirementsNotFound
   100  	}
   101  	r := &Requirements{}
   102  	return r, yaml.Unmarshal(data, r)
   103  }
   104  
   105  // LoadRequirementsLock loads a requirements lock file.
   106  func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
   107  	var data []byte
   108  	for _, f := range c.Files {
   109  		if f.TypeUrl == lockfileName {
   110  			data = f.Value
   111  		}
   112  	}
   113  	if len(data) == 0 {
   114  		return nil, ErrLockfileNotFound
   115  	}
   116  	r := &RequirementsLock{}
   117  	return r, yaml.Unmarshal(data, r)
   118  }
   119  
   120  // ProcessRequirementsConditions disables charts based on condition path value in values
   121  func ProcessRequirementsConditions(reqs *Requirements, cvals Values) {
   122  	var cond string
   123  	var conds []string
   124  	if reqs == nil || len(reqs.Dependencies) == 0 {
   125  		return
   126  	}
   127  	for _, r := range reqs.Dependencies {
   128  		var hasTrue, hasFalse bool
   129  		cond = string(r.Condition)
   130  		// check for list
   131  		if len(cond) > 0 {
   132  			if strings.Contains(cond, ",") {
   133  				conds = strings.Split(strings.TrimSpace(cond), ",")
   134  			} else {
   135  				conds = []string{strings.TrimSpace(cond)}
   136  			}
   137  			for _, c := range conds {
   138  				if len(c) > 0 {
   139  					// retrieve value
   140  					vv, err := cvals.PathValue(c)
   141  					if err == nil {
   142  						// if not bool, warn
   143  						if bv, ok := vv.(bool); ok {
   144  							if bv {
   145  								hasTrue = true
   146  							} else {
   147  								hasFalse = true
   148  							}
   149  						} else {
   150  							log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
   151  						}
   152  					} else if _, ok := err.(ErrNoValue); !ok {
   153  						// this is a real error
   154  						log.Printf("Warning: PathValue returned error %v", err)
   155  
   156  					}
   157  					if vv != nil {
   158  						// got first value, break loop
   159  						break
   160  					}
   161  				}
   162  			}
   163  			if !hasTrue && hasFalse {
   164  				r.Enabled = false
   165  			} else if hasTrue {
   166  				r.Enabled = true
   167  
   168  			}
   169  		}
   170  
   171  	}
   172  
   173  }
   174  
   175  // ProcessRequirementsTags disables charts based on tags in values
   176  func ProcessRequirementsTags(reqs *Requirements, cvals Values) {
   177  	vt, err := cvals.Table("tags")
   178  	if err != nil {
   179  		return
   180  
   181  	}
   182  	if reqs == nil || len(reqs.Dependencies) == 0 {
   183  		return
   184  	}
   185  	for _, r := range reqs.Dependencies {
   186  		if len(r.Tags) > 0 {
   187  			tags := r.Tags
   188  
   189  			var hasTrue, hasFalse bool
   190  			for _, k := range tags {
   191  				if b, ok := vt[k]; ok {
   192  					// if not bool, warn
   193  					if bv, ok := b.(bool); ok {
   194  						if bv {
   195  							hasTrue = true
   196  						} else {
   197  							hasFalse = true
   198  						}
   199  					} else {
   200  						log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name)
   201  					}
   202  				}
   203  			}
   204  			if !hasTrue && hasFalse {
   205  				r.Enabled = false
   206  			} else if hasTrue || !hasTrue && !hasFalse {
   207  				r.Enabled = true
   208  
   209  			}
   210  
   211  		}
   212  	}
   213  
   214  }
   215  
   216  // ProcessRequirementsEnabled removes disabled charts from dependencies
   217  func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
   218  	reqs, err := LoadRequirements(c)
   219  	if err != nil {
   220  		// if not just missing requirements file, return error
   221  		if nerr, ok := err.(ErrNoRequirementsFile); !ok {
   222  			return nerr
   223  		}
   224  
   225  		// no requirements to process
   226  		return nil
   227  	}
   228  	// set all to true
   229  	for _, lr := range reqs.Dependencies {
   230  		lr.Enabled = true
   231  	}
   232  	cvals, err := CoalesceValues(c, v)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	// flag dependencies as enabled/disabled
   237  	ProcessRequirementsTags(reqs, cvals)
   238  	ProcessRequirementsConditions(reqs, cvals)
   239  
   240  	// make a map of charts to remove
   241  	rm := map[string]bool{}
   242  	for _, r := range reqs.Dependencies {
   243  		if !r.Enabled {
   244  			// remove disabled chart
   245  			rm[r.Name] = true
   246  		}
   247  	}
   248  	// don't keep disabled charts in new slice
   249  	cd := []*chart.Chart{}
   250  	copy(cd, c.Dependencies[:0])
   251  	for _, n := range c.Dependencies {
   252  		if _, ok := rm[n.Metadata.Name]; !ok {
   253  			cd = append(cd, n)
   254  		}
   255  
   256  	}
   257  	// recursively call self to process sub dependencies
   258  	for _, t := range cd {
   259  		err := ProcessRequirementsEnabled(t, v)
   260  		// if its not just missing requirements file, return error
   261  		if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
   262  			return nerr
   263  		}
   264  	}
   265  	c.Dependencies = cd
   266  
   267  	return nil
   268  }