Fixes #344 by allowing explicit selectors to be specified for composed helmfiles using the following structure ```yaml helmfiles: - path: helmfile.d/a*.yaml selectors: - name=prometheus - name!=zipkin - helmfile.d/b*.yaml - path: helmfile.d/c*.yaml selectors: {} ``` 2 modes here : * legacy mode when no the env var HELMFILE_EXPERIMENTAL is not set to true * no selector : inherit from the command line. * selector: is specified then it is used (an emty means no inheritance from command line and take everything). * experimental when the env var HELMFILE_EXPERIMENTAL=true * no selector : nothing is inherited from the command line so use all releases. * selector: is specified then it is used (an emty means no inheritance from command line and take everything).
This commit is contained in:
		
							parent
							
								
									d2353b5b70
								
							
						
					
					
						commit
						4581e004b8
					
				
							
								
								
									
										29
									
								
								README.md
								
								
								
								
							
							
						
						
									
										29
									
								
								README.md
								
								
								
								
							|  | @ -332,7 +332,7 @@ The `selector` parameter can be specified multiple times. Each parameter is reso | ||||||
| 
 | 
 | ||||||
| `--selector tier=frontend --selector tier=backend` will select all the charts | `--selector tier=frontend --selector tier=backend` will select all the charts | ||||||
| 
 | 
 | ||||||
| In addition to user supplied labels the name, namespace, and chart are available to be used as selectors.  The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`). | In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors.  The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`). | ||||||
| 
 | 
 | ||||||
| ## Templates | ## Templates | ||||||
| 
 | 
 | ||||||
|  | @ -643,6 +643,27 @@ Just run `helmfile sync` inside `myteam/`, and you are done. | ||||||
| 
 | 
 | ||||||
| All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too. | All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too. | ||||||
| 
 | 
 | ||||||
