mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 16:37:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0854a5c0 | ||
|
|
7c793fdb88 | ||
|
|
b70956b5d8 | ||
|
|
3840605e04 |
+2
-2
@@ -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
@@ -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
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/imdario/mergo"
|
||||
version = "0.3.4"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package environment
|
||||
|
||||
type Environment struct {
|
||||
Name string
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
var EmptyEnvironment Environment
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package state
|
||||
|
||||
type EnvironmentSpec struct {
|
||||
Values []string `yaml:"values"`
|
||||
}
|
||||
+31
-43
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user