Skip to content

Commit

Permalink
feat(roboll#344): add inherits explicit selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
sgandon committed May 4, 2019
1 parent 88f1c64 commit 184d3dd
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 23 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Expand Down Expand Up @@ -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.

#### 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 `EXPERIMENTAL=explicit-selector-inheritance`.
* 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

The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source
Expand Down Expand Up @@ -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.
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 `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 `EXPERIMENTAL=true`

## Examples

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ require (
github.com/Masterminds/semver v1.4.1 // indirect
github.com/Masterminds/sprig v2.15.0+incompatible
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/huandu/xstrings v1.0.0 // indirect
github.com/imdario/mergo v0.3.6
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/urfave/cli v0.0.0-20160620154522-6011f165dc28
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.8.0
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect
gopkg.in/yaml.v2 v2.2.1
gotest.tools v2.2.0+incompatible
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk=
Expand All @@ -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/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/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/go.mod h1:omGxs4/6hNjxPKUTjmaNkPzehSnNJOJN6pMEbrlYIT4=
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 h1:Yf7/DcGM+61oLBGXQV2Q+ztEGBcOe3EUnbKSOn4fQuE=
Expand All @@ -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/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
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=
2 changes: 1 addition & 1 deletion pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles {
//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
if m.Selectors == nil && os.Getenv(ExperimentalEnvVar) != "true" {
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.Inherits {
m.Selectors = selector
}
if err := a.VisitDesiredStates(m.Path, m.Selectors, converge); err != nil {
Expand Down
121 changes: 116 additions & 5 deletions pkg/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"gotest.tools/env"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -376,7 +377,7 @@ releases:
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
Expand All @@ -388,8 +389,9 @@ releases:
{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 series")
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
Expand All @@ -400,10 +402,119 @@ releases:
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
}

os.Setenv(ExperimentalEnvVar, "true")
defer os.Unsetenv(ExperimentalEnvVar)
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 series")
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd inherits")

}

Expand Down
18 changes: 14 additions & 4 deletions pkg/app/constants.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package app

import (
"os"
"strings"
)

const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
ExperimentalEnvVar = "EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
ExperimentalEnvVar = "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)
}
27 changes: 27 additions & 0 deletions pkg/app/constants_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
84 changes: 84 additions & 0 deletions state/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package state

import (
"fmt"
. "gotest.tools/assert"
"path/filepath"
"reflect"
"testing"
Expand Down Expand Up @@ -247,3 +248,86 @@ 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
`),
wantErr: false,
helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml"},
{Path: "simple/helmfile/with/semicolon.yaml"},
{Path: "two/selectors.yaml", Selectors: []string{"name=foo", "name=bar"}},
{Path: "empty/selector.yaml", Selectors: []string{}},
{Path: "inherits/selector.yaml", 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:
selector: foo
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- failing4/helmfile.yaml:
selector:
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- failing4/helmfile.yaml:
selector:
- colon: not-authorized
`),
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)
}
}
Loading

0 comments on commit 184d3dd

Please sign in to comment.