github.com/argoproj/argo-cd/v3@v3.2.1/util/helm/helm.go (about) 1 package helm 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 12 log "github.com/sirupsen/logrus" 13 "sigs.k8s.io/yaml" 14 15 "github.com/argoproj/argo-cd/v3/util/config" 16 executil "github.com/argoproj/argo-cd/v3/util/exec" 17 pathutil "github.com/argoproj/argo-cd/v3/util/io/path" 18 ) 19 20 const ( 21 ResourcePolicyAnnotation = "helm.sh/resource-policy" 22 ResourcePolicyKeep = "keep" 23 ) 24 25 type HelmRepository struct { 26 Creds 27 Name string 28 Repo string 29 EnableOci bool 30 } 31 32 // Helm provides wrapper functionality around the `helm` command. 33 type Helm interface { 34 // Template returns a list of unstructured objects from a `helm template` command 35 Template(opts *TemplateOpts) (string, string, error) 36 // GetParameters returns a list of chart parameters taking into account values in provided YAML files. 37 GetParameters(valuesFiles []pathutil.ResolvedFilePath, appPath, repoRoot string) (map[string]string, error) 38 // DependencyBuild runs `helm dependency build` to download a chart's dependencies 39 DependencyBuild() error 40 // Dispose deletes temp resources 41 Dispose() 42 } 43 44 // NewHelmApp create a new wrapper to run commands on the `helm` command-line tool. 45 func NewHelmApp(workDir string, repos []HelmRepository, isLocal bool, version string, proxy string, noProxy string, passCredentials bool) (Helm, error) { 46 cmd, err := NewCmd(workDir, version, proxy, noProxy) 47 if err != nil { 48 return nil, fmt.Errorf("failed to create new helm command: %w", err) 49 } 50 cmd.IsLocal = isLocal 51 52 return &helm{repos: repos, cmd: *cmd, passCredentials: passCredentials}, nil 53 } 54 55 type helm struct { 56 cmd Cmd 57 repos []HelmRepository 58 passCredentials bool 59 } 60 61 var _ Helm = &helm{} 62 63 // IsMissingDependencyErr tests if the error is related to a missing chart dependency 64 func IsMissingDependencyErr(err error) bool { 65 return strings.Contains(err.Error(), "found in requirements.yaml, but missing in charts") || 66 strings.Contains(err.Error(), "found in Chart.yaml, but missing in charts/ directory") 67 } 68 69 func (h *helm) Template(templateOpts *TemplateOpts) (string, string, error) { 70 out, command, err := h.cmd.template(".", templateOpts) 71 if err != nil { 72 return "", command, fmt.Errorf("failed to execute helm template command: %w", err) 73 } 74 return out, command, nil 75 } 76 77 func (h *helm) DependencyBuild() error { 78 isHelmOci := h.cmd.IsHelmOci 79 defer func() { 80 h.cmd.IsHelmOci = isHelmOci 81 }() 82 83 for i := range h.repos { 84 repo := h.repos[i] 85 if repo.EnableOci { 86 h.cmd.IsHelmOci = true 87 helmPassword, err := repo.GetPassword() 88 if err != nil { 89 return fmt.Errorf("failed to get password for helm registry: %w", err) 90 } 91 if repo.GetUsername() != "" && helmPassword != "" { 92 _, err := h.cmd.RegistryLogin(repo.Repo, repo.Creds) 93 94 defer func() { 95 _, _ = h.cmd.RegistryLogout(repo.Repo, repo.Creds) 96 }() 97 98 if err != nil { 99 return fmt.Errorf("failed to login to registry %s: %w", repo.Repo, err) 100 } 101 } 102 } else { 103 _, err := h.cmd.RepoAdd(repo.Name, repo.Repo, repo.Creds, h.passCredentials) 104 if err != nil { 105 return fmt.Errorf("failed to add helm repository %s: %w", repo.Repo, err) 106 } 107 } 108 } 109 h.repos = nil 110 _, err := h.cmd.dependencyBuild() 111 if err != nil { 112 return fmt.Errorf("failed to build helm dependencies: %w", err) 113 } 114 return nil 115 } 116 117 func (h *helm) Dispose() { 118 h.cmd.Close() 119 } 120 121 func Version() (string, error) { 122 cmd := exec.Command("helm", "version", "--client", "--short") 123 // example version output: 124 // short: "v3.3.1+g249e521" 125 version, err := executil.RunWithRedactor(cmd, redactor) 126 if err != nil { 127 return "", fmt.Errorf("could not get helm version: %w", err) 128 } 129 return strings.TrimSpace(version), nil 130 } 131 132 func (h *helm) GetParameters(valuesFiles []pathutil.ResolvedFilePath, appPath, repoRoot string) (map[string]string, error) { 133 var values []string 134 // Don't load values.yaml if it's an out-of-bounds link. 135 if _, _, err := pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, "values.yaml", []string{}); err == nil { 136 out, err := h.cmd.inspectValues(".") 137 if err != nil { 138 return nil, fmt.Errorf("failed to execute helm inspect values command: %w", err) 139 } 140 values = append(values, out) 141 } else { 142 log.Warnf("Values file %s is not allowed: %v", filepath.Join(appPath, "values.yaml"), err) 143 } 144 for i := range valuesFiles { 145 file := string(valuesFiles[i]) 146 var fileValues []byte 147 parsedURL, err := url.ParseRequestURI(file) 148 if err == nil && (parsedURL.Scheme == "http" || parsedURL.Scheme == "https") { 149 fileValues, err = config.ReadRemoteFile(file) 150 } else { 151 _, fileReadErr := os.Stat(file) 152 if os.IsNotExist(fileReadErr) { 153 log.Debugf("File not found %s", file) 154 continue 155 } 156 if errors.Is(fileReadErr, os.ErrPermission) { 157 log.Debugf("File does not have permissions %s", file) 158 continue 159 } 160 fileValues, err = os.ReadFile(file) 161 } 162 if err != nil { 163 return nil, fmt.Errorf("failed to read value file %s: %w", file, err) 164 } 165 values = append(values, string(fileValues)) 166 } 167 168 output := map[string]string{} 169 for _, file := range values { 170 values := map[string]any{} 171 if err := yaml.Unmarshal([]byte(file), &values); err != nil { 172 return nil, fmt.Errorf("failed to parse values: %w", err) 173 } 174 flatVals(values, output) 175 } 176 177 return output, nil 178 } 179 180 func flatVals(input any, output map[string]string, prefixes ...string) { 181 switch i := input.(type) { 182 case map[string]any: 183 for k, v := range i { 184 flatVals(v, output, append(prefixes, k)...) 185 } 186 case []any: 187 p := append([]string(nil), prefixes...) 188 for j, v := range i { 189 flatVals(v, output, append(p[0:len(p)-1], fmt.Sprintf("%s[%v]", prefixes[len(p)-1], j))...) 190 } 191 default: 192 output[strings.Join(prefixes, ".")] = fmt.Sprintf("%v", i) 193 } 194 }