...

Source file src/sigs.k8s.io/kustomize/api/internal/builtins/HelmChartInflationGenerator.go

Documentation: sigs.k8s.io/kustomize/api/internal/builtins

     1  // Code generated by pluginator on HelmChartInflationGenerator; DO NOT EDIT.
     2  // pluginator {(devel)  unknown   }
     3  
     4  package builtins
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"sigs.k8s.io/kustomize/api/resmap"
    16  	"sigs.k8s.io/kustomize/api/types"
    17  	"sigs.k8s.io/kustomize/kyaml/errors"
    18  	"sigs.k8s.io/kustomize/kyaml/kio"
    19  	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
    20  	"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
    21  	"sigs.k8s.io/yaml"
    22  )
    23  
    24  // Generate resources from a remote or local helm chart.
    25  type HelmChartInflationGeneratorPlugin struct {
    26  	h *resmap.PluginHelpers
    27  	types.HelmGlobals
    28  	types.HelmChart
    29  	tmpDir string
    30  }
    31  
    32  const (
    33  	valuesMergeOptionMerge    = "merge"
    34  	valuesMergeOptionOverride = "override"
    35  	valuesMergeOptionReplace  = "replace"
    36  )
    37  
    38  var legalMergeOptions = []string{
    39  	valuesMergeOptionMerge,
    40  	valuesMergeOptionOverride,
    41  	valuesMergeOptionReplace,
    42  }
    43  
    44  // Config uses the input plugin configurations `config` to setup the generator
    45  // options
    46  func (p *HelmChartInflationGeneratorPlugin) Config(
    47  	h *resmap.PluginHelpers, config []byte) (err error) {
    48  	if h.GeneralConfig() == nil {
    49  		return fmt.Errorf("unable to access general config")
    50  	}
    51  	if !h.GeneralConfig().HelmConfig.Enabled {
    52  		return fmt.Errorf("must specify --enable-helm")
    53  	}
    54  	if h.GeneralConfig().HelmConfig.Command == "" {
    55  		return fmt.Errorf("must specify --helm-command")
    56  	}
    57  
    58  	// CLI args takes precedence
    59  	if h.GeneralConfig().HelmConfig.KubeVersion != "" {
    60  		p.HelmChart.KubeVersion = h.GeneralConfig().HelmConfig.KubeVersion
    61  	}
    62  	if len(h.GeneralConfig().HelmConfig.ApiVersions) != 0 {
    63  		p.HelmChart.ApiVersions = h.GeneralConfig().HelmConfig.ApiVersions
    64  	}
    65  
    66  	p.h = h
    67  	if err = yaml.Unmarshal(config, p); err != nil {
    68  		return
    69  	}
    70  	return p.validateArgs()
    71  }
    72  
    73  // This uses the real file system since tmpDir may be used
    74  // by the helm subprocess.  Cannot use a chroot jail or fake
    75  // filesystem since we allow the user to use previously
    76  // downloaded charts.  This is safe since this plugin is
    77  // owned by kustomize.
    78  func (p *HelmChartInflationGeneratorPlugin) establishTmpDir() (err error) {
    79  	if p.tmpDir != "" {
    80  		// already done.
    81  		return nil
    82  	}
    83  	p.tmpDir, err = os.MkdirTemp("", "kustomize-helm-")
    84  	return err
    85  }
    86  
    87  func (p *HelmChartInflationGeneratorPlugin) validateArgs() (err error) {
    88  	if p.Name == "" {
    89  		return fmt.Errorf("chart name cannot be empty")
    90  	}
    91  
    92  	// ChartHome might be consulted by the plugin (to read
    93  	// values files below it), so it must be located under
    94  	// the loader root (unless root restrictions are
    95  	// disabled, in which case this can be an absolute path).
    96  	if p.ChartHome == "" {
    97  		p.ChartHome = types.HelmDefaultHome
    98  	}
    99  
   100  	// The ValuesFile(s) may be consulted by the plugin, so it must
   101  	// be under the loader root (unless root restrictions are
   102  	// disabled).
   103  	if p.ValuesFile == "" {
   104  		p.ValuesFile = filepath.Join(p.absChartHome(), p.Name, "values.yaml")
   105  	}
   106  	for i, file := range p.AdditionalValuesFiles {
   107  		// use Load() to enforce root restrictions
   108  		if _, err := p.h.Loader().Load(file); err != nil {
   109  			return errors.WrapPrefixf(err, "could not load additionalValuesFile")
   110  		}
   111  		// the additional values filepaths must be relative to the kust root
   112  		p.AdditionalValuesFiles[i] = filepath.Join(p.h.Loader().Root(), file)
   113  	}
   114  
   115  	if err = p.errIfIllegalValuesMerge(); err != nil {
   116  		return err
   117  	}
   118  
   119  	// ConfigHome is not loaded by the plugin, and can be located anywhere.
   120  	if p.ConfigHome == "" {
   121  		if err = p.establishTmpDir(); err != nil {
   122  			return errors.WrapPrefixf(
   123  				err, "unable to create tmp dir for HELM_CONFIG_HOME")
   124  		}
   125  		p.ConfigHome = filepath.Join(p.tmpDir, "helm")
   126  	}
   127  	return nil
   128  }
   129  
   130  func (p *HelmChartInflationGeneratorPlugin) errIfIllegalValuesMerge() error {
   131  	if p.ValuesMerge == "" {
   132  		// Use the default.
   133  		p.ValuesMerge = valuesMergeOptionOverride
   134  		return nil
   135  	}
   136  	for _, opt := range legalMergeOptions {
   137  		if p.ValuesMerge == opt {
   138  			return nil
   139  		}
   140  	}
   141  	return fmt.Errorf("valuesMerge must be one of %v", legalMergeOptions)
   142  }
   143  
   144  func (p *HelmChartInflationGeneratorPlugin) absChartHome() string {
   145  	var chartHome string
   146  	if filepath.IsAbs(p.ChartHome) {
   147  		chartHome = p.ChartHome
   148  	} else {
   149  		chartHome = filepath.Join(p.h.Loader().Root(), p.ChartHome)
   150  	}
   151  
   152  	if p.Version != "" && p.Repo != "" {
   153  		return filepath.Join(chartHome, fmt.Sprintf("%s-%s", p.Name, p.Version))
   154  	}
   155  	return chartHome
   156  }
   157  
   158  func (p *HelmChartInflationGeneratorPlugin) runHelmCommand(
   159  	args []string) ([]byte, error) {
   160  	stdout := new(bytes.Buffer)
   161  	stderr := new(bytes.Buffer)
   162  	cmd := exec.Command(p.h.GeneralConfig().HelmConfig.Command, args...)
   163  	cmd.Stdout = stdout
   164  	cmd.Stderr = stderr
   165  	env := []string{
   166  		fmt.Sprintf("HELM_CONFIG_HOME=%s", p.ConfigHome),
   167  		fmt.Sprintf("HELM_CACHE_HOME=%s/.cache", p.ConfigHome),
   168  		fmt.Sprintf("HELM_DATA_HOME=%s/.data", p.ConfigHome)}
   169  	cmd.Env = append(os.Environ(), env...)
   170  	err := cmd.Run()
   171  	if err != nil {
   172  		helm := p.h.GeneralConfig().HelmConfig.Command
   173  		err = errors.WrapPrefixf(
   174  			fmt.Errorf(
   175  				"unable to run: '%s %s' with env=%s (is '%s' installed?): %w",
   176  				helm, strings.Join(args, " "), env, helm, err),
   177  			stderr.String(),
   178  		)
   179  	}
   180  	return stdout.Bytes(), err
   181  }
   182  
   183  // createNewMergedValuesFile replaces/merges original values file with ValuesInline.
   184  func (p *HelmChartInflationGeneratorPlugin) createNewMergedValuesFile() (
   185  	path string, err error) {
   186  	if p.ValuesMerge == valuesMergeOptionMerge ||
   187  		p.ValuesMerge == valuesMergeOptionOverride {
   188  		if err = p.replaceValuesInline(); err != nil {
   189  			return "", err
   190  		}
   191  	}
   192  	var b []byte
   193  	b, err = yaml.Marshal(p.ValuesInline)
   194  	if err != nil {
   195  		return "", err
   196  	}
   197  	return p.writeValuesBytes(b)
   198  }
   199  
   200  func (p *HelmChartInflationGeneratorPlugin) replaceValuesInline() error {
   201  	pValues, err := p.h.Loader().Load(p.ValuesFile)
   202  	if err != nil {
   203  		return err
   204  	}
   205  	chValues, err := kyaml.Parse(string(pValues))
   206  	if err != nil {
   207  		return errors.WrapPrefixf(err, "could not parse values file into rnode")
   208  	}
   209  	inlineValues, err := kyaml.FromMap(p.ValuesInline)
   210  	if err != nil {
   211  		return errors.WrapPrefixf(err, "could not parse values inline into rnode")
   212  	}
   213  	var outValues *kyaml.RNode
   214  	switch p.ValuesMerge {
   215  	// Function `merge2.Merge` overrides values in dest with values from src.
   216  	// To achieve override or merge behavior, we pass parameters in different order.
   217  	// Object passed as dest will be modified, so we copy it just in case someone
   218  	// decides to use it after this is called.
   219  	case valuesMergeOptionOverride:
   220  		outValues, err = merge2.Merge(inlineValues, chValues.Copy(), kyaml.MergeOptions{})
   221  	case valuesMergeOptionMerge:
   222  		outValues, err = merge2.Merge(chValues, inlineValues.Copy(), kyaml.MergeOptions{})
   223  	}
   224  	if err != nil {
   225  		return errors.WrapPrefixf(err, "could not merge values")
   226  	}
   227  	mapValues, err := outValues.Map()
   228  	if err != nil {
   229  		return errors.WrapPrefixf(err, "could not parse merged values into map")
   230  	}
   231  	p.ValuesInline = mapValues
   232  	return err
   233  }
   234  
   235  // copyValuesFile to avoid branching.  TODO: get rid of this.
   236  func (p *HelmChartInflationGeneratorPlugin) copyValuesFile() (string, error) {
   237  	b, err := p.h.Loader().Load(p.ValuesFile)
   238  	if err != nil {
   239  		return "", err
   240  	}
   241  	return p.writeValuesBytes(b)
   242  }
   243  
   244  // Write a absolute path file in the tmp file system.
   245  func (p *HelmChartInflationGeneratorPlugin) writeValuesBytes(
   246  	b []byte) (string, error) {
   247  	if err := p.establishTmpDir(); err != nil {
   248  		return "", fmt.Errorf("cannot create tmp dir to write helm values")
   249  	}
   250  	path := filepath.Join(p.tmpDir, p.Name+"-kustomize-values.yaml")
   251  	return path, errors.WrapPrefixf(os.WriteFile(path, b, 0644), "failed to write values file")
   252  }
   253  
   254  func (p *HelmChartInflationGeneratorPlugin) cleanup() {
   255  	if p.tmpDir != "" {
   256  		os.RemoveAll(p.tmpDir)
   257  	}
   258  }
   259  
   260  // Generate implements generator
   261  func (p *HelmChartInflationGeneratorPlugin) Generate() (rm resmap.ResMap, err error) {
   262  	defer p.cleanup()
   263  	if err = p.checkHelmVersion(); err != nil {
   264  		return nil, err
   265  	}
   266  	if path, exists := p.chartExistsLocally(); !exists {
   267  		if p.Repo == "" {
   268  			return nil, fmt.Errorf(
   269  				"no repo specified for pull, no chart found at '%s'", path)
   270  		}
   271  		if _, err := p.runHelmCommand(p.pullCommand()); err != nil {
   272  			return nil, err
   273  		}
   274  	}
   275  	if len(p.ValuesInline) > 0 {
   276  		p.ValuesFile, err = p.createNewMergedValuesFile()
   277  	} else {
   278  		p.ValuesFile, err = p.copyValuesFile()
   279  	}
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	var stdout []byte
   284  	stdout, err = p.runHelmCommand(p.AsHelmArgs(p.absChartHome()))
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	rm, resMapErr := p.h.ResmapFactory().NewResMapFromBytes(stdout)
   290  	if resMapErr == nil {
   291  		return rm, nil
   292  	}
   293  	// try to remove the contents before first "---" because
   294  	// helm may produce messages to stdout before it
   295  	r := &kio.ByteReader{Reader: bytes.NewBufferString(string(stdout)), OmitReaderAnnotations: true}
   296  	nodes, err := r.Read()
   297  	if err != nil {
   298  		return nil, fmt.Errorf("error reading helm output: %w", err)
   299  	}
   300  
   301  	if len(nodes) != 0 {
   302  		rm, err = p.h.ResmapFactory().NewResMapFromRNodeSlice(nodes)
   303  		if err != nil {
   304  			return nil, fmt.Errorf("could not parse rnode slice into resource map: %w", err)
   305  		}
   306  		return rm, nil
   307  	}
   308  	return nil, fmt.Errorf("could not parse bytes into resource map: %w", resMapErr)
   309  }
   310  
   311  func (p *HelmChartInflationGeneratorPlugin) pullCommand() []string {
   312  	args := []string{
   313  		"pull",
   314  		"--untar",
   315  		"--untardir", p.absChartHome(),
   316  	}
   317  
   318  	switch {
   319  	case strings.HasPrefix(p.Repo, "oci://"):
   320  		args = append(args, strings.TrimSuffix(p.Repo, "/")+"/"+p.Name)
   321  	case p.Repo != "":
   322  		args = append(args, "--repo", p.Repo)
   323  		fallthrough
   324  	default:
   325  		args = append(args, p.Name)
   326  	}
   327  
   328  	if p.Version != "" {
   329  		args = append(args, "--version", p.Version)
   330  	}
   331  	return args
   332  }
   333  
   334  // chartExistsLocally will return true if the chart does exist in
   335  // local chart home.
   336  func (p *HelmChartInflationGeneratorPlugin) chartExistsLocally() (string, bool) {
   337  	path := filepath.Join(p.absChartHome(), p.Name)
   338  	s, err := os.Stat(path)
   339  	if err != nil {
   340  		return "", false
   341  	}
   342  	return path, s.IsDir()
   343  }
   344  
   345  // checkHelmVersion will return an error if the helm version is not V3
   346  func (p *HelmChartInflationGeneratorPlugin) checkHelmVersion() error {
   347  	stdout, err := p.runHelmCommand([]string{"version", "-c", "--short"})
   348  	if err != nil {
   349  		return err
   350  	}
   351  	r, err := regexp.Compile(`v?\d+(\.\d+)+`)
   352  	if err != nil {
   353  		return err
   354  	}
   355  	v := r.FindString(string(stdout))
   356  	if v == "" {
   357  		return fmt.Errorf("cannot find version string in %s", string(stdout))
   358  	}
   359  	if v[0] == 'v' {
   360  		v = v[1:]
   361  	}
   362  	majorVersion := strings.Split(v, ".")[0]
   363  	if majorVersion != "3" {
   364  		return fmt.Errorf("this plugin requires helm V3 but got v%s", v)
   365  	}
   366  	return nil
   367  }
   368  
   369  func NewHelmChartInflationGeneratorPlugin() resmap.GeneratorPlugin {
   370  	return &HelmChartInflationGeneratorPlugin{}
   371  }
   372  

View as plain text