github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/helm.go (about)

     1  package tiltfile
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"regexp"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"sigs.k8s.io/yaml"
    13  )
    14  
    15  // The helm template command outputs predictable yaml with a "Source:" comment,
    16  // so take advantage of that.
    17  const helmSeparator = "---\n"
    18  
    19  const helmFileRepository = "file://"
    20  
    21  var helmTestYAMLMatcher = regexp.MustCompile("^# Source: .*/tests/")
    22  
    23  func filterHelmTestYAML(resourceBlob string) string {
    24  	result := []string{}
    25  	resources := strings.Split(resourceBlob, helmSeparator)
    26  	for _, resource := range resources {
    27  		if isHelmTestYAML(resource) {
    28  			continue
    29  		}
    30  
    31  		result = append(result, resource)
    32  	}
    33  	return strings.Join(result, helmSeparator)
    34  }
    35  
    36  func isHelmTestYAML(resource string) bool {
    37  	lines := strings.Split(resource, "\n")
    38  	for _, line := range lines {
    39  		if helmTestYAMLMatcher.MatchString(line) {
    40  			return true
    41  		}
    42  	}
    43  	return false
    44  }
    45  
    46  type helmVersion int
    47  
    48  const (
    49  	unknownHelmVersion helmVersion = iota
    50  	helmV2
    51  	helmV3_0
    52  	helmV3_1andAbove
    53  )
    54  
    55  func parseVersion(versionOutput string) (helmVersion, error) {
    56  	// helm v3.3.3 throws warnings on stdout, which messes up version parsing;
    57  	// if we have multiple lines of version output, assume version info is in
    58  	// the last line (see https://github.com/tilt-dev/tilt/issues/3788)
    59  	version := versionOutput
    60  	lines := strings.Split(strings.TrimSpace(versionOutput), "\n")
    61  	if len(lines) > 1 {
    62  		version = lines[(len(lines) - 1)]
    63  	}
    64  
    65  	if strings.HasPrefix(version, "v3.0.") {
    66  		return helmV3_0, nil
    67  	} else if strings.HasPrefix(version, "v3.") {
    68  		return helmV3_1andAbove, nil
    69  	} else if strings.HasPrefix(version, "Client: v2") {
    70  		return helmV2, nil
    71  	}
    72  
    73  	return unknownHelmVersion, fmt.Errorf("could not parse Helm version from string: %q", versionOutput)
    74  }
    75  
    76  func isHelmInstalled() bool {
    77  	if runtime.GOOS == "windows" {
    78  		cmd := exec.Command("where", "helm.exe")
    79  		if err := cmd.Run(); err != nil {
    80  			return false
    81  		}
    82  
    83  		return true
    84  	}
    85  
    86  	cmd := exec.Command("/bin/sh", "-c", "command -v helm")
    87  	if err := cmd.Run(); err != nil {
    88  		return false
    89  	}
    90  
    91  	return true
    92  }
    93  
    94  func getHelmVersion() (helmVersion, error) {
    95  	if !isHelmInstalled() {
    96  		return unknownHelmVersion, unableToFindHelmErrorMessage()
    97  	}
    98  
    99  	// NOTE(dmiller): I pass `--client` here even though that doesn't do anything in Helm v3.
   100  	// In Helm v2 that causes `helm version` to not reach out to tiller. Doing so can cause the
   101  	// command to fail, even though Tilt doesn't use the server at all (it just calls
   102  	// `helm template`).
   103  	// In Helm v3, it has no effect, not even an unknown flag error.
   104  	cmd := exec.Command("helm", "version", "--client", "--short")
   105  
   106  	out, err := cmd.Output()
   107  	if err != nil {
   108  		return unknownHelmVersion, err
   109  	}
   110  
   111  	return parseVersion(string(out))
   112  }
   113  
   114  func unableToFindHelmErrorMessage() error {
   115  	var binaryName string
   116  	if runtime.GOOS == "windows" {
   117  		binaryName = "helm.exe"
   118  	} else {
   119  		binaryName = "helm"
   120  	}
   121  
   122  	return fmt.Errorf("Unable to find Helm installation. Make sure `%s` is on your $PATH.", binaryName)
   123  }
   124  
   125  func localSubchartDependenciesFromPath(chartPath string) ([]string, error) {
   126  	var deps []string
   127  	requirementsPath := filepath.Join(chartPath, "requirements.yaml")
   128  	dat, err := os.ReadFile(requirementsPath)
   129  	if os.IsNotExist(err) {
   130  		return deps, nil
   131  	} else if err != nil {
   132  		return deps, err
   133  	}
   134  
   135  	return localSubchartDependencies(dat)
   136  }
   137  
   138  type chartDependency struct {
   139  	Repository string
   140  }
   141  
   142  type chartMetadata struct {
   143  	Dependencies []chartDependency
   144  }
   145  
   146  func localSubchartDependencies(dat []byte) ([]string, error) {
   147  	var deps []string
   148  	var metadata chartMetadata
   149  
   150  	err := yaml.Unmarshal(dat, &metadata)
   151  	if err != nil {
   152  		return deps, err
   153  	}
   154  
   155  	for _, d := range metadata.Dependencies {
   156  		if strings.HasPrefix(d.Repository, helmFileRepository) {
   157  			deps = append(deps, strings.TrimPrefix(d.Repository, helmFileRepository))
   158  		}
   159  	}
   160  
   161  	return deps, nil
   162  }