github.com/jenkins-x/jx/v2@v2.1.155/pkg/helm/helm_helpers.go (about)

     1  package helm
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/jenkins-x/jx/v2/pkg/versionstream"
    19  	"gopkg.in/src-d/go-git.v4"
    20  	"gopkg.in/src-d/go-git.v4/config"
    21  	"gopkg.in/src-d/go-git.v4/plumbing"
    22  
    23  	survey "gopkg.in/AlecAivazis/survey.v1"
    24  
    25  	"github.com/google/uuid"
    26  
    27  	"github.com/jenkins-x/jx-logging/pkg/log"
    28  	"github.com/jenkins-x/jx/v2/pkg/kube"
    29  	"github.com/jenkins-x/jx/v2/pkg/secreturl"
    30  	"github.com/jenkins-x/jx/v2/pkg/table"
    31  	"github.com/jenkins-x/jx/v2/pkg/util"
    32  	"k8s.io/client-go/kubernetes"
    33  
    34  	"github.com/ghodss/yaml"
    35  	"github.com/pkg/errors"
    36  	"k8s.io/helm/pkg/chartutil"
    37  	"k8s.io/helm/pkg/proto/hapi/chart"
    38  )
    39  
    40  const (
    41  	// ChartFileName file name for a chart
    42  	ChartFileName = "Chart.yaml"
    43  	// RequirementsFileName the file name for helm requirements
    44  	RequirementsFileName = "requirements.yaml"
    45  	// SecretsFileName the file name for secrets
    46  	SecretsFileName = "secrets.yaml"
    47  	// ValuesFileName the file name for values
    48  	ValuesFileName = "values.yaml"
    49  	// ValuesTemplateFileName a templated values.yaml file which can refer to parameter expressions
    50  	ValuesTemplateFileName = "values.tmpl.yaml"
    51  	// TemplatesDirName is the default name for the templates directory
    52  	TemplatesDirName = "templates"
    53  
    54  	// ParametersYAMLFile contains logical parameters (values or secrets) which can be fetched from a Secret URL or
    55  	// inlined if not a secret which can be referenced from a 'values.yaml` file via a `{{ .Parameters.foo.bar }}` expression
    56  	ParametersYAMLFile = "parameters.yaml"
    57  
    58  	// FakeChartmusuem is the url for the fake chart museum used in tests
    59  	FakeChartmusuem = "http://fake.chartmuseum"
    60  
    61  	// DefaultEnvironmentChartDir is the default environment path where charts are stored
    62  	DefaultEnvironmentChartDir = "env"
    63  
    64  	//RepoVaultPath is the path to the repo credentials in Vault
    65  	RepoVaultPath = "helm/repos"
    66  )
    67  
    68  // copied from helm to minimise dependencies...
    69  
    70  // Dependency describes a chart upon which another chart depends.
    71  //
    72  // Dependencies can be used to express developer intent, or to capture the state
    73  // of a chart.
    74  type Dependency struct {
    75  	// Name is the name of the dependency.
    76  	//
    77  	// This must mach the name in the dependency's Chart.yaml.
    78  	Name string `json:"name"`
    79  	// Version is the version (range) of this chart.
    80  	//
    81  	// A lock file will always produce a single version, while a dependency
    82  	// may contain a semantic version range.
    83  	Version string `json:"version,omitempty"`
    84  	// The URL to the repository.
    85  	//
    86  	// Appending `index.yaml` to this string should result in a URL that can be
    87  	// used to fetch the repository index.
    88  	Repository string `json:"repository"`
    89  	// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
    90  	Condition string `json:"condition,omitempty"`
    91  	// Tags can be used to group charts for enabling/disabling together
    92  	Tags []string `json:"tags,omitempty"`
    93  	// Enabled bool determines if chart should be loaded
    94  	Enabled bool `json:"enabled,omitempty"`
    95  	// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
    96  	// string or pair of child/parent sublist items.
    97  	ImportValues []interface{} `json:"import-values,omitempty"`
    98  	// Alias usable alias to be used for the chart
    99  	Alias string `json:"alias,omitempty"`
   100  }
   101  
   102  // ErrNoRequirementsFile to detect error condition
   103  type ErrNoRequirementsFile error
   104  
   105  // Requirements is a list of requirements for a chart.
   106  //
   107  // Requirements are charts upon which this chart depends. This expresses
   108  // developer intent.
   109  type Requirements struct {
   110  	Dependencies []*Dependency `json:"dependencies"`
   111  }
   112  
   113  // DepSorter Used to avoid merge conflicts by sorting deps by name
   114  type DepSorter []*Dependency
   115  
   116  func (a DepSorter) Len() int           { return len(a) }
   117  func (a DepSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   118  func (a DepSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }
   119  
   120  // SetAppVersion sets the version of the app to use
   121  func (r *Requirements) SetAppVersion(app string, version string, repository string, alias string) {
   122  	if r.Dependencies == nil {
   123  		r.Dependencies = []*Dependency{}
   124  	}
   125  	for _, dep := range r.Dependencies {
   126  		if dep != nil && dep.Name == app {
   127  			if version != dep.Version {
   128  				dep.Version = version
   129  			}
   130  			if repository != "" {
   131  				dep.Repository = repository
   132  			}
   133  			if alias != "" {
   134  				dep.Alias = alias
   135  			}
   136  			return
   137  		}
   138  	}
   139  	r.Dependencies = append(r.Dependencies, &Dependency{
   140  		Name:       app,
   141  		Version:    version,
   142  		Repository: repository,
   143  		Alias:      alias,
   144  	})
   145  	sort.Sort(DepSorter(r.Dependencies))
   146  }
   147  
   148  // RemoveApplication removes the given app name. Returns true if a dependency was removed
   149  func (r *Requirements) RemoveApplication(app string) bool {
   150  	for i, dep := range r.Dependencies {
   151  		if dep != nil && dep.Name == app {
   152  			r.Dependencies = append(r.Dependencies[:i], r.Dependencies[i+1:]...)
   153  			sort.Sort(DepSorter(r.Dependencies))
   154  			return true
   155  		}
   156  	}
   157  	return false
   158  }
   159  
   160  // FindRequirementsFileName returns the default requirements.yaml file name
   161  func FindRequirementsFileName(dir string) (string, error) {
   162  	return findFileName(dir, RequirementsFileName)
   163  }
   164  
   165  // FindChartFileName returns the default chart.yaml file name
   166  func FindChartFileName(dir string) (string, error) {
   167  	return findFileName(dir, ChartFileName)
   168  }
   169  
   170  // FindValuesFileName returns the default values.yaml file name
   171  func FindValuesFileName(dir string) (string, error) {
   172  	return findFileName(dir, ValuesFileName)
   173  }
   174  
   175  // FindValuesFileNameForChart returns the values.yaml file name for a given chart within the environment or the default if the chart name is empty
   176  func FindValuesFileNameForChart(dir string, chartName string) (string, error) {
   177  	//Chart name and file name are joined here to avoid hard coding the environment
   178  	//The chart name is ignored in the path if it's empty
   179  	return findFileName(dir, filepath.Join(chartName, ValuesFileName))
   180  }
   181  
   182  // FindTemplatesDirName returns the default templates/ dir name
   183  func FindTemplatesDirName(dir string) (string, error) {
   184  	return findFileName(dir, TemplatesDirName)
   185  }
   186  
   187  func findFileName(dir string, fileName string) (string, error) {
   188  	names := []string{
   189  		filepath.Join(dir, DefaultEnvironmentChartDir, fileName),
   190  		filepath.Join(dir, fileName),
   191  	}
   192  	for _, name := range names {
   193  		exists, err := util.FileExists(name)
   194  		if err != nil {
   195  			return "", err
   196  		}
   197  		if exists {
   198  			return name, nil
   199  		}
   200  	}
   201  	files, err := ioutil.ReadDir(dir)
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  	for _, f := range files {
   206  		if f.IsDir() {
   207  			name := filepath.Join(dir, f.Name(), fileName)
   208  			exists, err := util.FileExists(name)
   209  			if err != nil {
   210  				return "", err
   211  			}
   212  			if exists {
   213  				return name, nil
   214  			}
   215  		}
   216  	}
   217  	dirs := []string{
   218  		filepath.Join(dir, DefaultEnvironmentChartDir),
   219  		dir,
   220  	}
   221  	for _, d := range dirs {
   222  		name := filepath.Join(d, fileName)
   223  		exists, err := util.DirExists(d)
   224  		if err != nil {
   225  			return "", err
   226  		}
   227  		if exists {
   228  			return name, nil
   229  		}
   230  	}
   231  	return "", fmt.Errorf("could not deduce the default requirements.yaml file name")
   232  }
   233  
   234  // LoadRequirementsFile loads the requirements file or creates empty requirements if the file does not exist
   235  func LoadRequirementsFile(fileName string) (*Requirements, error) {
   236  	exists, err := util.FileExists(fileName)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	if exists {
   241  		data, err := ioutil.ReadFile(fileName)
   242  		if err != nil {
   243  			return nil, err
   244  		}
   245  		return LoadRequirements(data)
   246  	}
   247  	r := &Requirements{}
   248  	return r, nil
   249  }
   250  
   251  // LoadChartFile loads the chart file or creates empty chart if the file does not exist
   252  func LoadChartFile(fileName string) (*chart.Metadata, error) {
   253  	exists, err := util.FileExists(fileName)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	if exists {
   258  		data, err := ioutil.ReadFile(fileName)
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  		return LoadChart(data)
   263  	}
   264  	return &chart.Metadata{}, nil
   265  }
   266  
   267  // LoadValuesFile loads the values file or creates empty map if the file does not exist
   268  func LoadValuesFile(fileName string) (map[string]interface{}, error) {
   269  	exists, err := util.FileExists(fileName)
   270  	if err != nil {
   271  		return nil, errors.Wrapf(err, "checking %s exists", fileName)
   272  	}
   273  	if exists {
   274  		data, err := ioutil.ReadFile(fileName)
   275  		if err != nil {
   276  			return nil, errors.Wrapf(err, "reading %s", fileName)
   277  		}
   278  		v, err := LoadValues(data)
   279  		if err != nil {
   280  			return nil, errors.Wrapf(err, "unmarshaling %s", fileName)
   281  		}
   282  		return v, nil
   283  	}
   284  	return make(map[string]interface{}), nil
   285  }
   286  
   287  // LoadParametersValuesFile loads the parameters values file or creates empty map if the file does not exist
   288  func LoadParametersValuesFile(dir string) (map[string]interface{}, error) {
   289  	return LoadValuesFile(filepath.Join(dir, "env", ParametersYAMLFile))
   290  }
   291  
   292  // LoadTemplatesDir loads the files in the templates dir or creates empty map if none exist
   293  func LoadTemplatesDir(dirName string) (map[string]string, error) {
   294  	exists, err := util.DirExists(dirName)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	answer := make(map[string]string)
   299  	if exists {
   300  		files, err := ioutil.ReadDir(dirName)
   301  		if err != nil {
   302  			return nil, err
   303  		}
   304  		for _, f := range files {
   305  			filename, _ := filepath.Split(f.Name())
   306  			answer[filename] = f.Name()
   307  		}
   308  	}
   309  	return answer, nil
   310  }
   311  
   312  // LoadRequirements loads the requirements from some data
   313  func LoadRequirements(data []byte) (*Requirements, error) {
   314  	r := &Requirements{}
   315  	return r, yaml.Unmarshal(data, r)
   316  }
   317  
   318  // LoadChart loads the requirements from some data
   319  func LoadChart(data []byte) (*chart.Metadata, error) {
   320  	r := &chart.Metadata{}
   321  	return r, yaml.Unmarshal(data, r)
   322  }
   323  
   324  // LoadValues loads the values from some data
   325  func LoadValues(data []byte) (map[string]interface{}, error) {
   326  	r := map[string]interface{}{}
   327  	if data == nil || len(data) == 0 {
   328  		return r, nil
   329  	}
   330  	return r, yaml.Unmarshal(data, &r)
   331  }
   332  
   333  // SaveFile saves contents (a pointer to a data structure) to a file
   334  func SaveFile(fileName string, contents interface{}) error {
   335  	data, err := yaml.Marshal(contents)
   336  	if err != nil {
   337  		return errors.Wrapf(err, "failed to marshal helm file %s", fileName)
   338  	}
   339  	err = ioutil.WriteFile(fileName, data, util.DefaultWritePermissions)
   340  	if err != nil {
   341  		return errors.Wrapf(err, "failed to save helm file %s", fileName)
   342  	}
   343  	return nil
   344  }
   345  
   346  func LoadChartName(chartFile string) (string, error) {
   347  	chart, err := chartutil.LoadChartfile(chartFile)
   348  	if err != nil {
   349  		return "", err
   350  	}
   351  	return chart.Name, nil
   352  }
   353  
   354  func LoadChartNameAndVersion(chartFile string) (string, string, error) {
   355  	chart, err := chartutil.LoadChartfile(chartFile)
   356  	if err != nil {
   357  		return "", "", err
   358  	}
   359  	return chart.Name, chart.Version, nil
   360  }
   361  
   362  // ModifyChart modifies the given chart using a callback
   363  func ModifyChart(chartFile string, fn func(chart *chart.Metadata) error) error {
   364  	chart, err := chartutil.LoadChartfile(chartFile)
   365  	if err != nil {
   366  		return errors.Wrapf(err, "Failed to load chart file %s", chartFile)
   367  	}
   368  	err = fn(chart)
   369  	if err != nil {
   370  		return errors.Wrapf(err, "Failed to modify chart for file %s", chartFile)
   371  	}
   372  	err = chartutil.SaveChartfile(chartFile, chart)
   373  	if err != nil {
   374  		return errors.Wrapf(err, "Failed to save modified chart file %s", chartFile)
   375  	}
   376  	return nil
   377  }
   378  
   379  // SetChartVersion modifies the given chart file to update the version
   380  func SetChartVersion(chartFile string, version string) error {
   381  	callback := func(chart *chart.Metadata) error {
   382  		chart.Version = version
   383  		return nil
   384  	}
   385  	return ModifyChart(chartFile, callback)
   386  }
   387  
   388  func AppendMyValues(valueFiles []string) ([]string, error) {
   389  	// Overwrite the values with the content of myvalues.yaml files from the current folder if exists, otherwise
   390  	// from ~/.jx folder also only if it's present
   391  	curDir, err := os.Getwd()
   392  	if err != nil {
   393  		return nil, errors.Wrap(err, "failed to get the current working directory")
   394  	}
   395  	myValuesFile := filepath.Join(curDir, "myvalues.yaml")
   396  	exists, err := util.FileExists(myValuesFile)
   397  	if err != nil {
   398  		return nil, errors.Wrap(err, "failed to check if the myvaules.yaml file exists in the current directory")
   399  	}
   400  	if exists {
   401  		valueFiles = append(valueFiles, myValuesFile)
   402  		log.Logger().Infof("Using local value overrides file %s", util.ColorInfo(myValuesFile))
   403  	} else {
   404  		configDir, err := util.ConfigDir()
   405  		if err != nil {
   406  			return nil, errors.Wrap(err, "failed to read the config directory")
   407  		}
   408  		myValuesFile = filepath.Join(configDir, "myvalues.yaml")
   409  		exists, err = util.FileExists(myValuesFile)
   410  		if err != nil {
   411  			return nil, errors.Wrap(err, "failed to check if the myvaules.yaml file exists in the .jx directory")
   412  		}
   413  		if exists {
   414  			valueFiles = append(valueFiles, myValuesFile)
   415  			log.Logger().Infof("Using local value overrides file %s", util.ColorInfo(myValuesFile))
   416  		}
   417  	}
   418  	return valueFiles, nil
   419  }
   420  
   421  // CombineValueFilesToFile iterates through the input files and combines them into a single Values object and then
   422  // write it to the output file nested inside the chartName
   423  func CombineValueFilesToFile(outFile string, inputFiles []string, chartName string, extraValues map[string]interface{}) error {
   424  	answerMap := map[string]interface{}{}
   425  
   426  	// lets load any previous values if they exist
   427  	exists, err := util.FileExists(outFile)
   428  	if err != nil {
   429  		return err
   430  	}
   431  	if exists {
   432  		answerMap, err = LoadValuesFile(outFile)
   433  		if err != nil {
   434  			return err
   435  		}
   436  	}
   437  
   438  	// now lets merge any given input files
   439  	answer := chartutil.Values{}
   440  	for _, input := range inputFiles {
   441  		values, err := chartutil.ReadValuesFile(input)
   442  		if err != nil {
   443  			return errors.Wrapf(err, "Failed to read helm values YAML file %s", input)
   444  		}
   445  		sourceMap := answer.AsMap()
   446  		util.CombineMapTrees(sourceMap, values.AsMap())
   447  		answer = chartutil.Values(sourceMap)
   448  	}
   449  	m := answer.AsMap()
   450  	for k, v := range extraValues {
   451  		m[k] = v
   452  	}
   453  	answerMap[chartName] = m
   454  	answer = chartutil.Values(answerMap)
   455  	text, err := answer.YAML()
   456  	if err != nil {
   457  		return errors.Wrap(err, "Failed to marshal the combined values YAML files back to YAML")
   458  	}
   459  	err = ioutil.WriteFile(outFile, []byte(text), util.DefaultWritePermissions)
   460  	if err != nil {
   461  		return errors.Wrapf(err, "Failed to save combined helm values YAML file %s", outFile)
   462  	}
   463  	return nil
   464  }
   465  
   466  // IsGitURL tests whether the given URL is a git URL.
   467  // The given URL supports <git remote url>#<branch/commit/tag>
   468  // example: github.com/jenkins-x/jx#master, github.com/jenkins-x/jx#v3, github.com/jenkins-x/jx#2647027a83b543ffb886ee96dc413f860f79615d
   469  func IsGitURL(url string) (bool, error) {
   470  	re, err := regexp.Compile(`((git|ssh|http(s)?)|(git@[\w.]+))(:(//)?)([\w.@:/\-~]+)(\.git)(/)?`)
   471  	if err != nil {
   472  		return false, err
   473  	}
   474  	return re.MatchString(url), nil
   475  }
   476  
   477  // IsCommitSHA tests whether the given s string is a commit SHA.
   478  func IsCommitSHA(s string) (bool, error) {
   479  	re, err := regexp.Compile("^[a-f0-9]{5,40}$")
   480  	if err != nil {
   481  		return false, err
   482  	}
   483  	return re.MatchString(s), nil
   484  }
   485  
   486  // FetchChartFromGit fetch a helm chart from the given git URL, the URL support <git remote url>#<branch/commit/tag>.
   487  func FetchChartFromGit(path, url string) error {
   488  	ok, err := IsGitURL(url)
   489  	if err != nil {
   490  		return err
   491  	}
   492  	if !ok {
   493  		return errors.New(fmt.Sprintf("The %s is not Git URL", url))
   494  	}
   495  
   496  	ops := &git.CloneOptions{}
   497  	s := strings.SplitN(url, "#", 2)
   498  
   499  	ops.URL = s[0]
   500  
   501  	r, err := git.PlainClone(path, false, ops)
   502  	if err != nil {
   503  		return err
   504  	}
   505  
   506  	if len(s) == 1 {
   507  		return nil
   508  	}
   509  
   510  	w, err := r.Worktree()
   511  	if err != nil {
   512  		return err
   513  	}
   514  
   515  	// try checkout commit
   516  	isCommit, err := IsCommitSHA(s[1])
   517  	if err != nil {
   518  		return err
   519  	}
   520  	if isCommit {
   521  		err = w.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(s[1])})
   522  		if err != nil {
   523  			return err
   524  		}
   525  		return nil
   526  	}
   527  
   528  	// try checkout from tag
   529  	ref, err := r.Tag(s[1])
   530  	if err == nil {
   531  		err = w.Checkout(&git.CheckoutOptions{Hash: ref.Hash()})
   532  		if err != nil {
   533  			return err
   534  		}
   535  		return nil
   536  	}
   537  
   538  	// try checkout from branch
   539  	err = r.Fetch(&git.FetchOptions{
   540  		RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
   541  	})
   542  	if err != nil {
   543  		return err
   544  	}
   545  	err = w.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(s[1])})
   546  
   547  	return err
   548  }
   549  
   550  // IsLocal returns whether this chart is being installed from the local filesystem or not
   551  func IsLocal(chart string) bool {
   552  	b := strings.HasPrefix(chart, "/") || strings.HasPrefix(chart, ".") || strings.Count(chart, "/") > 1
   553  	if !b {
   554  		// check if file exists, then it's local
   555  		exists, err := util.FileExists(chart)
   556  		if err == nil {
   557  			return exists
   558  		}
   559  	}
   560  	return b
   561  }
   562  
   563  // InspectChart fetches the specified chart in a repo using helmer, and then calls the closure on it, before cleaning up
   564  func InspectChart(chart string, version string, repo string, username string, password string,
   565  	helmer Helmer, inspector func(dir string) error) error {
   566  	isLocal := IsLocal(chart)
   567  	dir, err := ioutil.TempDir("", "jx-helm-fetch-")
   568  	if err != nil {
   569  		return errors.Wrapf(err, "creating tempdir")
   570  	}
   571  
   572  	chartFromGit, err := IsGitURL(chart)
   573  
   574  	if err != nil {
   575  		return errors.Wrapf(err, "checking git URL")
   576  	}
   577  
   578  	defer func() {
   579  		err1 := os.RemoveAll(dir)
   580  		if err1 != nil {
   581  			log.Logger().Warnf("Error removing %s %v", dir, err1)
   582  		}
   583  	}()
   584  	parts := strings.Split(chart, "/")
   585  	inspectPath := filepath.Join(dir, parts[len(parts)-1])
   586  
   587  	if chartFromGit {
   588  		inspectPath = dir
   589  		downloadDir, err := ioutil.TempDir("", "jx-helm-fetch-git-")
   590  		defer func() {
   591  			err1 := os.RemoveAll(downloadDir)
   592  			if err1 != nil {
   593  				log.Logger().Warnf("Error removing %s %v", dir, err1)
   594  			}
   595  		}()
   596  		if err != nil {
   597  			return errors.Wrapf(err, "creating git repository tempdir")
   598  		}
   599  
   600  		err = FetchChartFromGit(downloadDir, chart)
   601  		if err != nil {
   602  			return errors.Wrapf(err, "fetching git repository")
   603  		}
   604  
   605  		var chartsDir []string
   606  		err = util.GlobAllFiles("", filepath.Join(downloadDir, "*", ChartFileName), func(path string) error {
   607  			chartsDir = append(chartsDir, filepath.Dir(path))
   608  			return nil
   609  		})
   610  		if err != nil {
   611  			return err
   612  		}
   613  
   614  		if len(chartsDir) == 0 {
   615  			return errors.New("Not found Helm chart")
   616  		}
   617  
   618  		if len(chartsDir) != 1 {
   619  			return errors.New("Cannot install multiple Helm chart")
   620  		}
   621  
   622  		err = util.CopyDir(chartsDir[0], dir, true)
   623  		if err != nil {
   624  			return errors.Wrapf(err, "copying %s to %s", chart, dir)
   625  		}
   626  		helmer.SetCWD(dir)
   627  	} else if isLocal {
   628  		// This is a local path
   629  		err := util.CopyDir(chart, dir, true)
   630  		if err != nil {
   631  			return errors.Wrapf(err, "copying %s to %s", chart, dir)
   632  		}
   633  		helmer.SetCWD(dir)
   634  	} else {
   635  		err = helmer.FetchChart(chart, version, true, dir, repo, username, password)
   636  		if err != nil {
   637  			return err
   638  		}
   639  	}
   640  	return inspector(inspectPath)
   641  }
   642  
   643  type InstallChartOptions struct {
   644  	Dir            string
   645  	ReleaseName    string
   646  	Chart          string
   647  	Version        string
   648  	Ns             string
   649  	HelmUpdate     bool
   650  	SetValues      []string
   651  	SetStrings     []string
   652  	ValueFiles     []string
   653  	Repository     string
   654  	Username       string
   655  	Password       string
   656  	VersionsDir    string
   657  	VersionsGitURL string
   658  	VersionsGitRef string
   659  	InstallOnly    bool
   660  	NoForce        bool
   661  	Wait           bool
   662  	UpgradeOnly    bool
   663  }
   664  
   665  // InstallFromChartOptions uses the helmer and kubeClient interfaces to install the chart from the options,
   666  // respecting the installTimeout, looking up or updating Vault with the username and password for the repo.
   667  // If secretURLClient is nil then username and passwords for repos will not be looked up in Vault.
   668  func InstallFromChartOptions(options InstallChartOptions, helmer Helmer, kubeClient kubernetes.Interface,
   669  	installTimeout string, secretURLClient secreturl.Client) error {
   670  	chart := options.Chart
   671  	if options.Version == "" {
   672  		versionsDir := options.VersionsDir
   673  		if versionsDir == "" {
   674  			return errors.Errorf("no VersionsDir specified when trying to install a chart")
   675  		}
   676  		var err error
   677  		options.Version, err = versionstream.LoadStableVersionNumber(versionsDir, versionstream.KindChart, chart)
   678  		if err != nil {
   679  			return errors.Wrapf(err, "failed to load stable version in dir %s for chart %s", versionsDir, chart)
   680  		}
   681  	}
   682  	if options.HelmUpdate {
   683  		log.Logger().Debugf("Updating Helm repository...")
   684  		err := helmer.UpdateRepo()
   685  		if err != nil {
   686  			return errors.Wrap(err, "failed to update repository")
   687  		}
   688  		log.Logger().Debugf("Helm repository update done.")
   689  	}
   690  	cleanup, err := options.DecorateWithSecrets(secretURLClient)
   691  	defer cleanup() //nolint:errcheck
   692  	if err != nil {
   693  		return errors.WithStack(err)
   694  	}
   695  	if options.Ns != "" {
   696  		annotations := map[string]string{"jenkins-x.io/created-by": "Jenkins X"}
   697  		err = kube.EnsureNamespaceCreated(kubeClient, options.Ns, nil, annotations)
   698  		if err != nil {
   699  			return errors.Wrap(err, "error creating namespace")
   700  		}
   701  	}
   702  	timeout, err := strconv.Atoi(installTimeout)
   703  	if err != nil {
   704  		return errors.Wrap(err, "failed to convert the timeout to an int")
   705  	}
   706  	helmer.SetCWD(options.Dir)
   707  	if options.InstallOnly {
   708  		return helmer.InstallChart(chart, options.ReleaseName, options.Ns, options.Version, timeout,
   709  			options.SetValues, options.SetStrings, options.ValueFiles, options.Repository, options.Username, options.Password)
   710  	}
   711  	return helmer.UpgradeChart(chart, options.ReleaseName, options.Ns, options.Version, !options.UpgradeOnly, timeout,
   712  		!options.NoForce, options.Wait, options.SetValues, options.SetStrings, options.ValueFiles, options.Repository,
   713  		options.Username, options.Password)
   714  }
   715  
   716  // HelmRepoCredentials is a map of repositories to HelmRepoCredential that stores all the helm repo credentials for
   717  // the cluster
   718  type HelmRepoCredentials map[string]HelmRepoCredential
   719  
   720  // HelmRepoCredential is a username and password pair that can ben used to authenticated against a Helm repo
   721  type HelmRepoCredential struct {
   722  	Username string `json:"username"`
   723  	Password string `json:"password"`
   724  }
   725  
   726  // DecorateWithSecrets will replace any vault: URIs with the secret from vault. Safe to call with a nil client (
   727  // no replacement will take place).
   728  func (options *InstallChartOptions) DecorateWithSecrets(secretURLClient secreturl.Client) (func(), error) {
   729  	newValuesFiles, cleanup, err := DecorateWithSecrets(options.ValueFiles, secretURLClient)
   730  	if err != nil {
   731  		return cleanup, errors.WithStack(err)
   732  	}
   733  	options.ValueFiles = newValuesFiles
   734  	return cleanup, nil
   735  }
   736  
   737  // DecorateWithSecrets will replace any vault: URIs with the secret from vault. Safe to call with a nil client (
   738  // no replacement will take place).
   739  func DecorateWithSecrets(valueFiles []string, secretURLClient secreturl.Client) ([]string, func(), error) {
   740  	cleanup := func() {
   741  	}
   742  	newValuesFiles := make([]string, 0)
   743  	if secretURLClient != nil {
   744  
   745  		cleanup = func() {
   746  			for _, f := range newValuesFiles {
   747  				err := util.DeleteFile(f)
   748  				if err != nil {
   749  					log.Logger().Errorf("Deleting temp file %s", f)
   750  				}
   751  			}
   752  		}
   753  		for _, valueFile := range valueFiles {
   754  			newValuesFile, err := ioutil.TempFile("", "values.yaml")
   755  			if err != nil {
   756  				return nil, cleanup, errors.Wrapf(err, "creating temp file for %s", valueFile)
   757  			}
   758  			bytes, err := ioutil.ReadFile(valueFile)
   759  			if err != nil {
   760  				return nil, cleanup, errors.Wrapf(err, "reading file %s", valueFile)
   761  			}
   762  			newValues := string(bytes)
   763  			if secretURLClient != nil {
   764  				newValues, err = secretURLClient.ReplaceURIs(newValues)
   765  				if err != nil {
   766  					return nil, cleanup, errors.Wrapf(err, "replacing vault URIs")
   767  				}
   768  			}
   769  			err = ioutil.WriteFile(newValuesFile.Name(), []byte(newValues), 0600)
   770  			if err != nil {
   771  				return nil, cleanup, errors.Wrapf(err, "writing new values file %s", newValuesFile.Name())
   772  			}
   773  			newValuesFiles = append(newValuesFiles, newValuesFile.Name())
   774  		}
   775  	}
   776  	return newValuesFiles, cleanup, nil
   777  }
   778  
   779  // LoadParameters loads the 'parameters.yaml' file if it exists in the current directory
   780  func LoadParameters(dir string, secretURLClient secreturl.Client) (chartutil.Values, error) {
   781  	fileName := filepath.Join(dir, ParametersYAMLFile)
   782  	exists, err := util.FileExists(fileName)
   783  	if err != nil {
   784  		return nil, errors.Wrapf(err, "checking %s exists", fileName)
   785  	}
   786  	m := map[string]interface{}{}
   787  	if exists {
   788  		data, err := ioutil.ReadFile(fileName)
   789  		if err != nil {
   790  			return nil, errors.Wrapf(err, "reading %s", fileName)
   791  		}
   792  		if secretURLClient != nil {
   793  			text, err := secretURLClient.ReplaceURIs(string(data))
   794  			if err != nil {
   795  				return nil, errors.Wrapf(err, "failed to convert secret URLs in parameters file %s", fileName)
   796  			}
   797  			data = []byte(text)
   798  		}
   799  
   800  		m, err = LoadValues(data)
   801  		if err != nil {
   802  			return nil, errors.Wrapf(err, "unmarshaling %s", fileName)
   803  		}
   804  	}
   805  	return chartutil.Values(m), err
   806  }
   807  
   808  // AddHelmRepoIfMissing will add the helm repo if there is no helm repo with that url present.
   809  // It will generate the repoName from the url (using the host name) if the repoName is empty.
   810  // The repo name may have a suffix added in order to prevent name collisions, and is returned for this reason.
   811  // The username and password will be stored in vault for the URL (if vault is enabled).
   812  func AddHelmRepoIfMissing(helmURL, repoName, username, password string, helmer Helmer,
   813  	secretURLClient secreturl.Client, handles util.IOFileHandles) (string, error) {
   814  	missing, existingName, err := helmer.IsRepoMissing(helmURL)
   815  	if err != nil {
   816  		return "", errors.Wrapf(err, "failed to check if the repository with URL '%s' is missing", helmURL)
   817  	}
   818  	if missing {
   819  		if repoName == "" {
   820  			// Generate the name
   821  			uri, err := url.Parse(helmURL)
   822  			if err != nil {
   823  				repoName = uuid.New().String()
   824  				log.Logger().Warnf("Unable to parse %s as URL so assigning random name %s", helmURL, repoName)
   825  			} else {
   826  				repoName = uri.Hostname()
   827  			}
   828  		}
   829  		// Avoid collisions
   830  		existingRepos, err := helmer.ListRepos()
   831  		if err != nil {
   832  			return "", errors.Wrapf(err, "listing helm repos")
   833  		}
   834  		baseName := repoName
   835  		for i := 0; true; i++ {
   836  			if _, exists := existingRepos[repoName]; exists {
   837  				repoName = fmt.Sprintf("%s-%d", baseName, i)
   838  			} else {
   839  				break
   840  			}
   841  		}
   842  		log.Logger().Infof("Adding missing Helm repo: %s %s", util.ColorInfo(repoName), util.ColorInfo(helmURL))
   843  		username, password, err = DecorateWithCredentials(helmURL, username, password, secretURLClient, handles)
   844  		if err != nil {
   845  			return "", errors.WithStack(err)
   846  		}
   847  		err = helmer.AddRepo(repoName, helmURL, username, password)
   848  		if err != nil {
   849  			return "", errors.Wrapf(err, "failed to add the repository '%s' with URL '%s'", repoName, helmURL)
   850  		}
   851  		log.Logger().Infof("Successfully added Helm repository %s.", repoName)
   852  	} else {
   853  		repoName = existingName
   854  	}
   855  	return repoName, nil
   856  }
   857  
   858  // DecorateWithCredentials will, if vault is installed, store or replace the username or password
   859  func DecorateWithCredentials(repo string, username string, password string, secretURLClient secreturl.Client, handles util.IOFileHandles) (string,
   860  	string, error) {
   861  	if repo != "" && secretURLClient != nil {
   862  		creds := HelmRepoCredentials{}
   863  		if err := secretURLClient.ReadObject(RepoVaultPath, &creds); err != nil {
   864  			log.Logger().Warnf("No secrets found on %q due: %s", RepoVaultPath, err)
   865  		}
   866  		var existingCred, cred HelmRepoCredential
   867  		if c, ok := creds[repo]; ok {
   868  			existingCred = c
   869  		}
   870  		if username != "" || password != "" {
   871  			cred = HelmRepoCredential{
   872  				Username: username,
   873  				Password: password,
   874  			}
   875  		} else {
   876  			cred = existingCred
   877  		}
   878  
   879  		err := PromptForRepoCredsIfNeeded(repo, &cred, handles)
   880  		if err != nil {
   881  			return "", "", errors.Wrapf(err, "prompting for creds for %s", repo)
   882  		}
   883  
   884  		if cred.Password != existingCred.Password || cred.Username != existingCred.Username {
   885  			log.Logger().Infof("Storing credentials for %s in vault %s", repo, RepoVaultPath)
   886  			creds[repo] = cred
   887  			_, err := secretURLClient.WriteObject(RepoVaultPath, creds)
   888  			if err != nil {
   889  				return "", "", errors.Wrapf(err, "updating repo credentials in vault %s", RepoVaultPath)
   890  			}
   891  		} else {
   892  			log.Logger().Infof("Read credentials for %s from vault %s", repo, RepoVaultPath)
   893  		}
   894  		return cred.Username, cred.Password, nil
   895  	}
   896  	cred := HelmRepoCredential{
   897  		Username: username,
   898  		Password: password,
   899  	}
   900  	err := PromptForRepoCredsIfNeeded(repo, &cred, handles)
   901  	if err != nil {
   902  		return "", "", errors.Wrapf(err, "prompting for creds for %s", repo)
   903  	}
   904  	return cred.Username, cred.Password, nil
   905  }
   906  
   907  // GenerateReadmeForChart generates a string that can be used as a README.MD,
   908  // and includes info on the chart.
   909  func GenerateReadmeForChart(name string, version string, description string, chartRepo string,
   910  	gitRepo string, releaseNotesURL string, appReadme string) string {
   911  	var readme strings.Builder
   912  	readme.WriteString(fmt.Sprintf("# %s\n\n|App Metadata||\n", unknownZeroValue(name)))
   913  	readme.WriteString("|---|---|\n")
   914  	if version != "" {
   915  		readme.WriteString(fmt.Sprintf("| **Version** | %s |\n", version))
   916  	}
   917  	if description != "" {
   918  		readme.WriteString(fmt.Sprintf("| **Description** | %s |\n", description))
   919  	}
   920  	if chartRepo != "" {
   921  		readme.WriteString(fmt.Sprintf("| **Chart Repository** | %s |\n", chartRepo))
   922  	}
   923  	if gitRepo != "" {
   924  		readme.WriteString(fmt.Sprintf("| **Git Repository** | %s |\n", gitRepo))
   925  	}
   926  	if releaseNotesURL != "" {
   927  		readme.WriteString(fmt.Sprintf("| **Release Notes** | %s |\n", releaseNotesURL))
   928  	}
   929  
   930  	if appReadme != "" {
   931  		readme.WriteString(fmt.Sprintf("\n## App README.MD\n\n%s\n", appReadme))
   932  	}
   933  	return readme.String()
   934  }
   935  
   936  func unknownZeroValue(value string) string {
   937  	if value == "" {
   938  		return "unknown"
   939  	}
   940  	return value
   941  
   942  }
   943  
   944  // SetValuesToMap converts the set of values of the form "foo.bar=123" into a helm values.yaml map structure
   945  func SetValuesToMap(setValues []string) map[string]interface{} {
   946  	answer := map[string]interface{}{}
   947  	for _, setValue := range setValues {
   948  		tokens := strings.SplitN(setValue, "=", 2)
   949  		if len(tokens) > 1 {
   950  			path := tokens[0]
   951  			value := tokens[1]
   952  
   953  			// lets assume false is a boolean
   954  			if value == "false" {
   955  				util.SetMapValueViaPath(answer, path, false)
   956  
   957  			} else {
   958  				util.SetMapValueViaPath(answer, path, value)
   959  			}
   960  		}
   961  	}
   962  	return answer
   963  }
   964  
   965  // PromptForRepoCredsIfNeeded will prompt for repo credentials if required. It first checks the existing cred (
   966  // if any) and then prompts for new credentials up to 3 times, trying each set.
   967  func PromptForRepoCredsIfNeeded(repo string, cred *HelmRepoCredential, handles util.IOFileHandles) error {
   968  	if repo == FakeChartmusuem || handles.In == nil || handles.Out == nil || handles.Err == nil {
   969  		// Avoid doing this in tests!
   970  		return nil
   971  	}
   972  	u := fmt.Sprintf("%s/index.yaml", strings.TrimSuffix(repo, "/"))
   973  
   974  	httpClient := &http.Client{}
   975  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
   976  	if cred.Username == "" && cred.Password == "" {
   977  		// Try without any auth
   978  		req, err := http.NewRequest("GET", u, nil)
   979  		if err != nil {
   980  			return errors.Wrapf(err, "creating GET request to %s", u)
   981  		}
   982  		resp, err := httpClient.Do(req)
   983  		if err != nil {
   984  			return errors.Wrapf(err, "checking status code of %s", u)
   985  		}
   986  		if resp.StatusCode == 200 {
   987  			return nil
   988  		}
   989  	}
   990  	for i := 0; true; i++ {
   991  		req, err := http.NewRequest("GET", u, nil)
   992  		if err != nil {
   993  			return errors.Wrapf(err, "creating GET request to %s", u)
   994  		}
   995  		auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cred.Username, cred.Password)))
   996  		req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth))
   997  		resp, err := httpClient.Do(req)
   998  		if err != nil {
   999  			return errors.Wrapf(err, "checking status code of %s", u)
  1000  		}
  1001  		if i == 4 {
  1002  			return errors.Errorf("username and password for %s not valid", repo)
  1003  		} else if resp.StatusCode == 401 {
  1004  			if cred.Username != "" || cred.Password != "" {
  1005  				log.Logger().Errorf("Authentication for %s failed (%s/%s)", repo, cred.Username, strings.Repeat("*",
  1006  					len(cred.Password)))
  1007  			}
  1008  			usernamePrompt := survey.Input{
  1009  				Message: "Repository username",
  1010  				Default: cred.Username,
  1011  				Help:    fmt.Sprintf("Enter the username for %s", repo),
  1012  			}
  1013  			err := survey.AskOne(&usernamePrompt, &cred.Username, nil, surveyOpts)
  1014  			if err != nil {
  1015  				return errors.Wrapf(err, "asking for username")
  1016  			}
  1017  			passwordPrompt := survey.Password{
  1018  				Message: "Repository password",
  1019  				Help:    fmt.Sprintf("Enter the password for %s", repo),
  1020  			}
  1021  			err = survey.AskOne(&passwordPrompt, &cred.Password, nil, surveyOpts)
  1022  			if err != nil {
  1023  				return errors.Wrapf(err, "asking for password")
  1024  			}
  1025  		} else {
  1026  			break
  1027  		}
  1028  	}
  1029  	return nil
  1030  }
  1031  
  1032  // RenderReleasesAsTable lists the current releases in a table
  1033  func RenderReleasesAsTable(releases map[string]ReleaseSummary, sortedKeys []string) (string, error) {
  1034  	var buffer bytes.Buffer
  1035  	writer := bufio.NewWriter(&buffer)
  1036  	t := table.CreateTable(writer)
  1037  	t.Separator = "\t"
  1038  	t.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
  1039  	for _, key := range sortedKeys {
  1040  		info := releases[key]
  1041  		t.AddRow(info.ReleaseName, info.Revision, info.Updated, info.Status, info.ChartFullName, info.AppVersion,
  1042  			info.Namespace)
  1043  	}
  1044  	t.Render()
  1045  	writer.Flush()
  1046  	return buffer.String(), nil
  1047  }
  1048  
  1049  // UpdateRequirementsToNewVersion update dependencies with name to newVersion, returning the oldVersions
  1050  func UpdateRequirementsToNewVersion(requirements *Requirements, name string, newVersion string) []string {
  1051  	answer := make([]string, 0)
  1052  	for _, dependency := range requirements.Dependencies {
  1053  		if dependency.Name == name {
  1054  			answer = append(answer, dependency.Version)
  1055  			dependency.Version = newVersion
  1056  		}
  1057  	}
  1058  	return answer
  1059  }
  1060  
  1061  // UpdateImagesInValuesToNewVersion update a (values) file, replacing that start with "Image: <name>:" to "Image: <name>:<newVersion>",
  1062  // returning the oldVersions
  1063  func UpdateImagesInValuesToNewVersion(data []byte, name string, newVersion string) ([]byte, []string) {
  1064  	oldVersions := make([]string, 0)
  1065  	var answer strings.Builder
  1066  	linePrefix := fmt.Sprintf("Image: %s:", name)
  1067  	for _, line := range strings.Split(string(data), "\n") {
  1068  		trimmedLine := strings.TrimSpace(line)
  1069  		if strings.HasPrefix(trimmedLine, linePrefix) {
  1070  			oldVersions = append(oldVersions, strings.TrimPrefix(trimmedLine, linePrefix))
  1071  			answer.WriteString(linePrefix)
  1072  			answer.WriteString(newVersion)
  1073  		} else {
  1074  			answer.WriteString(line)
  1075  		}
  1076  		answer.WriteString("\n")
  1077  	}
  1078  	return []byte(answer.String()), oldVersions
  1079  }
  1080  
  1081  // FindLatestChart uses helmer to find the latest chart for name
  1082  func FindLatestChart(name string, helmer Helmer) (*ChartSummary, error) {
  1083  	info, err := helmer.SearchCharts(name, true)
  1084  	if err != nil {
  1085  		return nil, err
  1086  	}
  1087  	if len(info) == 0 {
  1088  		return nil, fmt.Errorf("no version found for chart %s", name)
  1089  	}
  1090  	log.Logger().Debugf("found %d versions: %#v", len(info), info)
  1091  	return &info[0], nil
  1092  }