feat: "bases" for easier layerina
This adds the new configuration key `baeses` to your helmfile.yaml files, so that you can layer them without the `readFile` template function, which was a bit unintuitive. Please see https://github.com/roboll/helmfile/issues/388#issuecomment-491710348 for more context
This commit is contained in:
		
							parent
							
								
									4f83e69bf6
								
							
						
					
					
						commit
						1db205de48
					
				|  | @ -100,10 +100,10 @@ Use Layering to extract the common parts into a dedicated *library helmfile*s, s | ||||||
| Let's assume that your `helmfile.yaml` looks like: | Let's assume that your `helmfile.yaml` looks like: | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| { readFile "commons.yaml" }} | bases: | ||||||
| --- | - commons.yaml | ||||||
| {{ readFile "environments.yaml" }} | - environments.yaml | ||||||
| --- | 
 | ||||||
| releases: | releases: | ||||||
| - name: myapp | - name: myapp | ||||||
|   chart: mychart |   chart: mychart | ||||||
|  |  | ||||||
							
								
								
									
										128
									
								
								pkg/app/app.go
								
								
								
								
							
							
						
						
									
										128
									
								
								pkg/app/app.go
								
								
								
								
							|  | @ -7,6 +7,7 @@ import ( | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"syscall" | ||||||
| 
 | 
 | ||||||
| 	"github.com/roboll/helmfile/helmexec" | 	"github.com/roboll/helmfile/helmexec" | ||||||
| 	"github.com/roboll/helmfile/state" | 	"github.com/roboll/helmfile/state" | ||||||
|  | @ -14,7 +15,6 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"syscall" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type App struct { | type App struct { | ||||||
|  | @ -111,34 +111,42 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) { | ||||||
|  | 	ld := &desiredStateLoader{ | ||||||
|  | 		readFile:  a.readFile, | ||||||
|  | 		env:       a.Env, | ||||||
|  | 		namespace: a.Namespace, | ||||||
|  | 		logger:    a.Logger, | ||||||
|  | 		abs:       a.abs, | ||||||
|  | 
 | ||||||
|  | 		Reverse:     a.Reverse, | ||||||
|  | 		KubeContext: a.KubeContext, | ||||||
|  | 		glob:        a.glob, | ||||||
|  | 	} | ||||||
|  | 	return ld.Load(file) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { | func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { | ||||||
| 	noMatchInHelmfiles := true | 	noMatchInHelmfiles := true | ||||||
| 
 | 
 | ||||||
| 	err := a.visitStateFiles(fileOrDir, func(f string) error { | 	err := a.visitStateFiles(fileOrDir, func(f string) error { | ||||||
| 		content, err := a.readFile(f) | 		st, err := a.loadDesiredStateFromYaml(f) | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		// render template, in two runs
 |  | ||||||
| 		r := &twoPassRenderer{ |  | ||||||
| 			reader:    a.readFile, |  | ||||||
| 			env:       a.Env, |  | ||||||
| 			namespace: a.Namespace, |  | ||||||
| 			filename:  f, |  | ||||||
| 			logger:    a.Logger, |  | ||||||
| 			abs:       a.abs, |  | ||||||
| 		} |  | ||||||
| 		yamlBuf, err := r.renderTemplate(content) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("error during %s parsing: %v", f, err) |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		st, err := a.loadDesiredStateFromYaml( | 		sigs := make(chan os.Signal, 1) | ||||||
| 			yamlBuf.Bytes(), | 		signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | ||||||
| 			f, | 		go func() { | ||||||
| 			a.Namespace, | 			sig := <-sigs | ||||||
| 			a.Env, | 
 | ||||||
| 		) | 			errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)} | ||||||
|  | 			_ = context{a, st}.clean(errs) | ||||||
|  | 			// See http://tldp.org/LDP/abs/html/exitcodes.html
 | ||||||
|  | 			switch sig { | ||||||
|  | 			case syscall.SIGINT: | ||||||
|  | 				os.Exit(130) | ||||||
|  | 			case syscall.SIGTERM: | ||||||
|  | 				os.Exit(143) | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
| 
 | 
 | ||||||
| 		ctx := context{a, st} | 		ctx := context{a, st} | ||||||
| 
 | 
 | ||||||
|  | @ -313,78 +321,6 @@ func directoryExistsAt(path string) bool { | ||||||
| 	return err == nil && fileInfo.Mode().IsDir() | 	return err == nil && fileInfo.Mode().IsDir() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) { |  | ||||||
| 	c := state.NewCreator(a.Logger, a.readFile, a.abs) |  | ||||||
| 	st, err := c.CreateFromYaml(yaml, file, env) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	helmfiles := []state.SubHelmfileSpec{} |  | ||||||
| 	for _, hf := range st.Helmfiles { |  | ||||||
| 		globPattern := hf.Path |  | ||||||
| 		var absPathPattern string |  | ||||||
| 		if filepath.IsAbs(globPattern) { |  | ||||||
| 			absPathPattern = globPattern |  | ||||||
| 		} else { |  | ||||||
| 			absPathPattern = st.JoinBase(globPattern) |  | ||||||
| 		} |  | ||||||
| 		matches, err := a.glob(absPathPattern) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) |  | ||||||
| 		} |  | ||||||
| 		sort.Strings(matches) |  | ||||||
| 		for _, match := range matches { |  | ||||||
| 			newHelmfile := hf |  | ||||||
| 			newHelmfile.Path = match |  | ||||||
| 			helmfiles = append(helmfiles, newHelmfile) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 	} |  | ||||||
| 	st.Helmfiles = helmfiles |  | ||||||
| 
 |  | ||||||
| 	if a.Reverse { |  | ||||||
| 		rev := func(i, j int) bool { |  | ||||||
| 			return j < i |  | ||||||
| 		} |  | ||||||
| 		sort.Slice(st.Releases, rev) |  | ||||||
| 		sort.Slice(st.Helmfiles, rev) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if a.KubeContext != "" { |  | ||||||
| 		if st.HelmDefaults.KubeContext != "" { |  | ||||||
| 			log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.") |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 		st.HelmDefaults.KubeContext = a.KubeContext |  | ||||||
| 	} |  | ||||||
| 	if namespace != "" { |  | ||||||
| 		if st.Namespace != "" { |  | ||||||
| 			log.Printf("err: Cannot use option --namespace and set attribute namespace.") |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 		st.Namespace = namespace |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sigs := make(chan os.Signal, 1) |  | ||||||
| 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) |  | ||||||
| 	go func() { |  | ||||||
| 		sig := <-sigs |  | ||||||
| 
 |  | ||||||
| 		errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)} |  | ||||||
| 		_ = context{a, st}.clean(errs) |  | ||||||
| 		// See http://tldp.org/LDP/abs/html/exitcodes.html
 |  | ||||||
| 		switch sig { |  | ||||||
| 		case syscall.SIGINT: |  | ||||||
| 			os.Exit(130) |  | ||||||
| 		case syscall.SIGTERM: |  | ||||||
| 			os.Exit(143) |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	return st, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Error struct { | type Error struct { | ||||||
| 	msg string | 	msg string | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package app | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | @ -83,7 +82,14 @@ func (f *testFs) readFile(filename string) ([]byte, error) { | ||||||
| 	return []byte(str), nil | 	return []byte(str), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *testFs) glob(pattern string) ([]string, error) { | func (f *testFs) glob(relPattern string) ([]string, error) { | ||||||
|  | 	var pattern string | ||||||
|  | 	if relPattern[0] == '/' { | ||||||
|  | 		pattern = relPattern | ||||||
|  | 	} else { | ||||||
|  | 		pattern = filepath.Join(f.wd, relPattern) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	matches := []string{} | 	matches := []string{} | ||||||
| 	for name, _ := range f.files { | 	for name, _ := range f.files { | ||||||
| 		matched, err := filepath.Match(pattern, name) | 		matched, err := filepath.Match(pattern, name) | ||||||
|  | @ -95,7 +101,7 @@ func (f *testFs) glob(pattern string) ([]string, error) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if len(matches) == 0 { | 	if len(matches) == 0 { | ||||||
| 		return []string(nil), fmt.Errorf("no file matched: %s", pattern) | 		return []string(nil), fmt.Errorf("no file matched %s for files: %v", pattern, f.files) | ||||||
| 	} | 	} | ||||||
| 	return matches, nil | 	return matches, nil | ||||||
| } | } | ||||||
|  | @ -640,15 +646,98 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) { | ||||||
|   labels: |   labels: | ||||||
|     stage: post |     stage: post | ||||||
| `) | `) | ||||||
|  | 	readFile := func(filename string) ([]byte, error) { | ||||||
|  | 		if filename != yamlFile { | ||||||
|  | 			return nil, fmt.Errorf("unexpected filename: %s", filename) | ||||||
|  | 		} | ||||||
|  | 		return yamlContent, nil | ||||||
|  | 	} | ||||||
| 	app := &App{ | 	app := &App{ | ||||||
| 		readFile:    ioutil.ReadFile, | 		readFile:    readFile, | ||||||
| 		glob:        filepath.Glob, | 		glob:        filepath.Glob, | ||||||
| 		abs:         filepath.Abs, | 		abs:         filepath.Abs, | ||||||
| 		KubeContext: "default", | 		KubeContext: "default", | ||||||
|  | 		Env:         "default", | ||||||
| 		Logger:      helmexec.NewLogger(os.Stderr, "debug"), | 		Logger:      helmexec.NewLogger(os.Stderr, "debug"), | ||||||
| 	} | 	} | ||||||
| 	_, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", "default") | 	_, err := app.loadDesiredStateFromYaml(yamlFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Error("unexpected error") | 		t.Errorf("unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestLoadDesiredStateFromYaml_Bases(t *testing.T) { | ||||||
|  | 	yamlFile := "/path/to/yaml/file" | ||||||
|  | 	yamlContent := []byte(`bases: | ||||||
|  | - ../base.yaml | ||||||
|  | - ../base.gotmpl | ||||||
|  | 
 | ||||||
|  | {{ readFile "templates.yaml" }} | ||||||
|  | 
 | ||||||
|  | releases: | ||||||
|  | - name: myrelease1 | ||||||
|  |   chart: mychart1 | ||||||
|  |   labels: | ||||||
|  |     stage: pre | ||||||
|  |     foo: bar | ||||||
|  | - name: myrelease1 | ||||||
|  |   chart: mychart2 | ||||||
|  |   labels: | ||||||
|  |     stage: post | ||||||
|  |   <<: *default | ||||||
|  | `) | ||||||
|  | 	files := map[string][]byte{ | ||||||
|  | 		yamlFile: yamlContent, | ||||||
|  | 		"/path/to/base.yaml": []byte(`environments: | ||||||
|  |   default: | ||||||
|  |     values: | ||||||
|  |     - environments/default/1.yaml | ||||||
|  | `), | ||||||
|  | 		"/path/to/yaml/environments/default/1.yaml": []byte(`foo: FOO`), | ||||||
|  | 		"/path/to/base.gotmpl": []byte(`environments: | ||||||
|  |   default: | ||||||
|  |     values: | ||||||
|  |     - environments/default/2.yaml | ||||||
|  | 
 | ||||||
|  | helmDefaults: | ||||||
|  |   tillerNamespace: {{ .Environment.Values.tillerNs }} | ||||||
|  | `), | ||||||
|  | "/path/to/yaml/environments/default/2.yaml": []byte(`tillerNs: TILLER_NS`), | ||||||
|  | "/path/to/yaml/templates.yaml": []byte(`templates: | ||||||
|  |   default: &default | ||||||
|  |     missingFileHandler: Warn | ||||||
|  |     values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] | ||||||
|  | `), | ||||||
|  | 	} | ||||||
|  | 	readFile := func(filename string) ([]byte, error) { | ||||||
|  | 		content, ok := files[filename] | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, fmt.Errorf("unexpected filename: %s", filename) | ||||||
|  | 		} | ||||||
|  | 		return content, nil | ||||||
|  | 	} | ||||||
|  | 	app := &App{ | ||||||
|  | 		readFile:    readFile, | ||||||
|  | 		glob:        filepath.Glob, | ||||||
|  | 		abs:         filepath.Abs, | ||||||
|  | 		KubeContext: "default", | ||||||
|  | 		Env:         "default", | ||||||
|  | 		Logger:      helmexec.NewLogger(os.Stderr, "debug"), | ||||||
|  | 	} | ||||||
|  | 	st, err := app.loadDesiredStateFromYaml(yamlFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if st.HelmDefaults.TillerNamespace != "TILLER_NS" { | ||||||
|  | 		t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if *st.Releases[1].MissingFileHandler != "Warn" { | ||||||
|  | 		t.Errorf("unexpected releases[0].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if st.Releases[1].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" { | ||||||
|  | 		t.Errorf("unexpected releases[0].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0]) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -13,6 +13,10 @@ const ( | ||||||
| 	ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
 | 	ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func isExplicitSelectorInheritanceEnabled() bool { | func experimentalModeEnabled() bool { | ||||||
| 	return os.Getenv(ExperimentalEnvVar) == "true" || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit) | 	return os.Getenv(ExperimentalEnvVar) == "true" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func isExplicitSelectorInheritanceEnabled() bool { | ||||||
|  | 	return experimentalModeEnabled() || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,145 @@ | ||||||
|  | package app | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/imdario/mergo" | ||||||
|  | 	"github.com/roboll/helmfile/state" | ||||||
|  | 	"go.uber.org/zap" | ||||||
|  | 	"log" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"sort" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type desiredStateLoader struct { | ||||||
|  | 	KubeContext string | ||||||
|  | 	Reverse     bool | ||||||
|  | 
 | ||||||
|  | 	env       string | ||||||
|  | 	namespace string | ||||||
|  | 
 | ||||||
|  | 	readFile func(string) ([]byte, error) | ||||||
|  | 	abs      func(string) (string, error) | ||||||
|  | 	glob     func(string) ([]string, error) | ||||||
|  | 
 | ||||||
|  | 	logger *zap.SugaredLogger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) { | ||||||
|  | 	return ld.load(filepath.Dir(f), filepath.Base(f), true) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ld *desiredStateLoader) load(baseDir, file string, evaluateBases bool) (*state.HelmState, error) { | ||||||
|  | 	var f string | ||||||
|  | 	if filepath.IsAbs(file) { | ||||||
|  | 		f = file | ||||||
|  | 	} else { | ||||||
|  | 		f = filepath.Join(baseDir, file) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fileBytes, err := ld.readFile(f) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ext := filepath.Ext(f) | ||||||
|  | 
 | ||||||
|  | 	var yamlBytes []byte | ||||||
|  | 	if !experimentalModeEnabled() || ext == ".gotmpl" { | ||||||
|  | 		yamlBuf, err := ld.renderTemplateToYaml(baseDir, f, fileBytes) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("error during %s parsing: %v", f, err) | ||||||
|  | 		} | ||||||
|  | 		yamlBytes = yamlBuf.Bytes() | ||||||
|  | 	} else { | ||||||
|  | 		yamlBytes = fileBytes | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	self, err := ld.loadYaml( | ||||||
|  | 		yamlBytes, | ||||||
|  | 		baseDir, | ||||||
|  | 		file, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !evaluateBases { | ||||||
|  | 		return self, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	layers := []*state.HelmState{} | ||||||
|  | 	for _, b := range self.Bases { | ||||||
|  | 		base, err := ld.load(baseDir, b, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		layers = append(layers, base) | ||||||
|  | 	} | ||||||
|  | 	layers = append(layers, self) | ||||||
|  | 
 | ||||||
|  | 	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 (a *desiredStateLoader) loadYaml(yaml []byte, baseDir, file string) (*state.HelmState, error) { | ||||||
|  | 	c := state.NewCreator(a.logger, a.readFile, a.abs) | ||||||
|  | 	st, err := c.ParseAndLoadEnv(yaml, baseDir, file, a.env) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	helmfiles := []state.SubHelmfileSpec{} | ||||||
|  | 	for _, hf := range st.Helmfiles { | ||||||
|  | 		globPattern := hf.Path | ||||||
|  | 		var absPathPattern string | ||||||
|  | 		if filepath.IsAbs(globPattern) { | ||||||
|  | 			absPathPattern = globPattern | ||||||
|  | 		} else { | ||||||
|  | 			absPathPattern = st.JoinBase(globPattern) | ||||||
|  | 		} | ||||||
|  | 		matches, err := a.glob(absPathPattern) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) | ||||||
|  | 		} | ||||||
|  | 		sort.Strings(matches) | ||||||
|  | 		for _, match := range matches { | ||||||
|  | 			newHelmfile := hf | ||||||
|  | 			newHelmfile.Path = match | ||||||
|  | 			helmfiles = append(helmfiles, newHelmfile) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 	st.Helmfiles = helmfiles | ||||||
|  | 
 | ||||||
|  | 	if a.Reverse { | ||||||
|  | 		rev := func(i, j int) bool { | ||||||
|  | 			return j < i | ||||||
|  | 		} | ||||||
|  | 		sort.Slice(st.Releases, rev) | ||||||
|  | 		sort.Slice(st.Helmfiles, rev) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if a.KubeContext != "" { | ||||||
|  | 		if st.HelmDefaults.KubeContext != "" { | ||||||
|  | 			log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.") | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		st.HelmDefaults.KubeContext = a.KubeContext | ||||||
|  | 	} | ||||||
|  | 	if a.namespace != "" { | ||||||
|  | 		if st.Namespace != "" { | ||||||
|  | 			log.Printf("err: Cannot use option --namespace and set attribute namespace.") | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		st.Namespace = a.namespace | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return st, nil | ||||||
|  | } | ||||||
|  | @ -6,8 +6,6 @@ import ( | ||||||
| 	"github.com/roboll/helmfile/environment" | 	"github.com/roboll/helmfile/environment" | ||||||
| 	"github.com/roboll/helmfile/state" | 	"github.com/roboll/helmfile/state" | ||||||
| 	"github.com/roboll/helmfile/tmpl" | 	"github.com/roboll/helmfile/tmpl" | ||||||
| 	"go.uber.org/zap" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -20,39 +18,30 @@ func prependLineNumbers(text string) string { | ||||||
| 	return buf.String() | 	return buf.String() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type twoPassRenderer struct { | func (r *desiredStateLoader) renderEnvironment(baseDir, filename string, content []byte) environment.Environment { | ||||||
| 	reader    func(string) ([]byte, error) |  | ||||||
| 	env       string |  | ||||||
| 	namespace string |  | ||||||
| 	filename  string |  | ||||||
| 	logger    *zap.SugaredLogger |  | ||||||
| 	abs       func(string) (string, error) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environment { |  | ||||||
| 	firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)} | 	firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)} | ||||||
| 	tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace} | 	tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace} | ||||||
| 	firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), tmplData) | 	firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData) | ||||||
| 
 | 
 | ||||||
| 	// parse as much as we can, tolerate errors, this is a preparse
 | 	// parse as much as we can, tolerate errors, this is a preparse
 | ||||||
| 	yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content) | 	yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content) | ||||||
| 	if err != nil && r.logger != nil { | 	if err != nil && r.logger != nil { | ||||||
| 		r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content))) | 		r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content))) | ||||||
| 		if yamlBuf == nil { // we have a template syntax error, let the second parse report
 | 		if yamlBuf == nil { // we have a template syntax error, let the second parse report
 | ||||||
| 			r.logger.Debugf("template syntax error: %v", err) | 			r.logger.Debugf("template syntax error: %v", err) | ||||||
| 			return firstPassEnv | 			return firstPassEnv | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	c := state.NewCreator(r.logger, r.reader, r.abs) | 	c := state.NewCreator(r.logger, r.readFile, r.abs) | ||||||
| 	c.Strict = false | 	c.Strict = false | ||||||
| 	// create preliminary state, as we may have an environment. Tolerate errors.
 | 	// create preliminary state, as we may have an environment. Tolerate errors.
 | ||||||
| 	prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env) | 	prestate, err := c.ParseAndLoadEnv(yamlBuf.Bytes(), baseDir, filename, r.env) | ||||||
| 	if err != nil && r.logger != nil { | 	if err != nil && r.logger != nil { | ||||||
| 		switch err.(type) { | 		switch err.(type) { | ||||||
| 		case *state.StateLoadError: | 		case *state.StateLoadError: | ||||||
| 			r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err) | 			r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err) | ||||||
| 		} | 		} | ||||||
| 		r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String())) | 		r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String())) | ||||||
| 	} | 	} | ||||||
| 	if prestate != nil { | 	if prestate != nil { | ||||||
| 		firstPassEnv = prestate.Env | 		firstPassEnv = prestate.Env | ||||||
|  | @ -60,21 +49,21 @@ func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environm | ||||||
| 	return firstPassEnv | 	return firstPassEnv | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) { | func (r *desiredStateLoader) renderTemplateToYaml(baseDir, filename string, content []byte) (*bytes.Buffer, error) { | ||||||
| 	// try a first pass render. This will always succeed, but can produce a limited env
 | 	// try a first pass render. This will always succeed, but can produce a limited env
 | ||||||
| 	firstPassEnv := r.renderEnvironment(content) | 	firstPassEnv := r.renderEnvironment(baseDir, filename, content) | ||||||
| 
 | 
 | ||||||
| 	tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace} | 	tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace} | ||||||
| 	secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData) | 	secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData) | ||||||
| 	yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content) | 	yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if r.logger != nil { | 		if r.logger != nil { | ||||||
| 			r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content))) | 			r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", filename, prependLineNumbers(string(content))) | ||||||
| 		} | 		} | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if r.logger != nil { | 	if r.logger != nil { | ||||||
| 		r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String())) | 		r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String())) | ||||||
| 	} | 	} | ||||||
| 	return yamlBuf, nil | 	return yamlBuf, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,12 +12,11 @@ import ( | ||||||
| 	"gopkg.in/yaml.v2" | 	"gopkg.in/yaml.v2" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer { | func makeLoader(readFile func(string) ([]byte, error), env string) *desiredStateLoader { | ||||||
| 	return &twoPassRenderer{ | 	return &desiredStateLoader{ | ||||||
| 		reader:    readFile, | 		readFile:  readFile, | ||||||
| 		env:       env, | 		env:       env, | ||||||
| 		namespace: "namespace", | 		namespace: "namespace", | ||||||
| 		filename:  "", |  | ||||||
| 		logger:    helmexec.NewLogger(os.Stdout, "debug"), | 		logger:    helmexec.NewLogger(os.Stdout, "debug"), | ||||||
| 		abs:       filepath.Abs, | 		abs:       filepath.Abs, | ||||||
| 	} | 	} | ||||||
|  | @ -51,8 +50,8 @@ releases: | ||||||
| 		return []byte(""), nil | 		return []byte(""), nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	yamlBuf, err := r.renderTemplate(yamlContent) | 	yamlBuf, err := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("unexpected error: %v", err) | 		t.Errorf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -102,9 +101,9 @@ releases: | ||||||
| 		return defaultValuesYaml, nil | 		return defaultValuesYaml, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	// test the double rendering
 | 	// test the double rendering
 | ||||||
| 	yamlBuf, err := r.renderTemplate(yamlContent) | 	yamlBuf, err := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -151,9 +150,9 @@ releases: | ||||||
| 		return defaultValuesYaml, nil | 		return defaultValuesYaml, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	// test the double rendering
 | 	// test the double rendering
 | ||||||
| 	_, err := r.renderTemplate(yamlContent) | 	_, err := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 
 | 
 | ||||||
| 	if !strings.Contains(err.Error(), "stringTemplate:8") { | 	if !strings.Contains(err.Error(), "stringTemplate:8") { | ||||||
| 		t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err) | 		t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err) | ||||||
|  | @ -190,8 +189,8 @@ releases: | ||||||
| 		return defaultValuesYamlGotmpl, nil | 		return defaultValuesYamlGotmpl, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	rendered, _ := r.renderTemplate(yamlContent) | 	rendered, _ := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 
 | 
 | ||||||
| 	var state state.HelmState | 	var state state.HelmState | ||||||
| 	yaml.Unmarshal(rendered.Bytes(), &state) | 	yaml.Unmarshal(rendered.Bytes(), &state) | ||||||
|  | @ -217,8 +216,8 @@ func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) { | ||||||
| 		return defaultValuesYaml, nil | 		return defaultValuesYaml, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	yamlBuf, err := r.renderTemplate(yamlContent) | 	yamlBuf, err := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -231,7 +230,7 @@ func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestReadFromYaml_HelfileShouldBeResilentToTemplateErrors(t *testing.T) { | func TestReadFromYaml_HelmfileShouldBeResilentToTemplateErrors(t *testing.T) { | ||||||
| 	yamlContent := []byte(`environments: | 	yamlContent := []byte(`environments: | ||||||
|   staging: |   staging: | ||||||
| 	production: | 	production: | ||||||
|  | @ -248,8 +247,8 @@ releases: | ||||||
| 		return yamlContent, nil | 		return yamlContent, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := makeRenderer(fileReader, "staging") | 	r := makeLoader(fileReader, "staging") | ||||||
| 	_, err := r.renderTemplate(yamlContent) | 	_, err := r.renderTemplateToYaml("", "", yamlContent) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Fatalf("wanted error, none returned") | 		t.Fatalf("wanted error, none returned") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
|  | @ -33,16 +32,6 @@ func (e *UndefinedEnvError) Error() string { | ||||||
| 	return e.msg | 	return e.msg | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) { |  | ||||||
| 	c := &creator{ |  | ||||||
| 		logger, |  | ||||||
| 		ioutil.ReadFile, |  | ||||||
| 		filepath.Abs, |  | ||||||
| 		true, |  | ||||||
| 	} |  | ||||||
| 	return c.CreateFromYaml(content, file, env) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type creator struct { | type creator struct { | ||||||
| 	logger   *zap.SugaredLogger | 	logger   *zap.SugaredLogger | ||||||
| 	readFile func(string) ([]byte, error) | 	readFile func(string) ([]byte, error) | ||||||
|  | @ -60,15 +49,12 @@ func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *creator) CreateFromYaml(content []byte, file string, env string) (*HelmState, error) { | // Parses YAML into HelmState, while loading environment values files relative to the `cwd`
 | ||||||
|  | func (c *creator) ParseAndLoadEnv(content []byte, baseDir, file string, env string) (*HelmState, error) { | ||||||
| 	var state HelmState | 	var state HelmState | ||||||
| 
 | 
 | ||||||
| 	basePath, err := c.abs(filepath.Dir(file)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, &StateLoadError{fmt.Sprintf("failed to read %s", file), err} |  | ||||||
| 	} |  | ||||||
| 	state.FilePath = file | 	state.FilePath = file | ||||||
| 	state.basePath = basePath | 	state.basePath = baseDir | ||||||
| 
 | 
 | ||||||
| 	decoder := yaml.NewDecoder(bytes.NewReader(content)) | 	decoder := yaml.NewDecoder(bytes.NewReader(content)) | ||||||
| 	if !c.Strict { | 	if !c.Strict { | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ package state | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"go.uber.org/zap" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | @ -10,6 +12,16 @@ import ( | ||||||
| 	"gotest.tools/assert/cmp" | 	"gotest.tools/assert/cmp" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) { | ||||||
|  | 	c := &creator{ | ||||||
|  | 		logger, | ||||||
|  | 		ioutil.ReadFile, | ||||||
|  | 		filepath.Abs, | ||||||
|  | 		true, | ||||||
|  | 	} | ||||||
|  | 	return c.ParseAndLoadEnv(content, filepath.Dir(file), file, env) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestReadFromYaml(t *testing.T) { | func TestReadFromYaml(t *testing.T) { | ||||||
| 	yamlFile := "example/path/to/yaml/file" | 	yamlFile := "example/path/to/yaml/file" | ||||||
| 	yamlContent := []byte(`releases: | 	yamlContent := []byte(`releases: | ||||||
|  | @ -101,7 +113,7 @@ bar: {{ readFile "bar.txt" }} | ||||||
| 		return nil, fmt.Errorf("unexpected filename: %s", filename) | 		return nil, fmt.Errorf("unexpected filename: %s", filename) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	state, err := NewCreator(logger, readFile, filepath.Abs).CreateFromYaml(yamlContent, yamlFile, "production") | 	state, err := NewCreator(logger, readFile, filepath.Abs).ParseAndLoadEnv(yamlContent, filepath.Dir(yamlFile), yamlFile, "production") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("unexpected error: %v", err) | 		t.Errorf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -27,9 +27,11 @@ import ( | ||||||
| 
 | 
 | ||||||
| // HelmState structure for the helmfile
 | // HelmState structure for the helmfile
 | ||||||
| type HelmState struct { | type HelmState struct { | ||||||
| 	basePath           string | 	basePath     string | ||||||
| 	Environments       map[string]EnvironmentSpec | 	Environments map[string]EnvironmentSpec | ||||||
| 	FilePath           string | 	FilePath     string | ||||||
|  | 
 | ||||||
|  | 	Bases              []string          `yaml:"bases"` | ||||||
| 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | ||||||
| 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles"` | 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles"` | ||||||
| 	DeprecatedContext  string            `yaml:"context"` | 	DeprecatedContext  string            `yaml:"context"` | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue