mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-06 11:13:54 +02:00
Closes #444 and #782 This is the final PR to fully cache and parallelize helm secret decryption. It threads the shared helmexec.Interface into the StateCreator and HelmState structs to be used during environment secret decryption. This should effectively cache secrets for the duration of a helmfile run, regardless of where they are first decrypted.
318 lines
8.5 KiB
Go
318 lines
8.5 KiB
Go
package state
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/imdario/mergo"
|
|
"github.com/roboll/helmfile/pkg/environment"
|
|
"github.com/roboll/helmfile/pkg/helmexec"
|
|
"github.com/roboll/helmfile/pkg/maputil"
|
|
"go.uber.org/zap"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type StateLoadError struct {
|
|
msg string
|
|
Cause error
|
|
}
|
|
|
|
func (e *StateLoadError) Error() string {
|
|
return fmt.Sprintf("%s: %v", e.msg, e.Cause)
|
|
}
|
|
|
|
type UndefinedEnvError struct {
|
|
msg string
|
|
}
|
|
|
|
func (e *UndefinedEnvError) Error() string {
|
|
return e.msg
|
|
}
|
|
|
|
type StateCreator struct {
|
|
logger *zap.SugaredLogger
|
|
readFile func(string) ([]byte, error)
|
|
fileExists func(string) (bool, error)
|
|
abs func(string) (string, error)
|
|
glob func(string) ([]string, error)
|
|
helm helmexec.Interface
|
|
|
|
Strict bool
|
|
|
|
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
|
|
}
|
|
|
|
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error), helm helmexec.Interface) *StateCreator {
|
|
return &StateCreator{
|
|
logger: logger,
|
|
readFile: readFile,
|
|
fileExists: fileExists,
|
|
abs: abs,
|
|
glob: glob,
|
|
Strict: true,
|
|
helm: helm,
|
|
}
|
|
}
|
|
|
|
// Parse parses YAML into HelmState
|
|
func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, error) {
|
|
var state HelmState
|
|
|
|
state.FilePath = file
|
|
state.basePath = baseDir
|
|
state.helm = c.helm
|
|
|
|
decoder := yaml.NewDecoder(bytes.NewReader(content))
|
|
if !c.Strict {
|
|
decoder.SetStrict(false)
|
|
} else {
|
|
decoder.SetStrict(true)
|
|
}
|
|
i := 0
|
|
for {
|
|
i++
|
|
|
|
var intermediate HelmState
|
|
|
|
err := decoder.Decode(&intermediate)
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
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); err != nil {
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
|
|
}
|
|
}
|
|
|
|
if len(state.DeprecatedReleases) > 0 {
|
|
if len(state.Releases) > 0 {
|
|
return nil, fmt.Errorf("failed to parse %s: you can't specify both `charts` and `releases` sections", file)
|
|
}
|
|
state.Releases = state.DeprecatedReleases
|
|
state.DeprecatedReleases = []ReleaseSpec{}
|
|
}
|
|
|
|
if state.DeprecatedContext != "" && state.HelmDefaults.KubeContext == "" {
|
|
state.HelmDefaults.KubeContext = state.DeprecatedContext
|
|
}
|
|
|
|
state.logger = c.logger
|
|
|
|
state.readFile = c.readFile
|
|
state.removeFile = os.Remove
|
|
state.fileExists = c.fileExists
|
|
state.glob = c.glob
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// LoadEnvValues loads environment values files relative to the `baseDir`
|
|
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment) (*HelmState, error) {
|
|
state := *target
|
|
|
|
e, err := state.loadEnvValues(env, ctxEnv, c.readFile, c.glob)
|
|
if err != nil {
|
|
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
|
|
}
|
|
|
|
e.Defaults, err = state.loadValuesEntries(nil, state.DefaultValues)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state.Env = *e
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
|
|
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues *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!")
|
|
}
|
|
}
|
|
|
|
state, err = c.loadBases(envValues, state, baseDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state, err = c.LoadEnvValues(state, envName, envValues)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state.FilePath = file
|
|
|
|
return state, nil
|
|
}
|
|
|
|
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
|
|
layers := []*HelmState{}
|
|
for _, b := range st.Bases {
|
|
base, err := c.LoadFile(envValues, baseDir, b, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layers = append(layers, base)
|
|
}
|
|
layers = append(layers, st)
|
|
|
|
for i := 1; i < len(layers); i++ {
|
|
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return layers[0], nil
|
|
}
|
|
|
|
func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
|
|
envVals := map[string]interface{}{}
|
|
envSpec, ok := st.Environments[name]
|
|
if ok {
|
|
var err error
|
|
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(envSpec.Secrets) > 0 {
|
|
|
|
var envSecretFiles []string
|
|
for _, urlOrPath := range envSpec.Secrets {
|
|
resolved, skipped, err := st.storage().resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if skipped {
|
|
continue
|
|
}
|
|
|
|
envSecretFiles = append(envSecretFiles, resolved...)
|
|
}
|
|
if err = st.scatterGatherEnvSecretFiles(envSecretFiles, envVals, readFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else if ctxEnv == nil && name != DefaultEnv {
|
|
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
|
|
}
|
|
|
|
newEnv := &environment.Environment{Name: name, Values: envVals}
|
|
|
|
if ctxEnv != nil {
|
|
intEnv := *ctxEnv
|
|
|
|
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride); err != nil {
|
|
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
|
|
}
|
|
|
|
newEnv = &intEnv
|
|
}
|
|
|
|
return newEnv, nil
|
|
}
|
|
|
|
func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
|
|
var errs []error
|
|
|
|
inputs := envSecretFiles
|
|
inputsSize := len(inputs)
|
|
|
|
type secretResult struct {
|
|
result map[string]interface{}
|
|
err error
|
|
path string
|
|
}
|
|
|
|
secrets := make(chan string, inputsSize)
|
|
results := make(chan secretResult, inputsSize)
|
|
helm := st.helm
|
|
|
|
st.scatterGather(0, inputsSize,
|
|
func() {
|
|
for _, secretFile := range envSecretFiles {
|
|
secrets <- secretFile
|
|
}
|
|
close(secrets)
|
|
},
|
|
func(id int) {
|
|
for path := range secrets {
|
|
release := &ReleaseSpec{}
|
|
flags := st.appendConnectionFlags([]string{}, release)
|
|
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
|
|
if err != nil {
|
|
results <- secretResult{nil, err, path}
|
|
continue
|
|
}
|
|
bytes, err := readFile(decFile)
|
|
if err != nil {
|
|
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
|
continue
|
|
}
|
|
m := map[string]interface{}{}
|
|
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
|
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), 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[interface{}]interface{} with map[string]interface{} or vice versa.
|
|
// See https://github.com/roboll/helmfile/issues/677
|
|
vals, err := maputil.CastKeysToStrings(m)
|
|
if err != nil {
|
|
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
|
continue
|
|
}
|
|
results <- secretResult{vals, nil, path}
|
|
}
|
|
},
|
|
func() {
|
|
for i := 0; i < inputsSize; i++ {
|
|
result := <-results
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
close(results)
|
|
},
|
|
)
|
|
|
|
if len(errs) > 1 {
|
|
for _, err := range errs {
|
|
st.logger.Error(err)
|
|
}
|
|
return fmt.Errorf("Failed loading environment secrets with %d errors", len(errs))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}) (map[string]interface{}, error) {
|
|
envVals := map[string]interface{}{}
|
|
|
|
valuesEntries := append([]interface{}{}, entries...)
|
|
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger)
|
|
var err error
|
|
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return envVals, nil
|
|
}
|