mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 21:09:40 +02:00
Toolchain and CI:
- Bump go directive from 1.26.8 to 1.27.1 (go.mod)
- Use golang:1.27-alpine builder images in all Dockerfiles
- Bump golangci-lint to v2.13.2 (first release with go1.27 support)
- Add CI gate: `go fix -diff` fails when outdated Go patterns are
detected (locally: `make check-modernize`)
Note: darwin binaries now require macOS 13 or later.
Lint fixes required by golangci-lint v2.13.2:
- goconst: ignore tests (all 436 findings were test-only; goconst
got stricter since v2.12 and this option was added for it)
- openai.go: keep deprecated MaxTokens deliberately with a nolint
rationale (max_tokens is the only form universally supported by
OpenAI-compatible backends like One-API, LiteLLM, Ollama shim)
- state.go: drop always-nil flags param from appendChartVersionFlags
(renamed to chartVersionFlags, unparam)
Modernization (go fix ./..., 62 files):
- interface{} -> any, maps.Copy, strings.SplitSeq, range-over-int,
builtin min/max, slices.Contains/ContainsFunc/Sort, WaitGroup.Go,
reflect.Type.Fields(), new(expr)
- exit_error.go: strings.Builder + fmt.Fprintf instead of string
concatenation and WriteString(fmt.Sprintf(...)) (QF1012)
- chart_dependency.go: strings.CutLast for OCI dependency helpers
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Claude <noreply@anthropic.com>
210 lines
5.5 KiB
Go
210 lines
5.5 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/helmfile/vals"
|
|
|
|
"github.com/helmfile/helmfile/pkg/envvar"
|
|
)
|
|
|
|
const (
|
|
// cache size for improving performance of ref+.* secrets rendering
|
|
valsCacheSize = 512
|
|
)
|
|
|
|
var instance vals.Evaluator
|
|
var mu sync.Mutex
|
|
|
|
// ErrValsDisabled is returned by the evaluator when HELMFILE_DISABLE_VALS_STRICT is set
|
|
// and a `ref+` expression is encountered.
|
|
var ErrValsDisabled = errors.New("vals is disabled via HELMFILE_DISABLE_VALS_STRICT environment variable")
|
|
|
|
// refPlusRegexp mirrors the reference syntax understood by the vals library
|
|
// (`ref+<provider>://...` and `secretref+<provider>://...`), so that disabled-vals
|
|
// modes only report values that vals itself would have tried to resolve.
|
|
var refPlusRegexp = regexp.MustCompile(`(secret)?ref\+[^\s+:]*://`)
|
|
|
|
// passthroughEvaluator passes values through unchanged (for external vals)
|
|
type passthroughEvaluator struct{}
|
|
|
|
func (p *passthroughEvaluator) Eval(m map[string]any) (map[string]any, error) {
|
|
return normalizeMap(m), nil
|
|
}
|
|
|
|
// strictEvaluator passes through values but errors if ref+ is detected
|
|
type strictEvaluator struct{}
|
|
|
|
func (s *strictEvaluator) Eval(m map[string]any) (map[string]any, error) {
|
|
if containsRefPlus(m) {
|
|
return nil, ErrValsDisabled
|
|
}
|
|
return normalizeMap(m), nil
|
|
}
|
|
|
|
// normalizeMap converts []string values to []any to match vals.Eval behavior.
|
|
func normalizeMap(m map[string]any) map[string]any {
|
|
out := make(map[string]any, len(m))
|
|
for k, v := range m {
|
|
out[k] = normalizeValue(v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// normalizeValue recursively converts []string to []any and map[any]any to
|
|
// map[string]any, matching the type normalization performed by vals.Eval.
|
|
func normalizeValue(v any) any {
|
|
switch typed := v.(type) {
|
|
case map[string]any:
|
|
return normalizeMap(typed)
|
|
case map[any]any:
|
|
strmap := make(map[string]any, len(typed))
|
|
for k, v := range typed {
|
|
strmap[fmt.Sprintf("%v", k)] = normalizeValue(v)
|
|
}
|
|
return strmap
|
|
case []any:
|
|
a := make([]any, len(typed))
|
|
for i, e := range typed {
|
|
a[i] = normalizeValue(e)
|
|
}
|
|
return a
|
|
case []string:
|
|
a := make([]any, len(typed))
|
|
for i, s := range typed {
|
|
a[i] = s
|
|
}
|
|
return a
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func containsRefPlus(v any) bool {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return refPlusRegexp.MatchString(val)
|
|
case map[string]any:
|
|
for _, v := range val {
|
|
if containsRefPlus(v) {
|
|
return true
|
|
}
|
|
}
|
|
case map[any]any:
|
|
for _, v := range val {
|
|
if containsRefPlus(v) {
|
|
return true
|
|
}
|
|
}
|
|
case []any:
|
|
if slices.ContainsFunc(val, containsRefPlus) {
|
|
return true
|
|
}
|
|
case []string:
|
|
if slices.ContainsFunc(val, refPlusRegexp.MatchString) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func buildValsOptions() (vals.Options, error) {
|
|
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
|
|
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
|
|
// See issue #2270 and vals PR helmfile/vals#893
|
|
//
|
|
// Valid values:
|
|
// - "off" (default): No AWS SDK logging - secure, prevents credential leakage
|
|
// - "minimal": Log retries only - minimal debugging info
|
|
// - "standard": Log retries + requests - moderate debugging (previous default)
|
|
// - "verbose": Log everything - full debugging (requests, responses, bodies, signing)
|
|
// - Custom: Comma-separated values like "request,response"
|
|
//
|
|
// Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting
|
|
// Note: Case-insensitive for known values like "off", "OFF", "Off"
|
|
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
|
|
|
|
// Configure fail on missing key behavior
|
|
// Default to false for backward compatibility
|
|
// Set HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP=true to enable strict mode
|
|
// Supports common boolean values: "true", "TRUE", "1", etc.
|
|
// See issue #1563
|
|
envVal := strings.TrimSpace(os.Getenv(envvar.ValsFailOnMissingKeyInMap))
|
|
var failOnMissingKey bool
|
|
if envVal != "" {
|
|
var err error
|
|
failOnMissingKey, err = strconv.ParseBool(envVal)
|
|
if err != nil {
|
|
return vals.Options{}, fmt.Errorf("invalid value for %s: %q (must be a valid boolean)", envvar.ValsFailOnMissingKeyInMap, envVal)
|
|
}
|
|
}
|
|
|
|
// Default to "off" for security if not specified
|
|
if logLevel == "" {
|
|
logLevel = "off"
|
|
}
|
|
|
|
// Normalize known values to lowercase for case-insensitive handling
|
|
if strings.EqualFold(logLevel, "off") {
|
|
logLevel = "off"
|
|
}
|
|
|
|
opts := vals.Options{
|
|
CacheSize: valsCacheSize,
|
|
FailOnMissingKeyInMap: failOnMissingKey,
|
|
AWSLogLevel: logLevel,
|
|
}
|
|
|
|
// Also suppress vals' own internal logging unless user wants verbose output
|
|
// This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials
|
|
if logLevel == "off" {
|
|
opts.LogOutput = io.Discard
|
|
}
|
|
// For other levels, allow vals to log to default output for debugging
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
func ValsInstance() (vals.Evaluator, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if instance != nil {
|
|
return instance, nil
|
|
}
|
|
|
|
// HELMFILE_DISABLE_VALS_STRICT: error on ref+ usage
|
|
strict, _ := strconv.ParseBool(os.Getenv(envvar.DisableValsStrict))
|
|
if strict {
|
|
instance = &strictEvaluator{}
|
|
return instance, nil
|
|
}
|
|
|
|
// HELMFILE_DISABLE_VALS: pass-through for external vals
|
|
disabled, _ := strconv.ParseBool(os.Getenv(envvar.DisableVals))
|
|
if disabled {
|
|
instance = &passthroughEvaluator{}
|
|
return instance, nil
|
|
}
|
|
|
|
opts, err := buildValsOptions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
instance, err = vals.New(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return instance, nil
|
|
}
|