mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-04 21:11:31 +02:00
* fix: resolve issues #2281, #2270, #2269, and #2247 This commit addresses four critical bugs in helmfile: 1. **Issue #2281**: Fix array merging in --state-values-set - Problem: Arrays were being replaced entirely instead of merged element-by-element - Root cause: MergeMaps() didn't handle arrays, and mergo.Merge was used in some places - Solution: * Enhanced MergeMaps() with mergeSlices() and toInterfaceSlice() functions * Replaced mergo.Merge calls with MergeMaps in environment.go and create.go * Arrays now merge element-by-element, with nested maps merged recursively - Files changed: * pkg/maputil/maputil.go - Added array merging logic * pkg/maputil/maputil_test.go - Added comprehensive unit tests * pkg/environment/environment.go - Use MergeMaps instead of mergo.Merge * pkg/state/create.go - Use MergeMaps instead of mergo.Merge * test/integration/test-cases/issue-2281-array-merge/ - Integration test * test/integration/run.sh - Added new integration test 2. **Issue #2270**: Suppress AWS SDK debug logging - Problem: AWS SDK debug logs exposing sensitive information (tokens, auth headers) - Root cause: vals.New() called without LogOutput option - Solution: Set LogOutput to io.Discard in ValsInstance() - Files changed: * pkg/plugins/vals.go - Added LogOutput: io.Discard option 3. **Issue #2269**: Fix helmDefaults.skipDeps and helmDefaults.skipRefresh being ignored - Problem: skipRefresh only checked CLI flags, not helmDefaults or release settings - Root cause: Incomplete calculation at line 1559 in state.go - Solution: Added proper skipRefresh calculation mirroring skipDeps logic - Files changed: * pkg/state/state.go - Fixed skipRefresh calculation (lines 1522-1525, 1564) * pkg/state/skip_test.go - Added unit tests for skipDeps and skipRefresh 4. **Issue #2247**: Allow OCI charts without explicit version - Problem: OCI charts without version defaulted to "latest" which was then rejected - Root cause: getOCIQualifiedChartName() defaulted chartVersion to "latest" - Solution: Use release.Version directly without defaulting, only reject explicit "latest" - Files changed: * pkg/state/state.go - Remove default to "latest", use empty string * pkg/state/oci_chart_version_test.go - Added comprehensive unit tests * test/integration/test-cases/issue-2247/ - Integration test with registry * test/integration/run.sh - Added new integration test Fixes #2281, #2270, #2269, #2247 Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: correct integration test for issue #2281 array merging The helmfile template needed to pass the 'top' values to the chart so that .Values.top is accessible in the template context. Changes: - Pass state values to chart values using toYaml - Adjusted indentation for proper YAML structure - Template now correctly accesses .Values.top for array data Test output now matches expected output with proper element-by-element array merging. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: make Helm version parsing more robust in issue-2247 test Improved version parsing to handle edge cases in CI environments: - Added fallback to 3.8 if version parsing fails - Added default values for HELM_MAJOR and HELM_MINOR - Prevents test failures due to version detection issues This ensures the test runs correctly across different environments and Helm versions. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * debug: add diagnostic output for issue-2247 test failure Added debug logging to show: - helmfile command output when it succeeds unexpectedly - Helm version being used by the test This will help diagnose why the validation isn't triggering in CI. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: make OCI 'latest' validation work for all Helm versions The validation for explicit 'latest' in OCI charts was depending on helm.IsVersionAtLeast("3.8.0") which could fail if Helm version detection has issues in CI environments. Changes: - Remove Helm version check from validation - Always reject explicit 'latest' for OCI charts - Remove Helm version check from integration test - Update unit tests to expect 'latest' to fail for all Helm versions This ensures consistent behavior across all environments and Helm versions, fixing the CI failure where helm version detection was problematic. Fixes integration test failure in CI. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: remove unused helm parameter from getOCIQualifiedChartName Since the Helm version check was removed from the OCI validation, the helm parameter is no longer needed in getOCIQualifiedChartName. Changes: - Removed helm parameter from function signature - Updated all callers to not pass helm argument - Removed unused mockHelmExec test implementation - Removed unused imports (testutil, helmexec, chart) This resolves the golangci-lint unparam error. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * test: update TestGetOCIQualifiedChartName to expect 'latest' rejection Updated test case for Helm 3.7.0 to expect error when using 'latest' since we now reject explicit 'latest' for all Helm versions, not just >= 3.8.0. This aligns the test with the updated validation logic that ensures consistent behavior across all Helm versions. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: handle set -e in issue-2247 integration test The integration test script is sourced by run.sh which has `set -e` enabled. When helmfile commands fail (as expected for validation tests), the script would exit immediately before capturing the exit code. This fix temporarily disables `set -e` around each helmfile command that may fail, allowing proper exit code capture and validation. This resolves the persistent CI test failure where the test would exit at Test 1.1 without showing any error message. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: add set -e handling for helm commands in issue-2247 test Extends the previous set -e fix to cover helm package and push commands in the registry tests (Test 2.2). These commands can fail and need proper error handling without triggering immediate script exit. This ensures: - helm package failures are caught and handled gracefully - helm push failures are caught and handled gracefully - Test can skip registry tests and pass with validation-only results - set -e is properly re-enabled after each command sequence Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> --------- Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
550 lines
17 KiB
Go
550 lines
17 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
|
|
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
|
|
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, failOnMissingEnv, evaluateBases 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.
|
|
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
|
|
|
|
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 {
|
|
// Environment exists in both - merge the Values slices
|
|
mergedValues := append([]any{}, dstEnv.Values...)
|
|
mergedValues = append(mergedValues, srcEnv.Values...)
|
|
|
|
// Merge Secrets slices
|
|
mergedSecrets := append([]string{}, dstEnv.Secrets...)
|
|
mergedSecrets = append(mergedSecrets, srcEnv.Secrets...)
|
|
|
|
// Create merged environment
|
|
merged := EnvironmentSpec{
|
|
Values: mergedValues,
|
|
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
|
|
}
|
|
|
|
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)
|
|
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}
|
|
|
|
if ctxEnv != nil {
|
|
intCtxEnv := *ctxEnv
|
|
|
|
if err := mergo.Merge(&intCtxEnv, newEnv, mergo.WithOverride); err != nil {
|
|
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
|
|
}
|
|
|
|
newEnv = &intCtxEnv
|
|
}
|
|
|
|
if overrode != nil {
|
|
intOverrodeEnv := *newEnv
|
|
|
|
// Use MergeMaps instead of mergo.Merge to properly handle array merging element-by-element
|
|
// This fixes issue #2281 where arrays were being replaced entirely instead of merged
|
|
intOverrodeEnv.Values = maputil.MergeMaps(intOverrodeEnv.Values, overrode.Values)
|
|
|
|
newEnv = &intOverrodeEnv
|
|
}
|
|
|
|
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 i := 0; i < inputsSize; i++ {
|
|
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) (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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return envVals, nil
|
|
}
|