Skip to content

Commit

Permalink
faet: prepare and cleanup release event hooks
Browse files Browse the repository at this point in the history
Resolves #295
Resolves #330
Resolves #329 (Supports templating of only `releases[].hooks[].command` and `args` right now
Resolevs #324
  • Loading branch information
mumoshu committed Sep 19, 2018
1 parent 9808849 commit 0ab9eef
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 15 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,61 @@ mysetting: |

The possibility is endless. Try importing values from your golang app, bash script, jsonnet, or anything!

## Hooks

Hook is an per-release extension point of helmfile that is composed of:

- `events`
- `command`
- `args`

helmfile triggers various `events` while it is running.
One `events` are triggered, associated `hooks` are executed, by running the `command` with `args`.

Currently supported `events` are:

- `prepare`
- `cleanup`

Hooks associated to `prepare` events are triggered after each release in your helmfile is loaded from YAML, before executed.

Hooks associated to `cleanup` events are triggered after each release is processed.

The following is an example hook that just prints the contextual information provided to hook:

```
releases:
- name: myapp
chart: mychart
# *snip*
hooks:
- events: ["prepare", "cleanup"]
command: ["echo"]
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
```
Let's say you ran `helmfile --environment prod sync`, the above hook results in executing:
```
echo {{Environment.Name}} {{.Release.Name}} {{.HelmfileCommand}}
```
Whereas the template expressions are executed thus the command becomes:
```
echo prod myapp sync
```
Now, replace `echo` with any command you like, and rewrite `args` that actually conforms to the command, so that you can integrate any command that does:
- templating
- linting
- testing
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.
## Using env files
helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
Expand Down
103 changes: 103 additions & 0 deletions state/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package state

import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
)

const (
ReleaseEventHookPrepare = "prepare"
ReleaseEventHookCleanup = "cleanup"
)

type ReleaseEventHookSpec struct {
Name string `yaml:"name"`
Events []string `yaml:"events"`
Command []string `yaml:"command"`
Args []string `yaml:"args""`
}

type ReleaseEventHookTemplateData struct {
Release *ReleaseSpec
HelmfileCommand string
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
Namespace string
Event Event
}

type Event struct {
Name string
}

func (state *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return state.triggerEvent(r, helmfileCommand, ReleaseEventHookPrepare)
}

func (state *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return state.triggerEvent(r, helmfileCommand, ReleaseEventHookCleanup)
}

func (state *HelmState) triggerEvent(r *ReleaseSpec, helmfileCommand string, event string) (bool, error) {
if state.runner == nil {
state.runner = helmexec.ShellRunner{}
}

for _, hook := range r.Hooks {
contained := false
for _, e := range hook.Events {
contained = contained || e == event
}
if !contained {
continue
}

var err error

name := hook.Name
if name == "" {
name = hook.Command[0]
}

render := tmpl.NewTextRenderer(state.readFile, state.basePath, ReleaseEventHookTemplateData{
Release: r,
HelmfileCommand: helmfileCommand,
Environment: state.Env,
Namespace: state.Namespace,
Event: Event{
Name: event,
},
})

state.logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, event)

command := make([]string, len(hook.Command))
args := make([]string, len(hook.Args))
for i, raw := range hook.Command {
command[i], err = render.RenderTemplateText(raw)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
}
for i, raw := range hook.Args {
args[i], err = render.RenderTemplateText(raw)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
}

allargs := []string{}
allargs = append(allargs, command[1:]...)
allargs = append(allargs, args...)
bytes, err := state.runner.Execute(command[0], allargs)
if err != nil {
return false, err
}
state.logger.Debugf("hook[%s]: %s\n", name, string(bytes))
}

return true, nil
}
66 changes: 51 additions & 15 deletions state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type HelmState struct {
logger *zap.SugaredLogger

readFile func(string) ([]byte, error)

runner helmexec.Runner
}

// HelmSpec to defines helmDefault values
Expand Down Expand Up @@ -91,6 +93,9 @@ type ReleaseSpec struct {
// Installed, when set to true, `delete --purge` the release
Installed *bool `yaml:"installed"`

// 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 []ReleaseEventHookSpec `yaml:"hooks"`

// Name is the name of this release
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Expand Down Expand Up @@ -175,6 +180,12 @@ func (state *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalV
go func() {
for release := range jobs {
state.applyDefaultsTo(release)

if _, err := state.triggerPrepareEvent(release, "sync"); err != nil {
results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, err}}}
continue
}

flags, flagsErr := state.flagsForUpgrade(helm, release)
if flagsErr != nil {
results <- syncPrepareResult{errors: []*ReleaseError{&ReleaseError{release, flagsErr}}}
Expand Down Expand Up @@ -282,6 +293,10 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
} else {
results <- syncResult{}
}

if _, err := state.triggerCleanupEvent(prep.release, "sync"); err != nil {
state.logger.Warnf("warn: %v\n", err)
}
}
}()
}
Expand Down Expand Up @@ -312,7 +327,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
}

