Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Environment and Environment Values #267

Merged
merged 1 commit into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
[prune]
go-tests = true
unused-packages = true

[[constraint]]
name = "github.com/imdario/mergo"
version = "0.3.4"
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,70 @@ proxy:
scheme: {{ env "SCHEME" | default "https" }}
```

## Environment

When you want to customize the contents of `helmfile.yaml` or `values.yaml` files per environment, use this feature.

You can define as many environments as you want under `environments` in `helmfile.yaml`.

The environment name defaults to `default`, that is, `helmfile sync` implies the `default` environment.
The selected environment name can be referenced from `helmfile.yaml` and `values.yaml.gotmpl` by `{{ .Environment.Name }}`.

If you want to specify a non-default environment, provide a `--environment NAME` flag to `helmfile` like `helmfile --environment production sync`.

The below example shows how to define a production-only release:

```yaml
environments:
default:
production:

releases:

{{ if (eq .Environment.Name "production" }}
- name: newrelic-agent
# snip
{{ end }}
- name: myapp
# snip
```

## Environment Values

Environment Values allows you to inject a set of values specific to the selected environment, into values.yaml templates.
Use it to inject common values from the environment to multiple values files, to make your configuration DRY.

Suppose you have three files `helmfile.yaml`, `production.yaml` and `values.yaml.gotmpl`:

`helmfile.yaml`

```yaml
environments:
production:
values:
- production.yaml

releases:
- name: myapp
values:
- values.yaml.gotmpl
```

`production.yaml`

```yaml
domain: prod.example.com
```

`values.yaml.gotmpl`

```yaml
domain: {{ .Environment.Values.domain | default "dev.example.com" }}
```

`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
whereas `helmfile --environment production sync` installs the app with the value `domain=production.example.com`.

## Separating helmfile.yaml into multiple independent files

Once your `helmfile.yaml` got to contain too many releases,
Expand Down
8 changes: 8 additions & 0 deletions environment/environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package environment

type Environment struct {
Name string
Values map[string]interface{}
}

var EmptyEnvironment Environment
24 changes: 18 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os/exec"

"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
Expand Down Expand Up @@ -68,6 +69,10 @@ func main() {
Name: "file, f",
Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference",
},
cli.StringFlag{
Name: "environment, e",
Usage: "specify the environment name. defaults to `default`",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "Silence output. Equivalent to log-level warn",
Expand Down Expand Up @@ -463,18 +468,24 @@ func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*st
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
return findAndIterateOverDesiredStates(fileOrDir, converge, kubeContext, namespace, selectors, logger)

env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}

return findAndIterateOverDesiredStates(fileOrDir, converge, kubeContext, namespace, selectors, env, logger)
}

func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, kubeContext, namespace string, selectors []string, logger *zap.SugaredLogger) error {
func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error, kubeContext, namespace string, selectors []string, env string, logger *zap.SugaredLogger) error {
desiredStateFiles, err := findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
allSelectorNotMatched := true
for _, f := range desiredStateFiles {
logger.Debugf("Processing %s", f)
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "").RenderTemplateFileToBuffer(f)
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "", environment.EmptyEnvironment).RenderTemplateFileToBuffer(f)
if err != nil {
return err
}
Expand All @@ -484,6 +495,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
kubeContext,
namespace,
selectors,
env,
logger,
)
if err != nil {
Expand All @@ -498,7 +510,7 @@ func findAndIterateOverDesiredStates(fileOrDir string, converge func(*state.Helm
}
sort.Strings(matches)
for _, m := range matches {
if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, logger); err != nil {
if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, env, logger); err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err)
}
}
Expand Down Expand Up @@ -579,8 +591,8 @@ func directoryExistsAt(path string) bool {
return err == nil && fileInfo.Mode().IsDir()
}

func loadDesiredStateFromFile(yaml []byte, file string, kubeContext, namespace string, labels []string, logger *zap.SugaredLogger) (*state.HelmState, helmexec.Interface, bool, error) {
st, err := state.CreateFromYaml(yaml, file, logger)
func loadDesiredStateFromFile(yaml []byte, file string, kubeContext, namespace string, labels []string, env string, logger *zap.SugaredLogger) (*state.HelmState, helmexec.Interface, bool, error) {
st, err := state.CreateFromYaml(yaml, file, env, logger)
if err != nil {
return nil, nil, false, fmt.Errorf("failed to read %s: %v", file, err)
}
Expand Down
2 changes: 1 addition & 1 deletion main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestReadFromYaml_DuplicateReleaseName(t *testing.T) {
labels:
stage: post
`)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, logger)
_, _, _, err := loadDesiredStateFromFile(yamlContent, yamlFile, "default", "default", []string{}, "default", logger)
if err == nil {
t.Error("error expected but not happened")
}
Expand Down
71 changes: 71 additions & 0 deletions state/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package state

import (
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io/ioutil"
"path/filepath"
)

func CreateFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
return createFromYamlWithFileReader(content, file, env, logger, ioutil.ReadFile)
}

func createFromYamlWithFileReader(content []byte, file string, env string, logger *zap.SugaredLogger, readFile func(string) ([]byte, error)) (*HelmState, error) {
var state HelmState

state.basePath, _ = filepath.Abs(filepath.Dir(file))
if err := yaml.UnmarshalStrict(content, &state); err != nil {
return nil, err
}
state.FilePath = file

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{}
}

state.logger = logger

e, err := state.loadEnv(env, readFile)
if err != nil {
return nil, err
}
state.env = *e

state.readFile = readFile

return &state, nil
}

func (state *HelmState) loadEnv(name string, readFile func(string) ([]byte, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := state.Environments[name]
if ok {
r := valuesfile.NewRenderer(readFile, state.basePath, environment.EmptyEnvironment)
for _, envvalFile := range envSpec.Values {
bytes, err := r.RenderToBytes(filepath.Join(state.basePath, envvalFile))
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFile, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFile, err)
}
}
} else if name != DefaultEnv {
return nil, fmt.Errorf("environment \"%s\" is not defined in \"%s\"", name, state.FilePath)
}

return &environment.Environment{Name: name, Values: envVals}, nil
}
Loading