From f16d96bc8f8cc5c7cdbcea25e3909f9b4a01dbad Mon Sep 17 00:00:00 2001 From: Max Audron Date: Thu, 11 Jun 2020 03:05:38 +0200 Subject: [PATCH] Add global hooks (#1301) Changes: * Add global hooks * Add top level hooks field to yaml spec * Add functions for global prepare and cleanup events * Call global prepare and cleanup events in withPreparedCharts function * Update README * Add helmfileCommand variable to withPreparedCharts Pass the information on what helmfileCommand has been run down from the top level functions through withReposAndPreparedCharts and withPreparedCharts. --- README.md | 13 +++++++++++++ pkg/app/app.go | 28 ++++++++++++++-------------- pkg/app/run.go | 16 +++++++++++----- pkg/state/state.go | 27 +++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 47c9ab12..89d027c6 100644 --- a/README.md +++ b/README.md @@ -983,6 +983,19 @@ Now, replace `echo` with any command you like, and rewrite `args` that actually For templating, imagine that you created a hook that generates a helm chart on-the-fly by running an external tool like ksonnet, kustomize, or your own template engine. It will allow you to write your helm releases with any language you like, while still leveraging goodies provided by helm. +### Global Hooks +In contrast to the per release hooks mentioned above these are run only once at the very beginning and end of the execution of a helmfile command and only the `prepare` and `cleanup` hooks are available respectively. + +They use the same syntax as per release hooks, but at the top level of your helmfile: +``` yaml +hooks: +- events: ["prepare", "cleanup"] + showlogs: true + command: "echo" + args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\ +"] +``` + ### Helmfile + Kustomize Do you prefer `kustomize` to write and organize your Kubernetes apps, but still want to leverage helm's useful features diff --git a/pkg/app/app.go b/pkg/app/app.go index 5e9534b0..8a06cbbf 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -101,7 +101,7 @@ func Init(app *App) *App { func (a *App) Deps(c DepsConfigProvider) error { return a.ForEachStateFiltered(func(run *Run) (errs []error) { - prepErrs := run.withReposAndPreparedCharts(false, c.SkipRepos(), func() { + prepErrs := run.withReposAndPreparedCharts(false, c.SkipRepos(), "deps", func() { errs = run.Deps(c) }) @@ -113,7 +113,7 @@ func (a *App) Deps(c DepsConfigProvider) error { func (a *App) Repos(c ReposConfigProvider) error { return a.ForEachStateFiltered(func(run *Run) (errs []error) { - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "repos", func() { errs = run.Repos(c) }) @@ -127,7 +127,7 @@ func (a *App) Repos(c ReposConfigProvider) error { func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { return a.ForEachStateFiltered(func(run *Run) (errs []error) { - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "charts", func() { errs = run.DeprecatedSyncCharts(c) }) @@ -153,7 +153,7 @@ func (a *App) Diff(c DiffConfigProvider) error { var errs []error - prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), func() { + prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), "diff", func() { msg, matched, affected, errs = run.Diff(c) }) @@ -204,7 +204,7 @@ func (a *App) Diff(c DiffConfigProvider) error { func (a *App) Template(c TemplateConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - prepErrs := run.withReposAndPreparedCharts(true, c.SkipDeps(), func() { + prepErrs := run.withReposAndPreparedCharts(true, c.SkipDeps(), "template", func() { ok, errs = a.template(run, c) }) @@ -216,7 +216,7 @@ func (a *App) Template(c TemplateConfigProvider) error { func (a *App) Lint(c LintConfigProvider) error { return a.ForEachStateFiltered(func(run *Run) (errs []error) { - prepErrs := run.withReposAndPreparedCharts(true, c.SkipDeps(), func() { + prepErrs := run.withReposAndPreparedCharts(true, c.SkipDeps(), "lint", func() { errs = run.Lint(c) }) @@ -228,7 +228,7 @@ func (a *App) Lint(c LintConfigProvider) error { func (a *App) Sync(c SyncConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), func() { + prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), "sync", func() { ok, errs = a.sync(run, c) }) @@ -248,7 +248,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { opts = append(opts, SetRetainValuesFiles(c.RetainValuesFiles())) err := a.ForEachState(func(run *Run) (ok bool, errs []error) { - prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), func() { + prepErrs := run.withReposAndPreparedCharts(false, c.SkipDeps(), "apply", func() { matched, updated, es := a.apply(run, c) mut.Lock() @@ -278,7 +278,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { func (a *App) Status(c StatusesConfigProvider) error { return a.ForEachStateFiltered(func(run *Run) (errs []error) { - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "status", func() { errs = run.Status(c) }) @@ -292,7 +292,7 @@ func (a *App) Status(c StatusesConfigProvider) error { func (a *App) Delete(c DeleteConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "delete", func() { ok, errs = a.delete(run, c.Purge(), c) }) @@ -306,7 +306,7 @@ func (a *App) Delete(c DeleteConfigProvider) error { func (a *App) Destroy(c DestroyConfigProvider) error { return a.ForEachState(func(run *Run) (ok bool, errs []error) { - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "destroy", func() { ok, errs = a.delete(run, true, c) }) @@ -326,7 +326,7 @@ func (a *App) Test(c TestConfigProvider) error { "or set helm.sh/hook-delete-policy\n") } - err := run.withPreparedCharts(false, func() { + err := run.withPreparedCharts(false, "test", func() { errs = run.Test(c) }) @@ -340,7 +340,7 @@ func (a *App) Test(c TestConfigProvider) error { func (a *App) PrintState(c StateConfigProvider) error { return a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState) (errs []error) { - err := NewRun(st, nil, NewContext()).withPreparedCharts(false, func() { + err := NewRun(st, nil, NewContext()).withPreparedCharts(false, "build", func() { state, err := st.ToYaml() if err != nil { errs = []error{err} @@ -363,7 +363,7 @@ func (a *App) ListReleases(c ListConfigProvider) error { var releases []*HelmRelease err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState) []error { - err := NewRun(st, nil, NewContext()).withPreparedCharts(false, func() { + err := NewRun(st, nil, NewContext()).withPreparedCharts(false, "list", func() { //var releases m for _, r := range st.Releases { diff --git a/pkg/app/run.go b/pkg/app/run.go index 0895ec02..4e67bdf4 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -32,7 +32,7 @@ func (r *Run) askForConfirmation(msg string) bool { return AskForConfirmation(msg) } -func (r *Run) withReposAndPreparedCharts(forceDownload bool, skipRepos bool, f func()) []error { +func (r *Run) withReposAndPreparedCharts(forceDownload bool, skipRepos bool, helmfileCommand string, f func()) []error { if !skipRepos { ctx := r.ctx if errs := ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 { @@ -40,14 +40,14 @@ func (r *Run) withReposAndPreparedCharts(forceDownload bool, skipRepos bool, f f } } - if err := r.withPreparedCharts(forceDownload, f); err != nil { + if err := r.withPreparedCharts(forceDownload, helmfileCommand, f); err != nil { return []error{err} } return nil } -func (r *Run) withPreparedCharts(forceDownload bool, f func()) error { +func (r *Run) withPreparedCharts(forceDownload bool, helmfileCommand string, f func()) error { if r.ReleaseToChart != nil { panic("Run.PrepareCharts can be called only once") } @@ -59,7 +59,11 @@ func (r *Run) withPreparedCharts(forceDownload bool, f func()) error { } defer os.RemoveAll(dir) - releaseToChart, errs := state.PrepareCharts(r.helm, r.state, dir, 2, "template", forceDownload) + if _, err = r.state.TriggerGlobalPrepareEvent(helmfileCommand); err != nil { + return err + } + + releaseToChart, errs := state.PrepareCharts(r.helm, r.state, dir, 2, helmfileCommand, forceDownload) if len(errs) > 0 { return fmt.Errorf("%v", errs) @@ -75,7 +79,9 @@ func (r *Run) withPreparedCharts(forceDownload bool, f func()) error { f() - return nil + _, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand) + + return err } func (r *Run) Deps(c DepsConfigProvider) []error { diff --git a/pkg/state/state.go b/pkg/state/state.go index 21804a31..953f5afb 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -59,6 +59,9 @@ type HelmState struct { Selectors []string `yaml:"-"` ApiVersions []string `yaml:"apiVersions,omitempty"` + // Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile + Hooks []event.Hook `yaml:"hooks,omitempty"` + Templates map[string]TemplateSpec `yaml:"templates"` Env environment.Environment `yaml:"-"` @@ -1441,6 +1444,30 @@ func (st *HelmState) PrepareReleases(helm helmexec.Interface, helmfileCommand st return nil } +func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, error) { + return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand) +} + +func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) { + return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand) +} + +func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) { + bus := &event.Bus{ + Hooks: st.Hooks, + StateFilePath: st.FilePath, + BasePath: st.basePath, + Namespace: st.OverrideNamespace, + Env: st.Env, + Logger: st.logger, + ReadFile: st.readFile, + } + data := map[string]interface{}{ + "HelmfileCommand": helmfileCmd, + } + return bus.Trigger(evt, evtErr, data) +} + func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { return st.triggerReleaseEvent("prepare", nil, r, helmfileCommand) }