// downloadCharts will download and untar charts for Lint and Template
func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, workerLimit int) (map[string]string, []error) {
func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, workerLimit int, helmfileCommand string) (map[string]string, []error) {
temp := make(map[string]string, len(state.Releases))
type downloadResults struct {
releaseName string
Expand All @@ -336,19 +351,24 @@ func (state *HelmState) downloadCharts(helm helmexec.Interface, dir string, work
if pathExists(normalizeChart(state.basePath, release.Chart)) {
chartPath = normalizeChart(state.basePath, release.Chart)
} else {
fetchFlags := []string{}
if release.Version != "" {
chartPath = path.Join(dir, release.Name, release.Version, release.Chart)
fetchFlags = append(fetchFlags, "--version", release.Version)
} else {
chartPath = path.Join(dir, release.Name, "latest", release.Chart)
}
ok, err := state.triggerPrepareEvent(release, helmfileCommand)
if err != nil {
errs = append(errs, err)
} else if !ok {
fetchFlags := []string{}
if release.Version != "" {
chartPath = path.Join(dir, release.Name, release.Version, release.Chart)
fetchFlags = append(fetchFlags, "--version", release.Version)
} else {
chartPath = path.Join(dir, release.Name, "latest", release.Chart)
}

// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(release.Chart, fetchFlags...); err != nil {
errs = append(errs, err)
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(release.Chart, fetchFlags...); err != nil {
errs = append(errs, err)
}
}
}
chartPath = path.Join(chartPath, chartNameWithoutRepository(release.Chart))
Expand Down Expand Up @@ -387,7 +407,7 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu
}
defer os.RemoveAll(dir)

temp, errs := state.downloadCharts(helm, dir, workerLimit)
temp, errs := state.downloadCharts(helm, dir, workerLimit, "template")

if errs != nil {
errs = append(errs, err)
Expand Down Expand Up @@ -420,6 +440,10 @@ func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValu
errs = append(errs, err)
}
}

if _, err := state.triggerCleanupEvent(&release, "template"); err != nil {
state.logger.Warnf("warn: %v\n", err)
}
}

if len(errs) != 0 {
Expand All @@ -440,7 +464,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
}
defer os.RemoveAll(dir)

temp, errs := state.downloadCharts(helm, dir, workerLimit)
temp, errs := state.downloadCharts(helm, dir, workerLimit, "lint")
if errs != nil {
errs = append(errs, err)
return errs
Expand Down Expand Up @@ -472,6 +496,10 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
errs = append(errs, err)
}
}

if _, err := state.triggerCleanupEvent(&release, "lint"); err != nil {
state.logger.Warnf("warn: %v\n", err)
}
}

if len(errs) != 0 {
Expand Down Expand Up @@ -523,6 +551,10 @@ func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalV

state.applyDefaultsTo(release)

if _, err := state.triggerPrepareEvent(release, "diff"); err != nil {
errs = append(errs, err)
}

flags, err := state.flagsForDiff(helm, release)
if err != nil {
errs = append(errs, err)
Expand Down Expand Up @@ -617,6 +649,10 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
// diff succeeded, found no changes
results <- diffResult{}
}

if _, err := state.triggerCleanupEvent(prep.release, "diff"); err != nil {
state.logger.Warnf("warn: %v\n", err)
}
}
}()
}
Expand Down
34 changes: 34 additions & 0 deletions tmpl/text.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package tmpl

import (
"bytes"
)

type templateTextRenderer struct {
ReadText func(string) ([]byte, error)
Context *Context
Data interface{}
}

type TextRenderer interface {
RenderTemplateTextToBuffer(text string) (*bytes.Buffer, error)
}

func NewTextRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *templateTextRenderer {
return &templateTextRenderer{
ReadText: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: data,
}
}

func (r *templateTextRenderer) RenderTemplateText(text string) (string, error) {
buf, err := r.Context.RenderTemplateToBuffer(text, r.Data)
if err != nil {
return "", err
}
return buf.String(), nil
}

0 comments on commit 0ab9eef

Please sign in to comment.