Compare commits

...
4 Commits
Author SHA1 Message Date
KUOKA Yusuke ed0854a5c0 feat: Environment and Environment Values (#267)
Resolves #253
2018-08-31 22:59:27 +09:00
KUOKA Yusuke 7c793fdb88 feat: helmfiles: <ordered glob patterns of helmfiles> configuration (#266)
Resolves #247
2018-08-31 12:03:18 +09:00
Dan Wendorf b70956b5d8 Bump Helm 2.9.1 -> 2.10.0 (#265) 2018-08-31 10:17:51 +09:00
KUOKA Yusuke 3840605e04 feat: helmfile apply [--auto-approve] (#263)
This command syncs releases only if there is any difference between the desired and the current state. It asks for an confirmation by default. Provide `--auto-approve` flag after the `apply` command to skip it.

Resolves #205
2018-08-31 10:15:02 +09:00
18 changed files with 778 additions and 283 deletions
+2 -2
View File
@@ -11,10 +11,10 @@ FROM alpine:3.7
RUN apk add --no-cache ca-certificates git bash curl
ARG HELM_VERSION=v2.9.1
ARG HELM_VERSION=v2.10.0
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="56ae2d5d08c68d6e7400d462d6ed10c929effac929fedce18d2636a9b4e166ba"
ARG HELM_SHA256="0fa2ed4983b1e4a3f90f776d08b88b0c73fd83f305b5b634175cb15e61342ffe"
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
tar zxf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
Generated
+1 -1
View File
@@ -85,6 +85,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "57e868f6ae57c81a07ee682742f3b71bf5c7956311a3bb8ea76459677fc104c7"
inputs-digest = "b1f000751afc0a44973307c69b6a4b8e8c1b807fd9881a13f370c30fcbcab7a2"
solver-name = "gps-cdcl"
solver-version = 1
+4
View File
@@ -9,3 +9,7 @@
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/imdario/mergo"
version = "0.3.4"
+158 -15
View File
@@ -185,25 +185,28 @@ USAGE:
COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update)
charts sync charts from state file (helm upgrade --install)
diff diff charts from state file against env (helm diff)
charts sync releases from state file (helm upgrade --install)
diff diff releases from state file against env (helm diff)
lint lint charts from state file (helm lint)
sync sync all resources from state file (repos, charts and local chart deps)
sync sync all resources from state file (repos, releases and chart deps)
apply apply all resources from state file only when there are changes
status retrieve status of releases in state file
delete delete charts from state file (helm delete)
test tets releases from state file (helm test)
delete delete releases from state file (helm delete)
test test releases from state file (helm test)
GLOBAL OPTIONS:
--file FILE, -f FILE load config from FILE (default: "helmfile.yaml")
--quiet, -q silence output
--namespace value, -n value Set namespace. Uses the namespace set in the context by default
--selector,l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease
--kube-context value Set kubectl context. Uses current context by default
--help, -h show help
--version, -v print the version
--helm-binary value, -b value path to helm binary
--file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference
--quiet, -q Silence output. Equivalent to log-level warn
--kube-context value Set kubectl context. Uses current context by default
--log-level value Set log level, default info
--namespace value, -n value Set namespace. Uses the namespace set in the context by default
--selector value, -l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease
--help, -h show help
--version, -v print the version
```
### sync
@@ -224,6 +227,12 @@ To supply the diff functionality Helmfile needs the [helm-diff](https://github.c
you should be able to simply execute `helm plugin install https://github.com/databus23/helm-diff`. For more details
please look at their [documentation](https://github.com/databus23/helm-diff#helm-diff-plugin).
### apply
The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds that there is any changes, `sync` is executed after prompting you for an confirmation. `--auto-approve` skips the confirmation.
An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters.
### delete
The `helmfile delete` sub-command deletes all the releases defined in the manfiests
@@ -362,6 +371,140 @@ 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,
split it into multiple yaml files.
Recommended granularity of helmfile.yaml files is "per microservice" or "per team".
And there are two ways to organize your files.
- Single directory
- Glob patterns
### Single directory
`helmfile -f path/to/directory` loads and runs all the yaml files under the specified directory, each file as an independent helmfile.yaml.
The default helmfile directory is `helmfile.d`, that is,
in case helmfile is unable to locate `helmfile.yaml`, it tries to locate `helmfile.d/*.yaml`.
All the yaml files under the specified directory are processed in the alphabetical order. For example, you can use a `<two digit number>-<microservice>.yaml` naming convention to control the sync order.
- `helmfile.d`/
- `00-database.yaml`
- `00-backend.yaml`
- `01-frontend.yaml`
### Glob patterns
In case you want more control over how multiple `helmfile.yaml` files are organized, use `helmfiles:` configuration key in the `helmfile.yaml`:
Suppose you have multiple microservices organized in a Git reposistory that looks like:
- `myteam/` (sometimes it is equivalent to a k8s ns, that is `kube-system` for `clusterops` team)
- `apps/`
- `filebeat/`
- `helmfile.yaml` (no `charts/` exists, because it depends on the stable/filebeat chart hosted on the official helm charts repository)
- `README.md` (each app managed by my team has a dedicated README maintained by the owners of the app)
- `metricbeat/`
- `helmfile.yaml`
- `README.md`
- `elastalert-operator/`
- `helmfile.yaml`
- `README.md`
- `charts/`
- `elastalert-operator/`
- `<the content of the local helm chart>`
The benefits of this structure is that you can run `git diff` to locate in which directory=microservice a git commit has changes.
It allows your CI system to run a workflow for the changed microservice only.
A downside of this is that you don't have an obvious way to sync all microservices at once. That is, you have to run:
```bash
for d in apps/*; do helmfile -f $d diff; if [ $? -eq 2 ]; then helmfile -f $d sync; fi; done
```
At this point, you'll start writing a `Makefile` under `myteam/` so that `make sync-all` will do the job.
It does work, but you can rely on the helmfile's feature instead.
Put `myteam/helmfile.yaml` that looks like:
```yaml
helmfiles:
- apps/*/helmfile.yaml
```
So that you can get rid of the `Makefile` and the bash snippet.
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.
## 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:
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
// Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
//
// Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
func askForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true
} else if response == "n" || response == "no" {
return false
}
}
}
+8
View File
@@ -0,0 +1,8 @@
package environment
type Environment struct {
Name string
Values map[string]interface{}
}
var EmptyEnvironment Environment
+157 -55
View File
@@ -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"
@@ -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",
@@ -106,7 +111,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
@@ -139,7 +144,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
@@ -183,26 +188,8 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
if c.Bool("sync-repos") {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
detailedExitCode := c.Bool("detailed-exitcode")
return state.DiffReleases(helm, values, workers, detailedExitCode)
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"))
})
},
},
@@ -226,7 +213,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
@@ -262,27 +249,65 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return executeSyncCommand(c, state, helm)
})
},
},
{
Name: "apply",
Usage: "apply all resources from state file only when there are changes",
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.BoolFlag{
Name: "auto-approve",
Usage: "Skip interactive approval before applying",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
errs := executeDiffCommand(c, state, helm, true)
// sync only when there are changes
if len(errs) > 0 {
allErrsIndicateChanges := true
for _, err := range errs {
switch e := err.(type) {
case *exec.ExitError:
status := e.Sys().(syscall.WaitStatus)
// `helm diff --detailed-exitcode` returns 2 when there are changes
allErrsIndicateChanges = allErrsIndicateChanges && status.ExitStatus() == 2
default:
allErrsIndicateChanges = false
}
}
msg := `Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`
if allErrsIndicateChanges {
autoApprove := c.Bool("auto-approve")
if autoApprove || !autoApprove && askForConfirmation(msg) {
return executeSyncCommand(c, state, helm)
}
}
}
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
return errs
})
},
},
@@ -302,7 +327,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
workers := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state)
@@ -332,7 +357,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
purge := c.Bool("purge")
args := args.GetArgs(c.String("args"), state)
@@ -368,7 +393,7 @@ func main() {
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
cleanup := c.Bool("cleanup")
timeout := c.Int("timeout")
@@ -393,29 +418,106 @@ func main() {
}
}
func eachDesiredStateDo(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
fileOrDirPath := c.GlobalString("file")
desiredStateFiles, err := findDesiredStateFiles(fileOrDirPath)
func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(helm, values, workers)
}
func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode bool) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
if c.Bool("sync-repos") {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
return errs
}
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.DiffReleases(helm, values, workers, detailedExitCode)
}
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
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, env string, logger *zap.SugaredLogger) error {
desiredStateFiles, err := findDesiredStateFiles(fileOrDir)
if err != nil {
return err
}
allSelectorNotMatched := true
for _, f := range desiredStateFiles {
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "").RenderTemplateFileToBuffer(f)
logger.Debugf("Processing %s", f)
yamlBuf, err := tmpl.NewFileRenderer(ioutil.ReadFile, "", environment.EmptyEnvironment).RenderTemplateFileToBuffer(f)
if err != nil {
return err
}
state, helm, noReleases, err := loadDesiredStateFromFile(
yamlBuf.Bytes(),
f,
c.GlobalString("kube-context"),
c.GlobalString("namespace"),
c.GlobalStringSlice("selector"),
c.App.Metadata["logger"].(*zap.SugaredLogger),
kubeContext,
namespace,
selectors,
env,
logger,
)
if err != nil {
return err
}
if len(state.Helmfiles) > 0 {
for _, globPattern := range state.Helmfiles {
matches, err := filepath.Glob(globPattern)
if err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
for _, m := range matches {
if err := findAndIterateOverDesiredStates(m, converge, kubeContext, namespace, selectors, env, logger); err != nil {
return fmt.Errorf("failed processing %s: %v", globPattern, err)
}
}
}
return nil
}
allSelectorNotMatched = allSelectorNotMatched && noReleases
if noReleases {
continue
@@ -489,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)
}
+1 -1
View File
@@ -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")
}
+71
View File
@@ -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
}
+248
View File
@@ -0,0 +1,248 @@
package state
import (
"fmt"
"reflect"
"testing"
)
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
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].Namespace != "mynamespace" {
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_InexistentEnv(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, "production", logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
production:
values:
- foo.yaml
- bar.yaml.gotmpl
releases:
- name: myrelease
namespace: mynamespace
chart: mychart
values:
- values.yaml.gotmpl
`)
fooYamlFile := "/example/path/to/foo.yaml"
fooYamlContent := []byte(`foo: foo
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
baz: "{{ readFile \"baz.txt\" }}"`)
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
barYamlContent := []byte(`foo: FOO
bar: {{ readFile "bar.txt" }}
`)
barTextFile := "/example/path/to/bar.txt"
barTextContent := []byte("BAR")
expected := map[string]interface{}{
"foo": "FOO",
"bar": "BAR",
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
"baz": "{{ readFile \"baz.txt\" }}",
}
valuesFile := "/example/path/to/values.yaml.gotmpl"
valuesContent := []byte(`env: {{ .Environment.Name }}`)
expectedValues := `env: production`
readFile := func(filename string) ([]byte, error) {
switch filename {
case fooYamlFile:
return fooYamlContent, nil
case barYamlFile:
return barYamlContent, nil
case barTextFile:
return barTextContent, nil
case valuesFile:
return valuesContent, nil
}
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
state, err := createFromYamlWithFileReader(yamlContent, yamlFile, "production", logger, readFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := state.env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actualValues := string(actualValuesData)
if !reflect.DeepEqual(expectedValues, actualValues) {
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
}
}
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
releases: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
}
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
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 := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
tier: frontend
foo: bar
- name: myrelease2
chart: mychart2
labels:
tier: frontend
- name: myrelease3
chart: mychart3
labels:
tier: backend
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestReadFromYaml_FilterNegatives(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease2
chart: mychart2
labels:
stage: post
- name: myrelease3
chart: mychart3
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
[]bool{true, false, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
package state
type EnvironmentSpec struct {
Values []string `yaml:"values"`
}
+31 -43
View File
@@ -14,6 +14,7 @@ import (
"regexp"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/valuesfile"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
@@ -21,16 +22,22 @@ import (
// HelmState structure for the helmfile
type HelmState struct {
BaseChartPath string
basePath string
Environments map[string]EnvironmentSpec
FilePath string
HelmDefaults HelmSpec `yaml:"helmDefaults"`
Helmfiles []string `yaml:"helmfiles"`
Context string `yaml:"context"`
DeprecatedReleases []ReleaseSpec `yaml:"charts"`
Namespace string `yaml:"namespace"`
Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"`
env environment.Environment
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
}
// HelmSpec to defines helmDefault values
@@ -97,27 +104,7 @@ type SetValue struct {
Values []string `yaml:"values"`
}
func CreateFromYaml(content []byte, file string, logger *zap.SugaredLogger) (*HelmState, error) {
var state HelmState
state.BaseChartPath, _ = 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
return &state, nil
}
const DefaultEnv = "default"
func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if state.Namespace != "" {
@@ -195,7 +182,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [
continue
}
chart := normalizeChart(state.BaseChartPath, release.Chart)
chart := normalizeChart(state.basePath, release.Chart)
if err := helm.SyncRelease(release.Name, chart, flags...); err != nil {
errQueue <- &ReleaseError{release, err}
}
@@ -248,7 +235,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
state.applyDefaultsTo(release)
flags, err := state.flagsForDiff(helm, state.BaseChartPath, release)
flags, err := state.flagsForDiff(helm, release)
if err != nil {
errs = append(errs, err)
}
@@ -270,7 +257,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
}
if len(errs) == 0 {
if err := helm.DiffRelease(release.Name, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil {
if err := helm.DiffRelease(release.Name, normalizeChart(state.basePath, release.Chart), flags...); err != nil {
errs = append(errs, err)
}
}
@@ -332,7 +319,7 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
go func() {
for release := range jobQueue {
errs := []error{}
flags, err := state.flagsForLint(helm, state.BaseChartPath, release)
flags, err := state.flagsForLint(helm, release)
if err != nil {
errs = append(errs, err)
}
@@ -349,8 +336,8 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
}
chartPath := ""
if pathExists(normalizeChart(state.BaseChartPath, release.Chart)) {
chartPath = normalizeChart(state.BaseChartPath, release.Chart)
if pathExists(normalizeChart(state.basePath, release.Chart)) {
chartPath = normalizeChart(state.basePath, release.Chart)
} else {
fetchFlags := []string{}
if release.Version != "" {
@@ -570,7 +557,7 @@ func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error {
for _, release := range state.Releases {
if isLocalChart(release.Chart) {
if err := helm.UpdateDeps(normalizeChart(state.BaseChartPath, release.Chart)); err != nil {
if err := helm.UpdateDeps(normalizeChart(state.basePath, release.Chart)); err != nil {
errs = append(errs, err)
}
}
@@ -637,30 +624,35 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas
flags = append(flags, "--recreate-pods")
}
common, err := state.namespaceAndValuesFlags(helm, state.BaseChartPath, release)
common, err := state.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
}
return append(flags, common...), nil
}
func (state *HelmState) flagsForDiff(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
func (state *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Version != "" {
flags = append(flags, "--version", release.Version)
}
common, err := state.namespaceAndValuesFlags(helm, basePath, release)
common, err := state.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
}
return append(flags, common...), nil
}
func (state *HelmState) flagsForLint(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
return state.namespaceAndValuesFlags(helm, basePath, release)
func (state *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
return state.namespaceAndValuesFlags(helm, release)
}
func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
func (state *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
r := valuesfile.NewRenderer(state.readFile, state.basePath, state.env)
return r.RenderToBytes(path)
}
func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
@@ -672,24 +664,20 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat
if filepath.IsAbs(typedValue) {
path = typedValue
} else {
path = filepath.Join(basePath, typedValue)
path = filepath.Join(state.basePath, typedValue)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
yamlBytes, err := state.RenderValuesFileToBytes(path)
valfile, err := ioutil.TempFile("", "values")
if err != nil {
return nil, err
}
defer valfile.Close()
r := valuesfile.NewRenderer(ioutil.ReadFile, state.BaseChartPath)
yamlBytes, err := r.RenderToBytes(path)
if err != nil {
return nil, err
}
if _, err := valfile.Write(yamlBytes); err != nil {
return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
}
@@ -712,7 +700,7 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat
}
}
for _, value := range release.Secrets {
path := filepath.Join(basePath, value)
path := filepath.Join(state.basePath, value)
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
+6 -158
View File
@@ -12,158 +12,6 @@ import (
var logger = helmexec.NewLogger(os.Stdout, "warn")
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
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].Namespace != "mynamespace" {
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
releases: mychart
`)
_, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
}
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
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 := CreateFromYaml(yamlContent, yamlFile, logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
tier: frontend
foo: bar
- name: myrelease2
chart: mychart2
labels:
tier: frontend
- name: myrelease3
chart: mychart3
labels:
tier: backend
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestReadFromYaml_FilterNegatives(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease2
chart: mychart2
labels:
stage: post
- name: myrelease3
chart: mychart3
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
[]bool{true, false, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := CreateFromYaml(yamlContent, yamlFile, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
@@ -266,7 +114,7 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
BaseChartPath: tt.fields.BaseChartPath,
basePath: tt.fields.BaseChartPath,
Context: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
Namespace: tt.fields.Namespace,
@@ -495,10 +343,10 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
BaseChartPath: "./",
Context: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
basePath: "./",
Context: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
}
helm := helmexec.New(logger, "default")
args, err := state.flagsForUpgrade(helm, tt.release)
@@ -861,7 +709,7 @@ func TestHelmState_SyncReleases(t *testing.T) {
func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{
BaseChartPath: "/src",
basePath: "/src",
Releases: []ReleaseSpec{
{
Chart: "./..",
+12 -2
View File
@@ -2,24 +2,34 @@ package tmpl
import (
"bytes"
"github.com/roboll/helmfile/environment"
)
type templateFileRenderer struct {
ReadFile func(string) ([]byte, error)
Context *Context
Data TemplateData
}
type TemplateData struct {
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
}
type FileRenderer interface {
RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error)
}
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string) *templateFileRenderer {
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *templateFileRenderer {
return &templateFileRenderer{
ReadFile: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: TemplateData{
Environment: env,
},
}
}
@@ -29,5 +39,5 @@ func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.B
return nil, err
}
return r.Context.RenderTemplateToBuffer(string(content))
return r.Context.RenderTemplateToBuffer(string(content), r.Data)
}
+6 -2
View File
@@ -14,14 +14,18 @@ func (c *Context) stringTemplate() *template.Template {
return template.New("stringTemplate").Funcs(funcMap)
}
func (c *Context) RenderTemplateToBuffer(s string) (*bytes.Buffer, error) {
func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) {
var t, parseErr = c.stringTemplate().Parse(s)
if parseErr != nil {
return nil, parseErr
}
var tplString bytes.Buffer
var execErr = t.Execute(&tplString, nil)
var d interface{}
if len(data) > 0 {
d = data[0]
}
var execErr = t.Execute(&tplString, d)
if execErr != nil {
return nil, execErr
+29
View File
@@ -31,6 +31,35 @@ func TestRenderTemplate_Values(t *testing.T) {
}
}
func TestRenderTemplate_WithData(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ .foo.bar }}
`
expected := `foo:
bar: FOO_BAR
`
expectedFilename := "values.yaml"
data := map[string]interface{}{
"foo": map[string]interface{}{
"bar": "FOO_BAR",
},
}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func renderTemplateToString(s string) (string, error) {
ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
+3 -2
View File
@@ -2,6 +2,7 @@ package valuesfile
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/tmpl"
"strings"
)
@@ -11,10 +12,10 @@ type renderer struct {
tmplFileRenderer tmpl.FileRenderer
}
func NewRenderer(readFile func(filename string) ([]byte, error), basePath string) *renderer {
func NewRenderer(readFile func(filename string) ([]byte, error), basePath string, env environment.Environment) *renderer {
return &renderer{
readFile: readFile,
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath),
tmplFileRenderer: tmpl.NewFileRenderer(readFile, basePath, env),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package valuesfile
import (
"fmt"
"github.com/roboll/helmfile/environment"
"reflect"
"testing"
)
@@ -24,7 +25,7 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
return []byte(dataFileContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
}, "")
}, "", environment.EmptyEnvironment)
buf, err := r.RenderToBytes(valuesTmplFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
@@ -49,7 +50,7 @@ func TestRenderToBytes_Yaml(t *testing.T) {
return []byte(valuesYamlContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
}, "")
}, "", environment.EmptyEnvironment)
buf, err := r.RenderToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)