github.com/argoproj/argo-cd/v3@v3.2.1/util/helm/cmd.go (about) 1 package helm 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "os/exec" 8 "path" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 log "github.com/sirupsen/logrus" 14 15 "github.com/argoproj/argo-cd/v3/common" 16 executil "github.com/argoproj/argo-cd/v3/util/exec" 17 utilio "github.com/argoproj/argo-cd/v3/util/io" 18 pathutil "github.com/argoproj/argo-cd/v3/util/io/path" 19 "github.com/argoproj/argo-cd/v3/util/proxy" 20 ) 21 22 // A thin wrapper around the "helm" command, adding logging and error translation. 23 type Cmd struct { 24 helmHome string 25 WorkDir string 26 IsLocal bool 27 IsHelmOci bool 28 proxy string 29 noProxy string 30 } 31 32 func NewCmd(workDir string, version string, proxy string, noProxy string) (*Cmd, error) { 33 switch version { 34 // If v3 is specified (or by default, if no value is specified) then use v3 35 case "", "v3": 36 return NewCmdWithVersion(workDir, false, proxy, noProxy) 37 } 38 return nil, fmt.Errorf("helm chart version '%s' is not supported", version) 39 } 40 41 func NewCmdWithVersion(workDir string, isHelmOci bool, proxy string, noProxy string) (*Cmd, error) { 42 tmpDir, err := os.MkdirTemp("", "helm") 43 if err != nil { 44 return nil, fmt.Errorf("failed to create temporary directory for helm: %w", err) 45 } 46 return &Cmd{WorkDir: workDir, helmHome: tmpDir, IsHelmOci: isHelmOci, proxy: proxy, noProxy: noProxy}, err 47 } 48 49 var redactor = func(text string) string { 50 return regexp.MustCompile("(--username|--password) [^ ]*").ReplaceAllString(text, "$1 ******") 51 } 52 53 func (c Cmd) run(args ...string) (string, string, error) { 54 cmd := exec.Command("helm", args...) 55 cmd.Dir = c.WorkDir 56 cmd.Env = os.Environ() 57 if !c.IsLocal { 58 cmd.Env = append(cmd.Env, 59 fmt.Sprintf("XDG_CACHE_HOME=%s/cache", c.helmHome), 60 fmt.Sprintf("XDG_CONFIG_HOME=%s/config", c.helmHome), 61 fmt.Sprintf("XDG_DATA_HOME=%s/data", c.helmHome), 62 fmt.Sprintf("HELM_CONFIG_HOME=%s/config", c.helmHome)) 63 } 64 65 if c.IsHelmOci { 66 cmd.Env = append(cmd.Env, "HELM_EXPERIMENTAL_OCI=1") 67 } 68 69 cmd.Env = proxy.UpsertEnv(cmd, c.proxy, c.noProxy) 70 71 out, err := executil.RunWithRedactor(cmd, redactor) 72 fullCommand := executil.GetCommandArgsToLog(cmd) 73 if err != nil { 74 return out, fullCommand, fmt.Errorf("failed to get command args to log: %w", err) 75 } 76 return out, fullCommand, nil 77 } 78 79 func (c *Cmd) RegistryLogin(repo string, creds Creds) (string, error) { 80 args := []string{"registry", "login"} 81 args = append(args, repo) 82 83 if creds.GetUsername() != "" { 84 args = append(args, "--username", creds.GetUsername()) 85 } 86 87 helmPassword, err := creds.GetPassword() 88 if err != nil { 89 return "", fmt.Errorf("failed to get password for helm registry: %w", err) 90 } 91 if helmPassword != "" { 92 args = append(args, "--password", helmPassword) 93 } 94 95 if creds.GetCAPath() != "" { 96 args = append(args, "--ca-file", creds.GetCAPath()) 97 } 98 99 if len(creds.GetCertData()) > 0 { 100 filePath, closer, err := writeToTmp(creds.GetCertData()) 101 if err != nil { 102 return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err) 103 } 104 defer utilio.Close(closer) 105 args = append(args, "--cert-file", filePath) 106 } 107 108 if len(creds.GetKeyData()) > 0 { 109 filePath, closer, err := writeToTmp(creds.GetKeyData()) 110 if err != nil { 111 return "", fmt.Errorf("failed to write key data to temporary file: %w", err) 112 } 113 defer utilio.Close(closer) 114 args = append(args, "--key-file", filePath) 115 } 116 117 if creds.GetInsecureSkipVerify() { 118 args = append(args, "--insecure") 119 } 120 out, _, err := c.run(args...) 121 if err != nil { 122 return "", fmt.Errorf("failed to login to registry: %w", err) 123 } 124 return out, nil 125 } 126 127 func (c *Cmd) RegistryLogout(repo string, _ Creds) (string, error) { 128 args := []string{"registry", "logout"} 129 args = append(args, repo) 130 out, _, err := c.run(args...) 131 if err != nil { 132 return "", fmt.Errorf("failed to logout from registry: %w", err) 133 } 134 return out, nil 135 } 136 137 func (c *Cmd) RepoAdd(name string, url string, opts Creds, passCredentials bool) (string, error) { 138 tmp, err := os.MkdirTemp("", "helm") 139 if err != nil { 140 return "", fmt.Errorf("failed to create temporary directory for repo: %w", err) 141 } 142 defer func() { _ = os.RemoveAll(tmp) }() 143 144 args := []string{"repo", "add"} 145 146 if opts.GetUsername() != "" { 147 args = append(args, "--username", opts.GetUsername()) 148 } 149 150 helmPassword, err := opts.GetPassword() 151 if err != nil { 152 return "", fmt.Errorf("failed to get password for helm registry: %w", err) 153 } 154 if helmPassword != "" { 155 args = append(args, "--password", helmPassword) 156 } 157 158 if opts.GetCAPath() != "" { 159 args = append(args, "--ca-file", opts.GetCAPath()) 160 } 161 162 if opts.GetInsecureSkipVerify() { 163 args = append(args, "--insecure-skip-tls-verify") 164 } 165 166 if len(opts.GetCertData()) > 0 { 167 certFile, err := os.CreateTemp("", "helm") 168 if err != nil { 169 return "", fmt.Errorf("failed to create temporary certificate file: %w", err) 170 } 171 _, err = certFile.Write(opts.GetCertData()) 172 if err != nil { 173 return "", fmt.Errorf("failed to write certificate data: %w", err) 174 } 175 defer certFile.Close() 176 args = append(args, "--cert-file", certFile.Name()) 177 } 178 179 if len(opts.GetKeyData()) > 0 { 180 keyFile, err := os.CreateTemp("", "helm") 181 if err != nil { 182 return "", fmt.Errorf("failed to create temporary key file: %w", err) 183 } 184 _, err = keyFile.Write(opts.GetKeyData()) 185 if err != nil { 186 return "", fmt.Errorf("failed to write key data: %w", err) 187 } 188 defer keyFile.Close() 189 args = append(args, "--key-file", keyFile.Name()) 190 } 191 192 if passCredentials { 193 args = append(args, "--pass-credentials") 194 } 195 196 args = append(args, name, url) 197 198 out, _, err := c.run(args...) 199 if err != nil { 200 return "", fmt.Errorf("failed to add repository: %w", err) 201 } 202 return out, err 203 } 204 205 func writeToTmp(data []byte) (string, utilio.Closer, error) { 206 file, err := os.CreateTemp("", "") 207 if err != nil { 208 return "", nil, fmt.Errorf("failed to create temporary file: %w", err) 209 } 210 err = os.WriteFile(file.Name(), data, 0o644) 211 if err != nil { 212 _ = os.RemoveAll(file.Name()) 213 return "", nil, fmt.Errorf("failed to write data to temporary file: %w", err) 214 } 215 defer func() { 216 if err = file.Close(); err != nil { 217 log.WithFields(log.Fields{ 218 common.SecurityField: common.SecurityMedium, 219 common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, 220 }).Errorf("error closing file %q: %v", file.Name(), err) 221 } 222 }() 223 return file.Name(), utilio.NewCloser(func() error { 224 return os.RemoveAll(file.Name()) 225 }), nil 226 } 227 228 func (c *Cmd) Fetch(repo, chartName, version, destination string, creds Creds, passCredentials bool) (string, error) { 229 args := []string{"pull", "--destination", destination} 230 if version != "" { 231 args = append(args, "--version", version) 232 } 233 if creds.GetUsername() != "" { 234 args = append(args, "--username", creds.GetUsername()) 235 } 236 237 helmPassword, err := creds.GetPassword() 238 if err != nil { 239 return "", fmt.Errorf("failed to get password for helm registry: %w", err) 240 } 241 if helmPassword != "" { 242 args = append(args, "--password", helmPassword) 243 } 244 if creds.GetInsecureSkipVerify() { 245 args = append(args, "--insecure-skip-tls-verify") 246 } 247 248 args = append(args, "--repo", repo, chartName) 249 250 if creds.GetCAPath() != "" { 251 args = append(args, "--ca-file", creds.GetCAPath()) 252 } 253 if len(creds.GetCertData()) > 0 { 254 filePath, closer, err := writeToTmp(creds.GetCertData()) 255 if err != nil { 256 return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err) 257 } 258 defer utilio.Close(closer) 259 args = append(args, "--cert-file", filePath) 260 } 261 if len(creds.GetKeyData()) > 0 { 262 filePath, closer, err := writeToTmp(creds.GetKeyData()) 263 if err != nil { 264 return "", fmt.Errorf("failed to write key data to temporary file: %w", err) 265 } 266 defer utilio.Close(closer) 267 args = append(args, "--key-file", filePath) 268 } 269 if passCredentials { 270 args = append(args, "--pass-credentials") 271 } 272 273 out, _, err := c.run(args...) 274 if err != nil { 275 return "", fmt.Errorf("failed to fetch chart: %w", err) 276 } 277 return out, nil 278 } 279 280 func (c *Cmd) PullOCI(repo string, chart string, version string, destination string, creds Creds) (string, error) { 281 args := []string{ 282 "pull", fmt.Sprintf("oci://%s/%s", repo, chart), "--version", 283 version, 284 "--destination", 285 destination, 286 } 287 if creds.GetCAPath() != "" { 288 args = append(args, "--ca-file", creds.GetCAPath()) 289 } 290 291 if len(creds.GetCertData()) > 0 { 292 filePath, closer, err := writeToTmp(creds.GetCertData()) 293 if err != nil { 294 return "", fmt.Errorf("failed to write certificate data to temporary file: %w", err) 295 } 296 defer utilio.Close(closer) 297 args = append(args, "--cert-file", filePath) 298 } 299 300 if len(creds.GetKeyData()) > 0 { 301 filePath, closer, err := writeToTmp(creds.GetKeyData()) 302 if err != nil { 303 return "", fmt.Errorf("failed to write key data to temporary file: %w", err) 304 } 305 defer utilio.Close(closer) 306 args = append(args, "--key-file", filePath) 307 } 308 309 if creds.GetInsecureSkipVerify() { 310 args = append(args, "--insecure-skip-tls-verify") 311 } 312 out, _, err := c.run(args...) 313 if err != nil { 314 return "", fmt.Errorf("failed to pull OCI chart: %w", err) 315 } 316 return out, nil 317 } 318 319 func (c *Cmd) dependencyBuild() (string, error) { 320 out, _, err := c.run("dependency", "build") 321 if err != nil { 322 return "", fmt.Errorf("failed to build dependencies: %w", err) 323 } 324 return out, nil 325 } 326 327 func (c *Cmd) inspectValues(values string) (string, error) { 328 out, _, err := c.run("show", "values", values) 329 if err != nil { 330 return "", fmt.Errorf("failed to inspect values: %w", err) 331 } 332 return out, nil 333 } 334 335 func (c *Cmd) InspectChart() (string, error) { 336 out, _, err := c.run("show", "chart", ".") 337 if err != nil { 338 return "", fmt.Errorf("failed to inspect chart: %w", err) 339 } 340 return out, nil 341 } 342 343 type TemplateOpts struct { 344 Name string 345 Namespace string 346 KubeVersion string 347 APIVersions []string 348 Set map[string]string 349 SetString map[string]string 350 SetFile map[string]pathutil.ResolvedFilePath 351 Values []pathutil.ResolvedFilePath 352 // ExtraValues is the randomly-generated path to the temporary values file holding the contents of 353 // spec.source.helm.values/valuesObject. 354 ExtraValues pathutil.ResolvedFilePath 355 SkipCrds bool 356 SkipSchemaValidation bool 357 SkipTests bool 358 } 359 360 func cleanSetParameters(val string) string { 361 // `{}` equal helm list parameters format, so don't escape `,`. 362 if strings.HasPrefix(val, `{`) && strings.HasSuffix(val, `}`) { 363 return val 364 } 365 366 val = replaceAllWithLookbehind(val, ',', `\,`, '\\') 367 return val 368 } 369 370 func replaceAllWithLookbehind(val string, old rune, newV string, lookbehind rune) string { 371 var result strings.Builder 372 var prevR rune 373 for _, r := range val { 374 if r == old { 375 if prevR != lookbehind { 376 result.WriteString(newV) 377 } else { 378 result.WriteRune(old) 379 } 380 } else { 381 result.WriteRune(r) 382 } 383 prevR = r 384 } 385 return result.String() 386 } 387 388 var apiVersionsRemover = regexp.MustCompile(`(--api-versions [^ ]+ )+`) 389 390 func (c *Cmd) template(chartPath string, opts *TemplateOpts) (string, string, error) { 391 if callback, err := cleanupChartLockFile(filepath.Clean(path.Join(c.WorkDir, chartPath))); err == nil { 392 defer callback() 393 } else { 394 return "", "", fmt.Errorf("failed to clean up chart lock file: %w", err) 395 } 396 397 args := []string{"template", chartPath, "--name-template", opts.Name} 398 399 if opts.Namespace != "" { 400 args = append(args, "--namespace", opts.Namespace) 401 } 402 if opts.KubeVersion != "" { 403 args = append(args, "--kube-version", opts.KubeVersion) 404 } 405 for key, val := range opts.Set { 406 args = append(args, "--set", key+"="+cleanSetParameters(val)) 407 } 408 for key, val := range opts.SetString { 409 args = append(args, "--set-string", key+"="+cleanSetParameters(val)) 410 } 411 for key, val := range opts.SetFile { 412 args = append(args, "--set-file", key+"="+cleanSetParameters(string(val))) 413 } 414 for _, val := range opts.Values { 415 args = append(args, "--values", string(val)) 416 } 417 if opts.ExtraValues != "" { 418 args = append(args, "--values", string(opts.ExtraValues)) 419 } 420 for _, v := range opts.APIVersions { 421 args = append(args, "--api-versions", v) 422 } 423 if !opts.SkipCrds { 424 args = append(args, "--include-crds") 425 } 426 if opts.SkipSchemaValidation { 427 args = append(args, "--skip-schema-validation") 428 } 429 if opts.SkipTests { 430 args = append(args, "--skip-tests") 431 } 432 433 out, command, err := c.run(args...) 434 if err != nil { 435 msg := err.Error() 436 if strings.Contains(msg, "--api-versions") { 437 log.Debug(msg) 438 msg = apiVersionsRemover.ReplaceAllString(msg, "<api versions removed> ") 439 } 440 return "", command, errors.New(msg) 441 } 442 return out, command, nil 443 } 444 445 // Workaround for Helm3 behavior (see https://github.com/helm/helm/issues/6870). 446 // The `helm template` command generates Chart.lock after which `helm dependency build` does not work 447 // As workaround removing lock file unless it exists before running helm template 448 func cleanupChartLockFile(chartPath string) (func(), error) { 449 exists := true 450 lockPath := path.Join(chartPath, "Chart.lock") 451 if _, err := os.Stat(lockPath); err != nil { 452 if !os.IsNotExist(err) { 453 return nil, fmt.Errorf("failed to check lock file status: %w", err) 454 } 455 exists = false 456 } 457 return func() { 458 if !exists { 459 _ = os.Remove(lockPath) 460 } 461 }, nil 462 } 463 464 func (c *Cmd) Freestyle(args ...string) (string, error) { 465 out, _, err := c.run(args...) 466 if err != nil { 467 return "", fmt.Errorf("failed to execute freestyle helm command: %w", err) 468 } 469 return out, nil 470 } 471 472 func (c *Cmd) Close() { 473 _ = os.RemoveAll(c.helmHome) 474 }