mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-14 22:26:04 +02:00
* feat: add dir= selector for path-based release filtering and traversal skip Introduce a new selector key `dir=<path>` to the -l/--selector flag with two effects: it filters releases by the directory of their defining helmfile relative to the root helmfile (using directory-prefix matching, so dir=apps/foo matches both apps/foo and apps/foo/sub), and it short-circuits sub-helmfile traversal when paired with positive dir= constraints, so non-matching branches in the helmfiles: tree are not parsed, templated, or fetched. The motivation is consuming aggregator-style upstreams that the operator does not control (opendesk being the immediate example), where the user cannot restructure the helmfile layout but still wants to act on a subset. The dir label is auto-populated at filter time only; user-facing label output is unchanged. The label key "dir" is reserved at state load. Selectors that escape the root via .. or absolute paths, and the bare ".", are rejected at parse time. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * refactor: use sync.Once for root dir resolution, extract skipForDirFilter Add a unit test covering the per-entry skip decision: match descends, sibling skipped, remote never short-circuited. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * cmd: shorten dir selector help text, link to docs Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * docs: highlight selectors inheritance caveat, add bases example for dir label Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * feat: warn instead of fail on user-defined dir label Hard-failing on an existing dir label is a breaking change. Log a deprecation warning for now and turn it into an error in a later release. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * refactor: apply go fix modernizers to dir selector code Upstream CI now fails when go fix would rewrite code. Use maps.Copy in injectLabel and drop the explicit embedded ReleaseSetSpec in test literals. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * fix: tighten dir selector edge cases found in review - Always shadow a user-defined dir label at match time, also when the release has no auto-populated value (remote or outside-root helmfiles). Previously the user value was matched there despite the warning. - Skip sibling helmfiles entries that are directories, not only files. - Warn about a dir label declared in a release template, and warn only once when it comes from a bases file. - Share the root-relative path and at-or-below checks between the release filter and the traversal skip; a directory whose name merely starts with two dots is no longer treated as escaping the root. - Log when the root directory cannot be resolved. - Tests: require for setup steps, t.Chdir, shared app fixture. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * docs: sync dir selector CLI help, document anchor and traversal assumption docs/cli.md still carried the long help text. Also spell out what the dir value is relative to, that the traversal skip assumes nested helmfiles stay below their parent's directory, and that user-defined dir labels are ignored for releases without an auto-populated value as well. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * fix: complete dir selector values relative to the root helmfile Completion listed directories relative to the working directory, while dir values are relative to the root helmfile directory. Resolve the root from -f or HELMFILE_FILE_PATH with the same logic the app uses, and offer nothing for values the selector would reject (absolute paths, paths that escape the root) or for a remote root helmfile. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * refactor(state): flatten nested conditionals in label filter parsing and matching - LabelFilter.Match: extract labelMatches helper; positive/negative loops become single-level guards. Also removes the pre-existing empty then-branch (if !ok {} else if) in the negative-label loop and the redundant len()>0 guards around the loops - positiveLabelsCompatibleWith: extract positiveLabelPairCompatible so the double loop holds one guard instead of a nested if chain - ParseLabels: extract parseOneLabel (separator detection, dir= value validation, split) so the three-level regex/dir/error nesting becomes flat; drop the vestigial trailing 'var err error' return No behavior change; state/app/cmd tests incl. -race and golangci-lint pass. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> Signed-off-by: yxxhero <aiopsclub@163.com> Co-authored-by: yxxhero <aiopsclub@163.com>
587 lines
18 KiB
Go
587 lines
18 KiB
Go
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"dario.cat/mergo"
|
|
"github.com/helmfile/vals"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/helmfile/helmfile/pkg/environment"
|
|
"github.com/helmfile/helmfile/pkg/filesystem"
|
|
"github.com/helmfile/helmfile/pkg/helmexec"
|
|
"github.com/helmfile/helmfile/pkg/maputil"
|
|
"github.com/helmfile/helmfile/pkg/remote"
|
|
"github.com/helmfile/helmfile/pkg/yaml"
|
|
)
|
|
|
|
const (
|
|
DefaultHelmBinary = "helm"
|
|
DefaultKustomizeBinary = "kustomize"
|
|
DefaultHCLFileExtension = ".hcl"
|
|
)
|
|
|
|
var ValidUpdateStrategyValues = []string{UpdateStrategyReinstallIfForbidden}
|
|
|
|
type StateLoadError struct {
|
|
Msg string
|
|
Cause error
|
|
}
|
|
|
|
func (e *StateLoadError) Error() string {
|
|
return fmt.Sprintf("%s: %v", e.Msg, e.Cause)
|
|
}
|
|
|
|
type UndefinedEnvError struct {
|
|
Env string
|
|
}
|
|
|
|
func (e *UndefinedEnvError) Error() string {
|
|
return fmt.Sprintf("environment \"%s\" is not defined", e.Env)
|
|
}
|
|
|
|
type InvalidUpdateStrategyError struct {
|
|
UpdateStrategy string
|
|
}
|
|
|
|
func (e *InvalidUpdateStrategyError) Error() string {
|
|
return fmt.Sprintf("updateStrategy %q is invalid, valid values are: %s or not set", e.UpdateStrategy, strings.Join(ValidUpdateStrategyValues, ", "))
|
|
}
|
|
|
|
type StateCreator struct {
|
|
logger *zap.SugaredLogger
|
|
|
|
fs *filesystem.FileSystem
|
|
|
|
valsRuntime vals.Evaluator
|
|
|
|
Strict bool
|
|
|
|
LoadFile func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
|
|
|
|
getHelm func(*HelmState) (helmexec.Interface, error)
|
|
|
|
overrideHelmBinary string
|
|
|
|
overrideKustomizeBinary string
|
|
|
|
enableLiveOutput bool
|
|
|
|
remote *remote.Remote
|
|
|
|
lockFile string
|
|
}
|
|
|
|
func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntime vals.Evaluator, getHelm func(*HelmState) (helmexec.Interface, error), overrideHelmBinary string, overrideKustomizeBinary string, remote *remote.Remote, enableLiveOutput bool, lockFile string) *StateCreator {
|
|
return &StateCreator{
|
|
logger: logger,
|
|
|
|
Strict: true,
|
|
fs: fs,
|
|
valsRuntime: valsRuntime,
|
|
getHelm: getHelm,
|
|
|
|
overrideHelmBinary: overrideHelmBinary,
|
|
overrideKustomizeBinary: overrideKustomizeBinary,
|
|
enableLiveOutput: enableLiveOutput,
|
|
|
|
remote: remote,
|
|
|
|
lockFile: lockFile,
|
|
}
|
|
}
|
|
|
|
// Parse parses YAML into HelmState
|
|
func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, error) {
|
|
var state HelmState
|
|
|
|
state.fs = c.fs
|
|
state.FilePath = file
|
|
state.basePath = baseDir
|
|
|
|
state.LockFile = c.lockFile
|
|
|
|
decode := yaml.NewDecoder(content, c.Strict)
|
|
|
|
i := 0
|
|
for {
|
|
i++
|
|
|
|
var intermediate HelmState
|
|
|
|
intermediate.FilePath = file
|
|
|
|
err := decode(&intermediate)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
if filepath.Ext(file) != ".gotmpl" {
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d. Started seeing this since Helmfile v1? Add the .gotmpl file extension", file, i), err}
|
|
}
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d", file, i), err}
|
|
}
|
|
|
|
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
|
|
}
|
|
}
|
|
|
|
state.logger = c.logger
|
|
state.valsRuntime = c.valsRuntime
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// ApplyDefaultsAndOverrides applies default binary paths and command-line overrides.
|
|
// This is an exported version of applyDefaultsAndOverrides for use by the app package.
|
|
func (c *StateCreator) ApplyDefaultsAndOverrides(state *HelmState) {
|
|
c.applyDefaultsAndOverrides(state)
|
|
}
|
|
|
|
// applyDefaultsAndOverrides applies default binary paths and command-line overrides
|
|
func (c *StateCreator) applyDefaultsAndOverrides(state *HelmState) {
|
|
if c.overrideHelmBinary != "" && c.overrideHelmBinary != DefaultHelmBinary {
|
|
state.DefaultHelmBinary = c.overrideHelmBinary
|
|
} else if state.DefaultHelmBinary == "" {
|
|
// Let `helmfile --helm-binary ""` not break this helmfile run
|
|
state.DefaultHelmBinary = DefaultHelmBinary
|
|
}
|
|
|
|
if c.overrideKustomizeBinary != "" && c.overrideKustomizeBinary != DefaultKustomizeBinary {
|
|
state.DefaultKustomizeBinary = c.overrideKustomizeBinary
|
|
} else if state.DefaultKustomizeBinary == "" {
|
|
// Let `helmfile --kustomize-binary ""` not break this helmfile run
|
|
state.DefaultKustomizeBinary = DefaultKustomizeBinary
|
|
}
|
|
}
|
|
|
|
// LoadEnvValues loads environment values files relative to the `baseDir`
|
|
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, failOnMissingEnv bool, ctxEnv, overrode *environment.Environment) (*HelmState, error) {
|
|
state := *target
|
|
|
|
e, err := c.loadEnvValues(&state, env, failOnMissingEnv, ctxEnv, overrode)
|
|
if err != nil {
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
|
|
}
|
|
|
|
newDefaults, err := state.loadValuesEntries(nil, state.DefaultValues, c.remote, ctxEnv, env, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := mergo.Merge(&e.Defaults, newDefaults, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state.Env = *e
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
|
|
// evaluateBases=true means that this is NOT a base helmfile
|
|
// applyDefaults=true means that applyDefaultsAndOverrides should be called
|
|
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, failOnMissingEnv, evaluateBases, applyDefaults bool, envValues, overrode *environment.Environment) (*HelmState, error) {
|
|
state, err := c.Parse(content, baseDir, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !evaluateBases {
|
|
if len(state.Bases) > 0 {
|
|
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
|
|
}
|
|
} else {
|
|
state, err = c.loadBases(envValues, overrode, state, baseDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply default binaries and command-line overrides only for the main helmfile
|
|
// after loading and merging all bases. This ensures that values from bases are
|
|
// properly respected and that later bases/documents can override earlier ones.
|
|
if applyDefaults {
|
|
c.applyDefaultsAndOverrides(state)
|
|
}
|
|
}
|
|
|
|
state, err = c.LoadEnvValues(state, envName, failOnMissingEnv, envValues, overrode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state.FilePath = file
|
|
|
|
vals, err := state.Env.GetMergedValues()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rendering values: %w", err)
|
|
}
|
|
state.RenderedValues = vals
|
|
|
|
if evaluateBases {
|
|
for _, w := range state.reservedLabelWarnings() {
|
|
c.logger.Warnf("WARNING: %s", w)
|
|
}
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// mergeEnvironments deeply merges environment specifications from src into dst.
|
|
// Unlike mergo.WithOverride which replaces entire EnvironmentSpec values, this function
|
|
// properly merges the Values slices from both environments.
|
|
func mergeEnvironments(dst, src map[string]EnvironmentSpec) {
|
|
// If dst is nil, there's nothing to merge into
|
|
if dst == nil {
|
|
return
|
|
}
|
|
|
|
for envName, srcEnv := range src {
|
|
if dstEnv, exists := dst[envName]; exists {
|
|
// Merge Secrets slices
|
|
mergedSecrets := append([]string{}, dstEnv.Secrets...)
|
|
mergedSecrets = append(mergedSecrets, srcEnv.Secrets...)
|
|
|
|
// Create merged environment
|
|
merged := EnvironmentSpec{
|
|
Secrets: mergedSecrets,
|
|
}
|
|
|
|
// Override KubeContext if src has it
|
|
if srcEnv.KubeContext != "" {
|
|
merged.KubeContext = srcEnv.KubeContext
|
|
} else {
|
|
merged.KubeContext = dstEnv.KubeContext
|
|
}
|
|
|
|
// Override MissingFileHandler if src has it
|
|
if srcEnv.MissingFileHandler != nil {
|
|
merged.MissingFileHandler = srcEnv.MissingFileHandler
|
|
} else {
|
|
merged.MissingFileHandler = dstEnv.MissingFileHandler
|
|
}
|
|
|
|
// Override MissingFileHandlerConfig if src has it
|
|
if srcEnv.MissingFileHandlerConfig != nil {
|
|
merged.MissingFileHandlerConfig = srcEnv.MissingFileHandlerConfig
|
|
} else {
|
|
merged.MissingFileHandlerConfig = dstEnv.MissingFileHandlerConfig
|
|
}
|
|
|
|
// Override MergeStrategy if src has it
|
|
if srcEnv.MergeStrategy != "" {
|
|
merged.MergeStrategy = srcEnv.MergeStrategy
|
|
} else {
|
|
merged.MergeStrategy = dstEnv.MergeStrategy
|
|
}
|
|
|
|
// Concatenate Values so the later layer (src, e.g. main helmfile)
|
|
// always takes precedence over the earlier one (dst, e.g. a base
|
|
// helmfile). Under override the natural append order achieves that
|
|
// (last-file-wins). Under fallback we have to prepend src so that
|
|
// "first-file-wins" still resolves to "later layer wins".
|
|
if merged.MergeStrategy == MergeStrategyFallback {
|
|
mergedValues := append([]any{}, srcEnv.Values...)
|
|
merged.Values = append(mergedValues, dstEnv.Values...)
|
|
} else {
|
|
mergedValues := append([]any{}, dstEnv.Values...)
|
|
merged.Values = append(mergedValues, srcEnv.Values...)
|
|
}
|
|
|
|
dst[envName] = merged
|
|
} else {
|
|
// Environment only exists in src - just copy it
|
|
dst[envName] = srcEnv
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
|
|
var newOverrodeEnv *environment.Environment
|
|
if overrodeEnv != nil {
|
|
overrodeEnvCopier := overrodeEnv.DeepCopy()
|
|
newOverrodeEnv = &overrodeEnvCopier
|
|
}
|
|
layers := []*HelmState{}
|
|
for _, b := range st.Bases {
|
|
base, err := c.LoadFile(envValues, newOverrodeEnv, baseDir, b, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layers = append(layers, base)
|
|
}
|
|
layers = append(layers, st)
|
|
|
|
for i := 1; i < len(layers); i++ {
|
|
// Initialize Environments map if nil to avoid panic in mergeEnvironments
|
|
if layers[0].Environments == nil {
|
|
layers[0].Environments = make(map[string]EnvironmentSpec)
|
|
}
|
|
|
|
// Manually merge environments to ensure deep merging of environment values
|
|
mergeEnvironments(layers[0].Environments, layers[i].Environments)
|
|
|
|
// Clear the Environments from the source before mergo to avoid override
|
|
tmpEnvs := layers[i].Environments
|
|
layers[i].Environments = nil
|
|
|
|
// Now merge the rest of the fields
|
|
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Restore the Environments back to the source layer (in case it's used later)
|
|
layers[i].Environments = tmpEnvs
|
|
}
|
|
|
|
return layers[0], nil
|
|
}
|
|
|
|
// getEnvMissingFileHandlerConfig returns the first non-nil MissingFileHandlerConfig from the environment spec, state, or default.
|
|
func (st *HelmState) getEnvMissingFileHandlerConfig(es EnvironmentSpec) *MissingFileHandlerConfig {
|
|
switch {
|
|
case es.MissingFileHandlerConfig != nil:
|
|
return es.MissingFileHandlerConfig
|
|
case st.MissingFileHandlerConfig != nil:
|
|
return st.MissingFileHandlerConfig
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// getEnvMissingFileHandler returns the first non-nil MissingFileHandler from the environment spec, state, or default.
|
|
func (st *HelmState) getEnvMissingFileHandler(es EnvironmentSpec) *string {
|
|
defaultMissingFileHandler := "Error"
|
|
switch {
|
|
case es.MissingFileHandler != nil:
|
|
return es.MissingFileHandler
|
|
case st.MissingFileHandler != nil:
|
|
return st.MissingFileHandler
|
|
default:
|
|
return &defaultMissingFileHandler
|
|
}
|
|
}
|
|
|
|
// nolint: unparam
|
|
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv, overrode *environment.Environment) (*environment.Environment, error) {
|
|
secretVals := map[string]any{}
|
|
valuesVals := map[string]any{}
|
|
envSpec, ok := st.Environments[name]
|
|
decryptedFiles := []string{}
|
|
if ok {
|
|
var err error
|
|
// To keep supporting the secrets entries having precedence over the values
|
|
// This require to be done in 2 steps for HCL encrypted file support :
|
|
// 1. Get the Secrets
|
|
// 2. Merge the secrets with the envValues after
|
|
// Also makes the fail +- faster as it's trying to decrypt before loading values
|
|
var envSecretFiles []string
|
|
if len(envSpec.Secrets) > 0 {
|
|
for _, urlOrPath := range envSpec.Secrets {
|
|
resolved, skipped, err := st.storage().resolveFile(st.getEnvMissingFileHandler(envSpec), "environment values", urlOrPath, st.getEnvMissingFileHandlerConfig(envSpec).resolveFileOptions()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if skipped {
|
|
continue
|
|
}
|
|
envSecretFiles = append(envSecretFiles, resolved...)
|
|
}
|
|
keepSecretFilesExtensions := []string{DefaultHCLFileExtension}
|
|
decryptedFiles, err = c.scatterGatherEnvSecretFiles(st, envSecretFiles, secretVals, keepSecretFilesExtensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
for _, file := range decryptedFiles {
|
|
if err := c.fs.DeleteFile(file); err != nil {
|
|
c.logger.Warnf("failed removing decrypted file %s: %w", file, err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
var valuesFiles []any
|
|
for _, f := range decryptedFiles {
|
|
valuesFiles = append(valuesFiles, f)
|
|
}
|
|
envValuesEntries := append(valuesFiles, envSpec.Values...)
|
|
loadValuesEntriesEnv, err := ctxEnv.Merge(overrode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valuesVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envValuesEntries, c.remote, loadValuesEntriesEnv, name, envSpec.MergeStrategy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = mergo.Merge(&valuesVals, &secretVals, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
} else if ctxEnv == nil && overrode == nil && name != DefaultEnv && failOnMissingEnv {
|
|
return nil, &UndefinedEnvError{Env: name}
|
|
}
|
|
|
|
newEnv := &environment.Environment{Name: name, Values: valuesVals, KubeContext: envSpec.KubeContext, Defaults: map[string]any{}, CLIOverrides: map[string]any{}}
|
|
|
|
if ctxEnv != nil {
|
|
// Merge base defaults (from top-level values:) - needed for multi-part helmfiles
|
|
// ctxEnv.Defaults is the base, newEnv.Defaults overrides (later parts win)
|
|
newEnv.Defaults = maputil.MergeMaps(ctxEnv.Defaults, newEnv.Defaults)
|
|
// Merge layer values - ctxEnv is base, newEnv (current part) overrides
|
|
newEnv.Values = maputil.MergeMaps(ctxEnv.Values, newEnv.Values)
|
|
// Copy CLI overrides to be merged at GetMergedValues time
|
|
newEnv.CLIOverrides = maputil.MergeMaps(newEnv.CLIOverrides, ctxEnv.CLIOverrides,
|
|
maputil.MergeOptions{ArrayStrategy: maputil.ArrayMergeStrategyMerge})
|
|
if ctxEnv.Name != "" {
|
|
newEnv.Name = ctxEnv.Name
|
|
}
|
|
if ctxEnv.KubeContext != "" {
|
|
newEnv.KubeContext = ctxEnv.KubeContext
|
|
}
|
|
}
|
|
|
|
if overrode != nil {
|
|
// Merge base defaults from overrode
|
|
newEnv.Defaults = maputil.MergeMaps(newEnv.Defaults, overrode.Defaults)
|
|
// Merge layer values from overrode (arrays replace)
|
|
newEnv.Values = maputil.MergeMaps(newEnv.Values, overrode.Values)
|
|
// Merge CLI overrides (arrays merge element-by-element)
|
|
newEnv.CLIOverrides = maputil.MergeMaps(newEnv.CLIOverrides, overrode.CLIOverrides,
|
|
maputil.MergeOptions{ArrayStrategy: maputil.ArrayMergeStrategyMerge})
|
|
}
|
|
|
|
return newEnv, nil
|
|
}
|
|
|
|
// For all keepFileExtensions, the decrypted files will be retained
|
|
// with the specified extensions
|
|
// They will not be parsed nor added to the envVals.
|
|
// Only their decrypted filePath will be returned
|
|
// Up to the caller to remove them
|
|
func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]any, keepFileExtensions []string) ([]string, error) {
|
|
var errs []error
|
|
var decryptedFilesKeeper []string
|
|
helm, err := c.getHelm(st)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inputs := envSecretFiles
|
|
inputsSize := len(inputs)
|
|
|
|
type secretResult struct {
|
|
id int
|
|
result map[string]any
|
|
err error
|
|
path string
|
|
}
|
|
|
|
type secretInput struct {
|
|
id int
|
|
path string
|
|
}
|
|
|
|
secrets := make(chan secretInput, inputsSize)
|
|
results := make(chan secretResult, inputsSize)
|
|
|
|
st.scatterGather(0, inputsSize,
|
|
func() {
|
|
for i, secretFile := range envSecretFiles {
|
|
secrets <- secretInput{i, secretFile}
|
|
}
|
|
close(secrets)
|
|
},
|
|
func(id int) {
|
|
for secret := range secrets {
|
|
release := &ReleaseSpec{}
|
|
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), secret.path)
|
|
if err != nil {
|
|
results <- secretResult{secret.id, nil, err, secret.path}
|
|
continue
|
|
}
|
|
for _, ext := range keepFileExtensions {
|
|
if strings.HasSuffix(secret.path, ext) {
|
|
decryptedFilesKeeper = append(decryptedFilesKeeper, decFile)
|
|
}
|
|
}
|
|
// nolint: staticcheck
|
|
defer func() {
|
|
if !slices.Contains(decryptedFilesKeeper, decFile) {
|
|
if err := c.fs.DeleteFile(decFile); err != nil {
|
|
c.logger.Warnf("removing decrypted file %s: %w", decFile, err)
|
|
}
|
|
}
|
|
}()
|
|
var vals map[string]any
|
|
if !slices.Contains(decryptedFilesKeeper, decFile) {
|
|
bytes, err := c.fs.ReadFile(decFile)
|
|
if err != nil {
|
|
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
|
|
continue
|
|
}
|
|
m := map[string]any{}
|
|
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
|
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
|
|
continue
|
|
}
|
|
// All the nested map key should be string. Otherwise we get strange errors due to that
|
|
// mergo or reflect is unable to merge map[any]any with map[string]any or vice versa.
|
|
// See https://github.com/roboll/helmfile/issues/677
|
|
vals, err = maputil.CastKeysToStrings(m)
|
|
if err != nil {
|
|
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
|
|
continue
|
|
}
|
|
}
|
|
results <- secretResult{secret.id, vals, nil, secret.path}
|
|
}
|
|
},
|
|
func() {
|
|
sortedSecrets := make([]secretResult, inputsSize)
|
|
|
|
for range inputsSize {
|
|
result := <-results
|
|
sortedSecrets[result.id] = result
|
|
}
|
|
close(results)
|
|
|
|
for _, result := range sortedSecrets {
|
|
if result.err != nil {
|
|
errs = append(errs, result.err)
|
|
} else {
|
|
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to load environment secrets file \"%s\": %v", result.path, err))
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
if len(errs) > 0 {
|
|
for _, err := range errs {
|
|
st.logger.Error(err)
|
|
}
|
|
return decryptedFilesKeeper, fmt.Errorf("failed loading environment secrets with %d errors", len(errs))
|
|
}
|
|
return decryptedFilesKeeper, nil
|
|
}
|
|
|
|
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []any, remote *remote.Remote, ctxEnv *environment.Environment, envName string, mergeStrategy string) (map[string]any, error) {
|
|
var envVals map[string]any
|
|
|
|
valuesEntries := append([]any{}, entries...)
|
|
ld := NewEnvironmentValuesLoader(st.storage(), st.fs, st.logger, remote)
|
|
var err error
|
|
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries, ctxEnv, envName, mergeStrategy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return envVals, nil
|
|
}
|