diff --git a/README.md b/README.md index 863c3cd8..516a6c4c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Helmfile is a declarative spec for deploying helm charts. It lets you... To avoid upgrades for each iteration of `helm`, the `helmfile` executable delegates to `helm` - as a result, `helm` must be installed. -The default helmfile is `charts.yaml`: +The default helmfile is `helmfile.yaml`: ``` repositories: @@ -23,11 +23,11 @@ repositories: context: kube-context # kube-context (--kube-context) -charts: +releases: # Published chart example - - name: vault # helm deployment name + - name: vault # name of this release namespace: vault # target namespace - chart: roboll/vault-secret-manager # chart reference (repository) + chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax values: [ vault.yaml ] # value files (--values) set: # values (--set) - name: address @@ -38,9 +38,9 @@ charts: value: "{{ env \"PLATFORM_ID\" }}.my-domain.com" # Interpolate environment variable with a fixed string # Local chart example - - name: grafana # helm deployment name + - name: grafana # name of this release namespace: another # target namespace - chart: ../my-charts/grafana # chart reference (relative path to manifest) + chart: ../my-charts/grafana # the chart being installed to create this release, referenced by relative path to local chart values: - "../../my-values/grafana/values.yaml" # Values file (relative path to manifest) - "./values/{{ env \"PLATFORM_ENV\" }}/config.yaml" # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment. @@ -59,7 +59,7 @@ NAME: helmfile - USAGE: - main [global options] command [command options] [arguments...] + helmfile [global options] command [command options] [arguments...] COMMANDS: repos sync repositories from state file (helm repo add && helm repo update) @@ -69,7 +69,7 @@ COMMANDS: delete delete charts from state file (helm delete) GLOBAL OPTIONS: - --file FILE, -f FILE load config from FILE (default: "charts.yaml") + --file FILE, -f FILE load config from FILE (default: "helmfile.yaml") --quiet, -q silence output --kube-context value Set kubectl context. Uses current context by default --help, -h show help diff --git a/examples/deployments/published/charts.yaml b/examples/deployments/published/charts.yaml index 0b4a56c4..28335516 100644 --- a/examples/deployments/published/charts.yaml +++ b/examples/deployments/published/charts.yaml @@ -1,5 +1,5 @@ charts: # Published chart example - - name: grafana # helm deployment name + - name: grafana # helm release name namespace: grafana # target namespace chart: stable/grafana # chart reference (repository) diff --git a/helmexec/exec.go b/helmexec/exec.go index 09a51e9e..c92bab1c 100644 --- a/helmexec/exec.go +++ b/helmexec/exec.go @@ -46,7 +46,7 @@ func (helm *execer) UpdateRepo() error { return err } -func (helm *execer) SyncChart(name, chart string, flags ...string) error { +func (helm *execer) SyncRelease(name, chart string, flags ...string) error { out, err := helm.exec(append([]string{"upgrade", "--install", name, chart}, flags...)...) if helm.writer != nil { helm.writer.Write(out) @@ -54,7 +54,7 @@ func (helm *execer) SyncChart(name, chart string, flags ...string) error { return err } -func (helm *execer) DiffChart(name, chart string, flags ...string) error { +func (helm *execer) DiffRelease(name, chart string, flags ...string) error { out, err := helm.exec(append([]string{"diff", name, chart}, flags...)...) if helm.writer != nil { helm.writer.Write(out) @@ -62,7 +62,7 @@ func (helm *execer) DiffChart(name, chart string, flags ...string) error { return err } -func (helm *execer) DeleteChart(name string) error { +func (helm *execer) DeleteRelease(name string) error { out, err := helm.exec("delete", "--purge", name) if helm.writer != nil { helm.writer.Write(out) diff --git a/helmexec/helmexec.go b/helmexec/helmexec.go index b62fb70e..60b39796 100644 --- a/helmexec/helmexec.go +++ b/helmexec/helmexec.go @@ -6,7 +6,7 @@ type Interface interface { AddRepo(name, repository string) error UpdateRepo() error - SyncChart(name, chart string, flags ...string) error - DiffChart(name, chart string, flags ...string) error - DeleteChart(name string) error + SyncRelease(name, chart string, flags ...string) error + DiffRelease(name, chart string, flags ...string) error + DeleteRelease(name string) error } diff --git a/main.go b/main.go index 5d9f781f..a0c36341 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,8 @@ import ( ) const ( - helmfile = "charts.yaml" + DefaultHelmfile = "helmfile.yaml" + DeprecatedHelmfile = "charts.yaml" ) var Version string @@ -27,7 +28,7 @@ func main() { app.Flags = []cli.Flag{ cli.StringFlag{ Name: "file, f", - Value: helmfile, + Value: DefaultHelmfile, Usage: "load config from `FILE`", }, cli.BoolFlag{ @@ -108,7 +109,7 @@ func main() { values := c.StringSlice("values") workers := c.Int("concurrency") - if errs := state.SyncCharts(helm, values, workers); errs != nil && len(errs) > 0 { + if errs := state.SyncReleases(helm, values, workers); errs != nil && len(errs) > 0 { for _, err := range errs { fmt.Printf("err: %s\n", err.Error()) } @@ -157,7 +158,7 @@ func main() { values := c.StringSlice("values") - if errs := state.DiffCharts(helm, values); errs != nil && len(errs) > 0 { + if errs := state.DiffReleases(helm, values); errs != nil && len(errs) > 0 { for _, err := range errs { fmt.Printf("err: %s\n", err.Error()) } @@ -196,7 +197,7 @@ func main() { values := c.StringSlice("values") workers := c.Int("concurrency") - if errs := state.SyncCharts(helm, values, workers); errs != nil && len(errs) > 0 { + if errs := state.SyncReleases(helm, values, workers); errs != nil && len(errs) > 0 { for _, err := range errs { fmt.Printf("err: %s\n", err.Error()) } @@ -214,7 +215,7 @@ func main() { return err } - if errs := state.DeleteCharts(helm); errs != nil && len(errs) > 0 { + if errs := state.DeleteReleases(helm); errs != nil && len(errs) > 0 { for _, err := range errs { fmt.Printf("err: %s\n", err.Error()) } @@ -238,28 +239,36 @@ func before(c *cli.Context) (*state.HelmState, helmexec.Interface, error) { kubeContext := c.GlobalString("kube-context") namespace := c.GlobalString("namespace") - state, err := state.ReadFromFile(file) + st, err := state.ReadFromFile(file) + if err != nil && strings.Contains(err.Error(), fmt.Sprintf("open %s:", DefaultHelmfile)) { + var fallbackErr error + st, fallbackErr = state.ReadFromFile(DeprecatedHelmfile) + if fallbackErr != nil { + return nil, nil, fmt.Errorf("failed to read %s and %s: %v", file, DeprecatedHelmfile, err) + } + log.Printf("warn: charts.yaml is loaded: charts.yaml is deprecated in favor of helmfile.yaml. See https://github.com/roboll/helmfile/issues/25 for more information") + } if err != nil { return nil, nil, err } - if state.Context != "" { + if st.Context != "" { if kubeContext != "" { log.Printf("err: Cannot use option --kube-context and set attribute context.") os.Exit(1) } - kubeContext = state.Context + kubeContext = st.Context } if namespace != "" { - if state.Namespace != "" { + if st.Namespace != "" { log.Printf("err: Cannot use option --namespace and set attribute namespace.") os.Exit(1) } - state.Namespace = namespace + st.Namespace = namespace } var writer io.Writer if !quiet { writer = os.Stdout } - return state, helmexec.NewHelmExec(writer, kubeContext), nil + return st, helmexec.NewHelmExec(writer, kubeContext), nil } diff --git a/state/state.go b/state/state.go index 774c791a..7659f3df 100644 --- a/state/state.go +++ b/state/state.go @@ -13,17 +13,19 @@ import ( "github.com/roboll/helmfile/helmexec" "bytes" - yaml "gopkg.in/yaml.v1" "path" "regexp" + + yaml "gopkg.in/yaml.v1" ) type HelmState struct { - BaseChartPath string - Context string `yaml:"context"` - Namespace string `yaml:"namespace"` - Repositories []RepositorySpec `yaml:"repositories"` - Charts []ChartSpec `yaml:"charts"` + BaseChartPath string + Context string `yaml:"context"` + DeprecatedReleases []ReleaseSpec `yaml:"charts"` + Namespace string `yaml:"namespace"` + Repositories []RepositorySpec `yaml:"repositories"` + Releases []ReleaseSpec `yaml:"releases"` } type RepositorySpec struct { @@ -31,11 +33,13 @@ type RepositorySpec struct { URL string `yaml:"url"` } -type ChartSpec struct { +type ReleaseSpec struct { + // Chart is the name of the chart being installed to create this release Chart string `yaml:"chart"` Version string `yaml:"version"` Verify bool `yaml:"verify"` + // Name is the name of this release Name string `yaml:"name"` Namespace string `yaml:"namespace"` Values []string `yaml:"values"` @@ -55,13 +59,25 @@ func ReadFromFile(file string) (*HelmState, error) { if err != nil { return nil, err } + return readFromYaml(content, file) +} +func readFromYaml(content []byte, file string) (*HelmState, error) { var state HelmState state.BaseChartPath, _ = filepath.Abs(path.Dir(file)) if err := yaml.Unmarshal(content, &state); err != nil { return nil, err } + + if len(state.DeprecatedReleases) > 0 { + if len(state.Releases) > 0 { + return nil, fmt.Errorf("failed to parse %s: you can't specify both `charts` and `releases` sections", file) + } + state.Releases = state.DeprecatedReleases + state.DeprecatedReleases = []ReleaseSpec{} + } + return &state, nil } @@ -100,7 +116,7 @@ func renderTemplateString(s string) (string, error) { return tplString.String(), nil } -func (state *HelmState) applyDefaultsTo(spec ChartSpec) ChartSpec { +func (state *HelmState) applyDefaultsTo(spec ReleaseSpec) ReleaseSpec { spec.Namespace = state.Namespace return spec } @@ -130,21 +146,21 @@ func (state *HelmState) SyncRepos(helm helmexec.Interface) []error { return nil } -func (state *HelmState) SyncCharts(helm helmexec.Interface, additonalValues []string, workerLimit int) []error { +func (state *HelmState) SyncReleases(helm helmexec.Interface, additonalValues []string, workerLimit int) []error { errs := []error{} - jobQueue := make(chan ChartSpec) + jobQueue := make(chan ReleaseSpec) doneQueue := make(chan bool) errQueue := make(chan error) if workerLimit < 1 { - workerLimit = len(state.Charts) + workerLimit = len(state.Releases) } for w := 1; w <= workerLimit; w++ { go func() { - for chart := range jobQueue { - chartWithDefaults := state.applyDefaultsTo(chart) - flags, flagsErr := flagsForChart(state.BaseChartPath, &chartWithDefaults) + for release := range jobQueue { + releaseWithDefaults := state.applyDefaultsTo(release) + flags, flagsErr := flagsForRelease(state.BaseChartPath, &releaseWithDefaults) if flagsErr != nil { errQueue <- flagsErr doneQueue <- true @@ -166,7 +182,7 @@ func (state *HelmState) SyncCharts(helm helmexec.Interface, additonalValues []st continue } - if err := helm.SyncChart(chart.Name, normalizeChart(state.BaseChartPath, chart.Chart), flags...); err != nil { + if err := helm.SyncRelease(release.Name, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil { errQueue <- err } doneQueue <- true @@ -175,13 +191,13 @@ func (state *HelmState) SyncCharts(helm helmexec.Interface, additonalValues []st } go func() { - for _, chart := range state.Charts { - jobQueue <- chart + for _, release := range state.Releases { + jobQueue <- release } close(jobQueue) }() - for i := 0; i < len(state.Charts); { + for i := 0; i < len(state.Releases); { select { case err := <-errQueue: errs = append(errs, err) @@ -197,16 +213,16 @@ func (state *HelmState) SyncCharts(helm helmexec.Interface, additonalValues []st return nil } -func (state *HelmState) DiffCharts(helm helmexec.Interface, additonalValues []string) []error { +func (state *HelmState) DiffReleases(helm helmexec.Interface, additonalValues []string) []error { var wg sync.WaitGroup errs := []error{} - for _, chart := range state.Charts { + for _, release := range state.Releases { wg.Add(1) - go func(wg *sync.WaitGroup, chart ChartSpec) { + go func(wg *sync.WaitGroup, release ReleaseSpec) { // Plugin command doesn't support explicit namespace - chart.Namespace = "" - flags, flagsErr := flagsForChart(state.BaseChartPath, &chart) + release.Namespace = "" + flags, flagsErr := flagsForRelease(state.BaseChartPath, &release) if flagsErr != nil { errs = append(errs, flagsErr) } @@ -218,12 +234,12 @@ func (state *HelmState) DiffCharts(helm helmexec.Interface, additonalValues []st flags = append(flags, "--values", valfile) } if len(errs) == 0 { - if err := helm.DiffChart(chart.Name, normalizeChart(state.BaseChartPath, chart.Chart), flags...); err != nil { + if err := helm.DiffRelease(release.Name, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil { errs = append(errs, err) } } wg.Done() - }(&wg, chart) + }(&wg, release) } wg.Wait() @@ -234,18 +250,18 @@ func (state *HelmState) DiffCharts(helm helmexec.Interface, additonalValues []st return nil } -func (state *HelmState) DeleteCharts(helm helmexec.Interface) []error { +func (state *HelmState) DeleteReleases(helm helmexec.Interface) []error { var wg sync.WaitGroup errs := []error{} - for _, chart := range state.Charts { + for _, release := range state.Releases { wg.Add(1) - go func(wg *sync.WaitGroup, chart ChartSpec) { - if err := helm.DeleteChart(chart.Name); err != nil { + go func(wg *sync.WaitGroup, release ReleaseSpec) { + if err := helm.DeleteRelease(release.Name); err != nil { errs = append(errs, err) } wg.Done() - }(&wg, chart) + }(&wg, release) } wg.Wait() @@ -268,18 +284,18 @@ func normalizeChart(basePath, chart string) string { return filepath.Join(basePath, chart) } -func flagsForChart(basePath string, chart *ChartSpec) ([]string, error) { +func flagsForRelease(basePath string, release *ReleaseSpec) ([]string, error) { flags := []string{} - if chart.Version != "" { - flags = append(flags, "--version", chart.Version) + if release.Version != "" { + flags = append(flags, "--version", release.Version) } - if chart.Verify { + if release.Verify { flags = append(flags, "--verify") } - if chart.Namespace != "" { - flags = append(flags, "--namespace", chart.Namespace) + if release.Namespace != "" { + flags = append(flags, "--namespace", release.Namespace) } - for _, value := range chart.Values { + for _, value := range release.Values { valfile := filepath.Join(basePath, value) valfileRendered, err := renderTemplateString(valfile) if err != nil { @@ -287,9 +303,9 @@ func flagsForChart(basePath string, chart *ChartSpec) ([]string, error) { } flags = append(flags, "--values", valfileRendered) } - if len(chart.SetValues) > 0 { + if len(release.SetValues) > 0 { val := []string{} - for _, set := range chart.SetValues { + for _, set := range release.SetValues { renderedValue, err := renderTemplateString(set.Value) if err != nil { return nil, err @@ -303,10 +319,10 @@ func flagsForChart(basePath string, chart *ChartSpec) ([]string, error) { * START 'env' section for backwards compatibility ***********/ // The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality - if len(chart.EnvValues) > 0 { + if len(release.EnvValues) > 0 { val := []string{} envValErrs := []string{} - for _, set := range chart.EnvValues { + for _, set := range release.EnvValues { value, isSet := os.LookupEnv(set.Value) if isSet { val = append(val, fmt.Sprintf("%s=%s", set.Name, value)) diff --git a/state/state_test.go b/state/state_test.go new file mode 100644 index 00000000..3a879393 --- /dev/null +++ b/state/state_test.go @@ -0,0 +1,58 @@ +package state + +import ( + "testing" +) + +func TestReadFromYaml(t *testing.T) { + yamlFile := "example/path/to/yaml/file" + yamlContent := []byte(`releases: +- name: myrelease + chart: mychart +`) + state, err := readFromYaml(yamlContent, yamlFile) + if err != nil { + t.Errorf("unxpected error: %v", err) + } + + if state.Releases[0].Name != "myrelease" { + t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name) + } + if state.Releases[0].Chart != "mychart" { + t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart) + } +} + +func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) { + yamlFile := "example/path/to/yaml/file" + yamlContent := []byte(`charts: +- name: myrelease + chart: mychart +`) + state, err := readFromYaml(yamlContent, yamlFile) + if err != nil { + t.Errorf("unxpected error: %v", err) + } + + if state.Releases[0].Name != "myrelease" { + t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name) + } + if state.Releases[0].Chart != "mychart" { + t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart) + } +} + +func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) { + yamlFile := "example/path/to/yaml/file" + yamlContent := []byte(`charts: +- name: myrelease1 + chart: mychart1 +releases: +- name: myrelease2 + chart: mychart2 +`) + _, err := readFromYaml(yamlContent, yamlFile) + if err == nil { + t.Error("expected error") + } +}