|  | #### selectors | ||||||
|  | When composing helmfiles you can use selectors from the command line as well as explicit selectors inside the parent helmfile to filter the releases to be used. | ||||||
|  | ```yaml | ||||||
|  | helmfiles: | ||||||
|  | - apps/*/helmfile.yaml | ||||||
|  | - apps/a-helmfile.yaml: | ||||||
|  |     selectors:          # list of selectors | ||||||
|  |     - name=prometheus       | ||||||
|  |     - tier=frontend     | ||||||
|  | - apps/b-helmfile.yaml: # no selector, so all releases are used | ||||||
|  |     selectors: {} | ||||||
|  | - apps/c-helmfile.yaml: # parent selector to be used or cli selector for the initial helmfile | ||||||
|  |     selectors: inherits | ||||||
|  | ``` | ||||||
|  | * When a selector is specified, only this selector applies and the parents or CLI selectors are ignored. | ||||||
|  | * When not selector is specified there are 2 modes for the selector inheritance because we would like to change the current inheritance behavior (see [issue #344](https://github.com/roboll/helmfile/issues/344)  ). | ||||||
|  |   * Legacy mode, sub-helmfiles without selectors inherit selectors from their parent helmfile. The initial helmfiles inherit from the command line selectors.  | ||||||
|  |   * explicit mode, sub-helmfile without selectors do not inherit from their parent or the CLI selector. If you want them to inherit from their parent selector then use `selectors: inherits`. To enable this explicit mode you need to set the following environment variable `HELMFILE_EXPERIMENTAL=explicit-selector-inheritance` (see [experimental](#experimental-features)). | ||||||
|  | * Using `selector: {}` will for all releases to be used regardless of the parent selector or cli for the initial helmfile | ||||||
|  | * using `selector: inherits` make the sub-helmfile selects release with the parent selector or the cli for the initial helmfile | ||||||
|  | 
 | ||||||
| ## Importing values from any source | ## Importing values from any source | ||||||
| 
 | 
 | ||||||
| The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source | The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source | ||||||
|  | @ -805,6 +826,12 @@ Once you download all required charts into your machine, you can run `helmfile c | ||||||
| It basically run only `helm upgrade --install` with your already-downloaded charts, hence no Internet connection is required. | It basically run only `helm upgrade --install` with your already-downloaded charts, hence no Internet connection is required. | ||||||
| See #155 for more information on this topic. | See #155 for more information on this topic. | ||||||
| 
 | 
 | ||||||
|  | ## Experimental features | ||||||
|  | Some experimental features may be available for testing in perspective of being (or not) included in a future release. | ||||||
|  | Those features are set using the environment variable `HELMFILE_EXPERIMENTAL`. Here is the current experimental feature : | ||||||
|  | * `explicit-selector-inheritance` : remove today implicit cli selectors inheritance for composed helmfiles, see [composition selector](#selectors) | ||||||
|  | 
 | ||||||
|  | If you want to enable all experimental features set the env var to `HELMFILE_EXPERIMENTAL=true` | ||||||
| 
 | 
 | ||||||
| ## Examples | ## Examples | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helme | ||||||
| 		return converge(st, helm, ctx) | 		return converge(st, helm, ctx) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = a.VisitDesiredStates(fileOrDir, convergeWithHelmBinary) | 	err = a.VisitDesiredStates(fileOrDir, a.Selectors, convergeWithHelmBinary) | ||||||
| 
 | 
 | ||||||
| 	return toCliError(err) | 	return toCliError(err) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							|  | @ -6,10 +6,12 @@ require ( | ||||||
| 	github.com/Masterminds/semver v1.4.1 // indirect | 	github.com/Masterminds/semver v1.4.1 // indirect | ||||||
| 	github.com/Masterminds/sprig v2.15.0+incompatible | 	github.com/Masterminds/sprig v2.15.0+incompatible | ||||||
| 	github.com/aokoli/goutils v1.0.1 // indirect | 	github.com/aokoli/goutils v1.0.1 // indirect | ||||||
|  | 	github.com/google/go-cmp v0.3.0 // indirect | ||||||
| 	github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect | 	github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect | ||||||
| 	github.com/huandu/xstrings v1.0.0 // indirect | 	github.com/huandu/xstrings v1.0.0 // indirect | ||||||
| 	github.com/imdario/mergo v0.3.6 | 	github.com/imdario/mergo v0.3.6 | ||||||
| 	github.com/mattn/go-runewidth v0.0.4 // indirect | 	github.com/mattn/go-runewidth v0.0.4 // indirect | ||||||
|  | 	github.com/pkg/errors v0.8.1 // indirect | ||||||
| 	github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 | 	github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 | ||||||
| 	github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 | 	github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 | ||||||
| 	go.uber.org/atomic v1.3.2 // indirect | 	go.uber.org/atomic v1.3.2 // indirect | ||||||
|  | @ -17,4 +19,5 @@ require ( | ||||||
| 	go.uber.org/zap v1.8.0 | 	go.uber.org/zap v1.8.0 | ||||||
| 	golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect | 	golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect | ||||||
| 	gopkg.in/yaml.v2 v2.2.1 | 	gopkg.in/yaml.v2 v2.2.1 | ||||||
|  | 	gotest.tools v2.2.0+incompatible | ||||||
| ) | ) | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										6
									
								
								go.sum
								
								
								
								
							|  | @ -4,6 +4,8 @@ github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRD | ||||||
| github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= | github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= | ||||||
| github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= | github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= | ||||||
| github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= | github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= | ||||||
|  | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= | ||||||
|  | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
| github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c= | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c= | ||||||
| github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk= | github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk= | ||||||
|  | @ -12,6 +14,8 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= | ||||||
| github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= | ||||||
| github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= | ||||||
| github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | ||||||
|  | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | ||||||
|  | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 h1:BhIUXV2ySTLrKgh/Hnts+QTQlIbWtomXt3LMdzME0A0= | github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 h1:BhIUXV2ySTLrKgh/Hnts+QTQlIbWtomXt3LMdzME0A0= | ||||||
| github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939/go.mod h1:omGxs4/6hNjxPKUTjmaNkPzehSnNJOJN6pMEbrlYIT4= | github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939/go.mod h1:omGxs4/6hNjxPKUTjmaNkPzehSnNJOJN6pMEbrlYIT4= | ||||||
| github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 h1:Yf7/DcGM+61oLBGXQV2Q+ztEGBcOe3EUnbKSOn4fQuE= | github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 h1:Yf7/DcGM+61oLBGXQV2Q+ztEGBcOe3EUnbKSOn4fQuE= | ||||||
|  | @ -27,3 +31,5 @@ golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc/go.mod h1:6SG95UA2DQfeDnf | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= | ||||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
|  | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= | ||||||
|  | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= | ||||||
|  |  | ||||||
|  | @ -108,7 +108,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) VisitDesiredStates(fileOrDir 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 { | ||||||
|  | @ -153,16 +153,21 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		st.Selectors = selector | ||||||
| 
 | 
 | ||||||
| 		if len(st.Helmfiles) > 0 { | 		if len(st.Helmfiles) > 0 { | ||||||
| 			noMatchInSubHelmfiles := true | 			noMatchInSubHelmfiles := true | ||||||
| 			for _, m := range st.Helmfiles { | 			for _, m := range st.Helmfiles { | ||||||
| 				if err := a.VisitDesiredStates(m, converge); err != nil { | 				//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
 | ||||||
|  | 				if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.Inherits { | ||||||
|  | 					m.Selectors = selector | ||||||
|  | 				} | ||||||
|  | 				if err := a.VisitDesiredStates(m.Path, m.Selectors, converge); err != nil { | ||||||
| 					switch err.(type) { | 					switch err.(type) { | ||||||
| 					case *NoMatchingHelmfileError: | 					case *NoMatchingHelmfileError: | ||||||
| 
 | 
 | ||||||
| 					default: | 					default: | ||||||
| 						return fmt.Errorf("failed processing %s: %v", m, err) | 						return fmt.Errorf("failed processing %s: %v", m.Path, err) | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					noMatchInSubHelmfiles = false | 					noMatchInSubHelmfiles = false | ||||||
|  | @ -192,11 +197,10 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { | func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { | ||||||
| 	selectors := a.Selectors |  | ||||||
| 
 | 
 | ||||||
| 	err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | 	err := a.VisitDesiredStates(fileOrDir, a.Selectors, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | ||||||
| 		if len(selectors) > 0 { | 		if len(st.Selectors) > 0 { | ||||||
| 			err := st.FilterReleases(selectors) | 			err := st.FilterReleases() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return false, []error{err} | 				return false, []error{err} | ||||||
| 			} | 			} | ||||||
|  | @ -311,8 +315,9 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	helmfiles := []string{} | 	helmfiles := []state.SubHelmfileSpec{} | ||||||
| 	for _, globPattern := range st.Helmfiles { | 	for _, hf := range st.Helmfiles { | ||||||
|  | 		globPattern := hf.Path | ||||||
| 		var absPathPattern string | 		var absPathPattern string | ||||||
| 		if filepath.IsAbs(globPattern) { | 		if filepath.IsAbs(globPattern) { | ||||||
| 			absPathPattern = globPattern | 			absPathPattern = globPattern | ||||||
|  | @ -324,8 +329,12 @@ func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace strin | ||||||
| 			return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) | 			return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) | ||||||
| 		} | 		} | ||||||
| 		sort.Strings(matches) | 		sort.Strings(matches) | ||||||
|  | 		for _, match := range matches { | ||||||
|  | 			newHelmfile := hf | ||||||
|  | 			newHelmfile.Path = match | ||||||
|  | 			helmfiles = append(helmfiles, newHelmfile) | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		helmfiles = append(helmfiles, matches...) |  | ||||||
| 	} | 	} | ||||||
| 	st.Helmfiles = helmfiles | 	st.Helmfiles = helmfiles | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/roboll/helmfile/helmexec" | 	"github.com/roboll/helmfile/helmexec" | ||||||
| 	"github.com/roboll/helmfile/state" | 	"github.com/roboll/helmfile/state" | ||||||
|  | 	"gotest.tools/env" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | @ -324,6 +325,242 @@ releases: | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestVisitDesiredStatesWithReleasesFiltered_EmbeddedSelectors(t *testing.T) { | ||||||
|  | 	files := map[string]string{ | ||||||
|  | 		"/path/to/helmfile.yaml": ` | ||||||
|  | helmfiles: | ||||||
|  | - helmfile.d/a*.yaml: | ||||||
|  |     selectors: | ||||||
|  |     - name=prometheus       | ||||||
|  |     - name=zipkin       | ||||||
|  | - helmfile.d/b*.yaml | ||||||
|  | - helmfile.d/c*.yaml: | ||||||
|  |     selectors: {} | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/a1.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: zipkin | ||||||
|  |   chart: stable/zipkin | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/a2.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: prometheus | ||||||
|  |   chart: stable/prometheus | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/a3.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: mongodb | ||||||
|  |   chart: stable/mongodb | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/b.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: grafana | ||||||
|  |   chart: stable/grafana | ||||||
|  | - name: bar | ||||||
|  |   chart: charts/foo | ||||||
|  |   tillerNamespace:  bar1 | ||||||
|  |   labels: | ||||||
|  |     duplicatedOK: yes | ||||||
|  | - name: bar | ||||||
|  |   chart: charts/foo | ||||||
|  |   tillerNamespace: bar2 | ||||||
|  |   labels: | ||||||
|  |     duplicatedOK: yes | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/c.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: grafana | ||||||
|  |   chart: stable/grafana | ||||||
|  | - name: postgresql | ||||||
|  |   chart: charts/postgresql | ||||||
|  |   labels: | ||||||
|  |     whatever: yes | ||||||
|  | `, | ||||||
|  | 	} | ||||||
|  | 	//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
 | ||||||
|  | 	legacyTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "bar", "bar", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 		{label: "name=zipkin", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 		{label: "name=grafana", expectedReleases: []string{"zipkin", "prometheus", "grafana", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 		{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 	runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st EmbeddedSelectors") | ||||||
|  | 
 | ||||||
|  | 	//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
 | ||||||
|  | 	desiredTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 		{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)() | ||||||
|  | 
 | ||||||
|  | 	runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd EmbeddedSelectors") | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_3leveldeep(t *testing.T) { | ||||||
|  | 	files := map[string]string{ | ||||||
|  | 		"/path/to/helmfile.yaml": ` | ||||||
|  | helmfiles: | ||||||
|  | - helmfile.d/a*.yaml | ||||||
|  | releases: | ||||||
|  | - name: mongodb | ||||||
|  |   chart: stable/mongodb | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/a.yaml": ` | ||||||
|  | helmfiles: | ||||||
|  | - b*.yaml | ||||||
|  | releases: | ||||||
|  | - name: zipkin | ||||||
|  |   chart: stable/zipkin | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/b.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: grafana | ||||||
|  |   chart: stable/grafana | ||||||
|  | `, | ||||||
|  | 	} | ||||||
|  | 	//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
 | ||||||
|  | 	legacyTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "name!=grafana", expectedReleases: []string{"zipkin", "mongodb"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 	runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st 3leveldeep") | ||||||
|  | 
 | ||||||
|  | 	//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
 | ||||||
|  | 	desiredTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "name!=grafana", expectedReleases: []string{"grafana", "zipkin", "mongodb"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)() | ||||||
|  | 
 | ||||||
|  | 	runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd 3leveldeep") | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_inherits(t *testing.T) { | ||||||
|  | 	files := map[string]string{ | ||||||
|  | 		"/path/to/helmfile.yaml": ` | ||||||
|  | helmfiles: | ||||||
|  | - helmfile.d/a*.yaml | ||||||
|  | - helmfile.d/a*.yaml: | ||||||
|  |     selectors: | ||||||
|  |     - select=foo | ||||||
|  | releases: | ||||||
|  | - name: mongodb | ||||||
|  |   chart: stable/mongodb | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/a.yaml": ` | ||||||
|  | helmfiles: | ||||||
|  | - b*.yaml: | ||||||
|  |     selectors: inherits | ||||||
|  | releases: | ||||||
|  | - name: zipkin | ||||||
|  |   chart: stable/zipkin | ||||||
|  |   labels: | ||||||
|  |     select: foo | ||||||
|  | `, | ||||||
|  | 		"/path/to/helmfile.d/b.yaml": ` | ||||||
|  | releases: | ||||||
|  | - name: grafana | ||||||
|  |   chart: stable/grafana | ||||||
|  | - name: prometheus | ||||||
|  |   chart: stable/prometheus | ||||||
|  |   labels: | ||||||
|  |     select: foo | ||||||
|  | `, | ||||||
|  | 	} | ||||||
|  | 	//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
 | ||||||
|  | 	legacyTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin"}, expectErr: false}, | ||||||
|  | 		{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "mongodb"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 	runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st inherits") | ||||||
|  | 
 | ||||||
|  | 	//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
 | ||||||
|  | 	desiredTestcases := []struct { | ||||||
|  | 		label            string | ||||||
|  | 		expectedReleases []string | ||||||
|  | 		expectErr        bool | ||||||
|  | 		errMsg           string | ||||||
|  | 	}{ | ||||||
|  | 		{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin"}, expectErr: false}, | ||||||
|  | 		{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin", "mongodb"}, expectErr: false}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)() | ||||||
|  | 
 | ||||||
|  | 	runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd inherits") | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func runFilterSubHelmFilesTests(testcases []struct { | ||||||
|  | 	label            string | ||||||
|  | 	expectedReleases []string | ||||||
|  | 	expectErr        bool | ||||||
|  | 	errMsg           string | ||||||
|  | }, files map[string]string, t *testing.T, testName string) { | ||||||
|  | 	for _, testcase := range testcases { | ||||||
|  | 		actual := []string{} | ||||||
|  | 
 | ||||||
|  | 		collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error { | ||||||
|  | 			for _, r := range st.Releases { | ||||||
|  | 				actual = append(actual, r.Name) | ||||||
|  | 			} | ||||||
|  | 			return []error{} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		app := appWithFs(&App{ | ||||||
|  | 			KubeContext: "default", | ||||||
|  | 			Logger:      helmexec.NewLogger(os.Stderr, "debug"), | ||||||
|  | 			Namespace:   "", | ||||||
|  | 			Selectors:   []string{testcase.label}, | ||||||
|  | 			Env:         "default", | ||||||
|  | 		}, files) | ||||||
|  | 
 | ||||||
|  | 		err := app.VisitDesiredStatesWithReleasesFiltered( | ||||||
|  | 			"helmfile.yaml", collectReleases, | ||||||
|  | 		) | ||||||
|  | 		if testcase.expectErr { | ||||||
|  | 			if err == nil { | ||||||
|  | 				t.Errorf("[%s]error expected but not happened for selector %s", testName, testcase.label) | ||||||
|  | 			} else if err.Error() != testcase.errMsg { | ||||||
|  | 				t.Errorf("[%s]unexpected error message: expected=\"%s\", actual=\"%s\"", testName, testcase.errMsg, err.Error()) | ||||||
|  | 			} | ||||||
|  | 		} else if !testcase.expectErr && err != nil { | ||||||
|  | 			t.Errorf("[%s]unexpected error for selector %s: %v", testName, testcase.label, err) | ||||||
|  | 		} | ||||||
|  | 		if !reflect.DeepEqual(actual, testcase.expectedReleases) { | ||||||
|  | 			t.Errorf("[%s]unexpected releases for selector %s: expected=%v, actual=%v", testName, testcase.label, testcase.expectedReleases, actual) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // See https://github.com/roboll/helmfile/issues/312
 | // See https://github.com/roboll/helmfile/issues/312
 | ||||||
| func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) { | func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) { | ||||||
| 	files := map[string]string{ | 	files := map[string]string{ | ||||||
|  |  | ||||||
|  | @ -1,7 +1,18 @@ | ||||||
| package app | package app | ||||||
| 
 | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| const ( | const ( | ||||||
| 	DefaultHelmfile              = "helmfile.yaml" | 	DefaultHelmfile              = "helmfile.yaml" | ||||||
| 	DeprecatedHelmfile           = "charts.yaml" | 	DeprecatedHelmfile           = "charts.yaml" | ||||||
| 	DefaultHelmfileDirectory     = "helmfile.d" | 	DefaultHelmfileDirectory     = "helmfile.d" | ||||||
|  | 	ExperimentalEnvVar           = "HELMFILE_EXPERIMENTAL"         // environment variable for experimental features, expecting "true" lower case
 | ||||||
|  | 	ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
 | ||||||
| ) | ) | ||||||
|  | 
 | ||||||
|  | func isExplicitSelectorInheritanceEnabled() bool { | ||||||
|  | 	return os.Getenv(ExperimentalEnvVar) == "true" || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | package app | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	. "gotest.tools/assert" | ||||||
|  | 	is "gotest.tools/assert/cmp" | ||||||
|  | 	"gotest.tools/env" | ||||||
|  | 	"os" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) { | ||||||
|  | 	//env var ExperimentalEnvVar is set
 | ||||||
|  | 	Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), "")) | ||||||
|  | 	Check(t, !isExplicitSelectorInheritanceEnabled()) | ||||||
|  | 
 | ||||||
|  | 	//check for env var ExperimentalEnvVar set to true
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, "true")() | ||||||
|  | 	Check(t, isExplicitSelectorInheritanceEnabled()) | ||||||
|  | 
 | ||||||
|  | 	//check for env var ExperimentalEnvVar set to anything
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, "foo")() | ||||||
|  | 	Check(t, !isExplicitSelectorInheritanceEnabled()) | ||||||
|  | 
 | ||||||
|  | 	//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
 | ||||||
|  | 	defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)() | ||||||
|  | 	Check(t, isExplicitSelectorInheritanceEnabled()) | ||||||
|  | } | ||||||
|  | @ -5,6 +5,8 @@ import ( | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	. "gotest.tools/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestReadFromYaml(t *testing.T) { | func TestReadFromYaml(t *testing.T) { | ||||||
|  | @ -247,3 +249,112 @@ func TestReadFromYaml_FilterNegatives(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestReadFromYaml_Helmfiles_Selectors(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		path      string | ||||||
|  | 		content   []byte | ||||||
|  | 		wantErr   bool | ||||||
|  | 		helmfiles []SubHelmfileSpec | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			path: "working/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - simple/helmfile.yaml | ||||||
|  | - simple/helmfile/with/semicolon.yaml: | ||||||
|  | - two/selectors.yaml: | ||||||
|  |     selectors: | ||||||
|  |       - name=foo | ||||||
|  |       - name=bar | ||||||
|  | - empty/selector.yaml: | ||||||
|  |     selectors: {} | ||||||
|  | - inherits/selector.yaml: | ||||||
|  |     selectors: inherits | ||||||
|  | - path: path/prefix/selector.yaml | ||||||
|  |   selectors: | ||||||
|  |     - name=zorba | ||||||
|  | - path: path/prefix/empty/selector.yaml | ||||||
|  |   selectors: {} | ||||||
|  | - path: path/prefix/inherits/selector.yaml | ||||||
|  |   selectors: inherits | ||||||
|  | `), | ||||||
|  | 			wantErr: false, | ||||||
|  | 			helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml"}, | ||||||
|  | 				{Path: "simple/helmfile/with/semicolon.yaml", Selectors: nil, Inherits: false}, | ||||||
|  | 				{Path: "two/selectors.yaml", Selectors: []string{"name=foo", "name=bar"}, Inherits: false}, | ||||||
|  | 				{Path: "empty/selector.yaml", Selectors: []string{}, Inherits: false}, | ||||||
|  | 				{Path: "inherits/selector.yaml", Selectors: nil, Inherits: true}, | ||||||
|  | 				{Path: "path/prefix/selector.yaml", Selectors: []string{"name=zorba"}, Inherits: false}, | ||||||
|  | 				{Path: "path/prefix/empty/selector.yaml", Selectors: []string{}, Inherits: false}, | ||||||
|  | 				{Path: "path/prefix/inherits/selector.yaml", Selectors: nil, Inherits: true}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing1/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - failing1/helmfile.yaml: foo | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing2/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - failing2/helmfile.yaml:  | ||||||
|  |     wrongkey: | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing3/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - failing3/helmfile.yaml:  | ||||||
|  |     selectors: foo | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing4/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - failing4/helmfile.yaml:  | ||||||
|  |     selectors: | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing4/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - failing4/helmfile.yaml:  | ||||||
|  |     selectors: | ||||||
|  |       - colon: not-authorized | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing5/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - selectors: | ||||||
|  |     - colon: not-authorized | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			path: "failing6/selector", | ||||||
|  | 			content: []byte(`helmfiles: | ||||||
|  | - selectors: | ||||||
|  |     - whatever | ||||||
|  | `), | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, test := range tests { | ||||||
|  | 		st, err := createFromYaml(test.content, test.path, DefaultEnv, logger) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if test.wantErr { | ||||||
|  | 				continue | ||||||
|  | 			} else { | ||||||
|  | 				t.Error("unexpected error:", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		DeepEqual(t, st.Helmfiles, test.helmfiles) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										119
									
								
								state/state.go
								
								
								
								
							
							
						
						
									
										119
									
								
								state/state.go
								
								
								
								
							|  | @ -18,13 +18,14 @@ import ( | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
| 	"github.com/roboll/helmfile/environment" | 	"github.com/roboll/helmfile/environment" | ||||||
| 	"github.com/roboll/helmfile/event" | 	"github.com/roboll/helmfile/event" | ||||||
| 	"github.com/roboll/helmfile/tmpl" | 	"github.com/roboll/helmfile/tmpl" | ||||||
| 	"github.com/tatsushid/go-prettytable" | 	"github.com/tatsushid/go-prettytable" | ||||||
| 	"go.uber.org/zap" | 	"go.uber.org/zap" | ||||||
| 	"gopkg.in/yaml.v2" | 	"gopkg.in/yaml.v2" | ||||||
| 	"net/url" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // HelmState structure for the helmfile
 | // HelmState structure for the helmfile
 | ||||||
|  | @ -33,12 +34,13 @@ type HelmState struct { | ||||||
| 	Environments       map[string]EnvironmentSpec | 	Environments       map[string]EnvironmentSpec | ||||||
| 	FilePath           string | 	FilePath           string | ||||||
| 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | 	HelmDefaults       HelmSpec          `yaml:"helmDefaults"` | ||||||
| 	Helmfiles          []string         `yaml:"helmfiles"` | 	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles"` | ||||||
| 	DeprecatedContext  string            `yaml:"context"` | 	DeprecatedContext  string            `yaml:"context"` | ||||||
| 	DeprecatedReleases []ReleaseSpec     `yaml:"charts"` | 	DeprecatedReleases []ReleaseSpec     `yaml:"charts"` | ||||||
| 	Namespace          string            `yaml:"namespace"` | 	Namespace          string            `yaml:"namespace"` | ||||||
| 	Repositories       []RepositorySpec  `yaml:"repositories"` | 	Repositories       []RepositorySpec  `yaml:"repositories"` | ||||||
| 	Releases           []ReleaseSpec     `yaml:"releases"` | 	Releases           []ReleaseSpec     `yaml:"releases"` | ||||||
|  | 	Selectors          []string | ||||||
| 
 | 
 | ||||||
| 	Templates map[string]TemplateSpec `yaml:"templates"` | 	Templates map[string]TemplateSpec `yaml:"templates"` | ||||||
| 
 | 
 | ||||||
|  | @ -54,6 +56,15 @@ type HelmState struct { | ||||||
| 	runner helmexec.Runner | 	runner helmexec.Runner | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SubHelmfileSpec defines the subhelmfile path and options
 | ||||||
|  | type SubHelmfileSpec struct { | ||||||
|  | 	Path      string   //path or glob pattern for the sub helmfiles
 | ||||||
|  | 	Selectors []string //chosen selectors for the sub helmfiles
 | ||||||
|  | 	Inherits  bool     //do the sub helmfiles inherits from parent selectors
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const InheritsYamlValue = "inherits" | ||||||
|  | 
 | ||||||
| // HelmSpec to defines helmDefault values
 | // HelmSpec to defines helmDefault values
 | ||||||
| type HelmSpec struct { | type HelmSpec struct { | ||||||
| 	KubeContext     string   `yaml:"kubeContext"` | 	KubeContext     string   `yaml:"kubeContext"` | ||||||
|  | @ -866,11 +877,11 @@ func (st *HelmState) Clean() []error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
 | // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
 | ||||||
| func (st *HelmState) FilterReleases(labels []string) error { | func (st *HelmState) FilterReleases() error { | ||||||
| 	var filteredReleases []ReleaseSpec | 	var filteredReleases []ReleaseSpec | ||||||
| 	releaseSet := map[string][]ReleaseSpec{} | 	releaseSet := map[string][]ReleaseSpec{} | ||||||
| 	filters := []ReleaseFilter{} | 	filters := []ReleaseFilter{} | ||||||
| 	for _, label := range labels { | 	for _, label := range st.Selectors { | ||||||
| 		f, err := ParseLabels(label) | 		f, err := ParseLabels(label) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | @ -902,7 +913,7 @@ func (st *HelmState) FilterReleases(labels []string) error { | ||||||
| 	} | 	} | ||||||
| 	st.Releases = filteredReleases | 	st.Releases = filteredReleases | ||||||
| 	numFound := len(filteredReleases) | 	numFound := len(filteredReleases) | ||||||
| 	st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(labels, ","), st.FilePath) | 	st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(st.Selectors, ","), st.FilePath) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1387,3 +1398,101 @@ func escape(value string) string { | ||||||
| 	intermediate = strings.Replace(intermediate, "}", "\\}", -1) | 	intermediate = strings.Replace(intermediate, "}", "\\}", -1) | ||||||
| 	return strings.Replace(intermediate, ",", "\\,", -1) | 	return strings.Replace(intermediate, ",", "\\,", -1) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | //UnmarshalYAML will unmarshal the helmfile yaml section and fill the SubHelmfileSpec structure
 | ||||||
|  | //this is required to keep allowing string scalar for defining helmfile (maybe)
 | ||||||
|  | func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||||
|  | 
 | ||||||
|  | 	var tmp interface{} | ||||||
|  | 	if err := unmarshal(&tmp); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch i := tmp.(type) { | ||||||
|  | 	case string: // single path definition without sub items
 | ||||||
|  | 		hf.Path = i | ||||||
|  | 	case map[interface{}]interface{}: // helmfile path with sub section
 | ||||||
|  | 		for k, v := range i { | ||||||
|  | 			switch key := k.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				//get the path
 | ||||||
|  | 				if key == "path" { | ||||||
|  | 					hf.Path = v.(string) | ||||||
|  | 				} else if key == "selectors" { | ||||||
|  | 					if err := extractSelectorContent(hf, v); err != nil { | ||||||
|  | 						return err | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					hf.Path = key | ||||||
|  | 					//get the selectors if something is specified
 | ||||||
|  | 					if v != nil { //we have a path, now compute the selector
 | ||||||
|  | 						if err := extractSelector(hf, v); err != nil { | ||||||
|  | 							return err | ||||||
|  | 						} | ||||||
|  | 					} //else it is a path with ending semi-colon without anything else below and it is fine
 | ||||||
|  | 				} | ||||||
|  | 			default: | ||||||
|  | 				return fmt.Errorf("Expecting a \"string\" scalar for the helmfile collection but got: %v", key) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	//since we cannot make sur the "console" string can be red after the "path" we must check we don't have
 | ||||||
|  | 	//a SubHelmfileSpec with only selector and no path
 | ||||||
|  | 	if hf.Selectors != nil && hf.Path == "" { | ||||||
|  | 		return fmt.Errorf("found 'selectors' definition without path: %v", hf.Selectors) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //extractSelector this will extract the selectors: from the helmfile section
 | ||||||
|  | //this has been developed to only expect selectors: under the helmfiles for now.
 | ||||||
|  | func extractSelector(hf *SubHelmfileSpec, value interface{}) error { | ||||||
|  | 	switch value := value.(type) { | ||||||
|  | 	case map[interface{}]interface{}: | ||||||
|  | 		for k, v := range value { | ||||||
|  | 			switch key := k.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				if key == "selectors" { | ||||||
|  | 					return extractSelectorContent(hf, v) | ||||||
|  | 				} else { //not 'selectors' so error
 | ||||||
|  | 					return fmt.Errorf("Expecting a \"selectors\" mapping but got: %v", key) | ||||||
|  | 				} | ||||||
|  | 			default: //we where expecting a selector but go something else
 | ||||||
|  | 				return fmt.Errorf("Expecting a \"selectors\" mapping for string [-%v] but got: %v of type %T", hf.Path, key, key) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("Expecting a \"selectors\" mapping for string [-%v] but got: %v of type %T", hf.Path, value, value) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func extractSelectorContent(hf *SubHelmfileSpec, value interface{}) error { | ||||||
|  | 	switch selectors := value.(type) { | ||||||
|  | 	case string: //check the string is `inherits` else error
 | ||||||
|  | 		if selectors == InheritsYamlValue { | ||||||
|  | 			hf.Inherits = true | ||||||
|  | 		} else { | ||||||
|  | 			return fmt.Errorf("Expecting a list of string, an empty {} or '%v' but got: %v", InheritsYamlValue, selectors) | ||||||
|  | 		} | ||||||
|  | 	case []interface{}: //expect an array if strings
 | ||||||
|  | 		for _, sel := range selectors { | ||||||
|  | 			switch selValue := sel.(type) { | ||||||
|  | 			case string: | ||||||
|  | 				hf.Selectors = append(hf.Selectors, selValue) | ||||||
|  | 			default: | ||||||
|  | 				return fmt.Errorf("Expecting a string, but got: %v", selValue) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	case map[interface{}]interface{}: | ||||||
|  | 		if len(selectors) == 0 { | ||||||
|  | 			hf.Selectors = make([]string, 0) //allocate and non nil empty array
 | ||||||
|  | 		} else { //unexpected unempty map so error
 | ||||||
|  | 			return fmt.Errorf("unexpected unempty map in selector [-%v] but got: %v", hf.Path, selectors) | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("Expecting list of strings or and empty {} mapping [-%v] but got: [%v] of type [%T] ", hf.Path, selectors, selectors) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1636,7 +1636,8 @@ func TestHelmState_NoReleaseMatched(t *testing.T) { | ||||||
| 				Releases: releases, | 				Releases: releases, | ||||||
| 				logger:   logger, | 				logger:   logger, | ||||||
| 			} | 			} | ||||||
| 			errs := state.FilterReleases([]string{tt.labels}) | 			state.Selectors = []string{tt.labels} | ||||||
|  | 			errs := state.FilterReleases() | ||||||
| 			if (errs != nil) != tt.wantErr { | 			if (errs != nil) != tt.wantErr { | ||||||
| 				t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) | 				t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) | ||||||
| 				return | 				return | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue