Compare commits

...
12 Commits
Author SHA1 Message Date
Yusuke KUOKA 08b48a8cca fix test 2019-04-03 23:43:23 +09:00
KUOKA Yusuke 0e00cdf2a5 fix: helmfile destroy should delete releases in the specified tiller namespace (#536)
Actually, 4 helm commands including "list", "diff", "status" and "delete" were not taking tiller-rerelated flags into account in helmfile. This fixes all these commands.

Fixes #534
2019-04-03 23:41:41 +09:00
Erik Osterman f522ba320b Add slack links (#533) 2019-04-03 22:36:17 +09:00
KUOKA Yusuke 8f1a15c9cd feat: helmfile destroy deletes and purges releases (#530)
* feat: `helmfile destroy` deletes and purges releases

This adds `helmfile destroy` that is basically `helmfile delete --purge`.

I've also tweaked the behavior of `delete` and `destroy` for releases with `installed: false`, so that it becomes consistent with other helmfile commands.
It now delete releases only when `installed: true` AND the release is already installed.

**Why an another command?**

Because it's easy to remember, and it also makes it easier to iterate on your helmfile.

We've been using `helmfile delete` from the beginning of helmfile,
and several months have been passed since we've added `--purge` to it.

We noticed that we always prefer to use `--purge` so that we can quickly iterate on helmfile by
e.g. `helmfile delete --purge && helmfile sync`. But making `--purge` default makes the `delete` command inconsistent with the helm's `delete`.

`destroy`, on the other hand, doesn't have such problem, and is still easy to remember for terraform users.

Resolves #511

* Update docs about `helmfile delete` and `helmfile destroy`
2019-04-02 21:17:38 +09:00
Patrick Valsecchi fa95e0dd92 Allow the same release name on different tillerNamespaces (#529)
Closes #528
2019-04-02 18:51:11 +09:00
KUOKA Yusuke eb36e93c5a fix: helmfile should not complain on missing values files for undesired releases (#526)
Fixes #519
2019-04-01 22:29:16 +09:00
KUOKA Yusuke 3edc7d6f33 fix: helmfile diff should not leave temp values files (#525)
Fixes #503 once again. A follow-up to #520 which was incomplete.
2019-04-01 22:24:18 +09:00
sgandon 57df25c3ff doc(team): changed the sub-helmfile as template (#524)
try to make the doc more clear.
2019-04-01 07:41:11 +09:00
KUOKA Yusuke 14a392666e feat: easier way to share configuration across helmfiles (#522)
This basically allows to define both `releases` and `helmfiles` within a helmfile.yaml, so that you can start using sub-helmfiles easily, by extracting only reused releases.

Resolves #445
2019-03-31 22:51:06 +09:00
KUOKA Yusuke 283dac1531 fix: Various helmfile commands should not leave temp values files (#520)
Fixes #504
2019-03-31 14:49:51 +09:00
KUOKA Yusuke f5e565ea3e feat: Tiller configuration per release (#516)
Resolves #486
2019-03-29 23:45:31 +09:00
KUOKA Yusuke 5f8b2e5c7f feat: Enhanced get template function (#513)
`get` is now able to take one more optional argument, that is used as the default value when the value for the key does not exist.

Resolves #465
Fixes #427
Fixes #357
Ref #460
2019-03-29 12:38:16 +09:00
15 changed files with 1140 additions and 87 deletions
+39 -4
View File
@@ -3,6 +3,7 @@
Deploy Kubernetes Helm Charts
[![Docker Repository on Quay](https://quay.io/repository/roboll/helmfile/status "Docker Repository on Quay")](https://quay.io/repository/roboll/helmfile)
[![Slack Community #helmfile](https://slack.sweetops.com/badge.svg)](https://slack.sweetops.com)
## status
@@ -52,6 +53,14 @@ helmDefaults:
timeout: 600
recreatePods: true
force: true
# enable TLS for request to Tiller
tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
releases:
# Published chart example
@@ -101,6 +110,14 @@ releases:
installed: true
# restores previous state in case of failed release
atomic: true
# enable TLS for request to Tiller
tls: true
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
tlsCACert: "path/to/ca.pem"
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
tlsCert: "path/to/cert.pem"
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
# Local chart example
- name: grafana # name of this release
@@ -196,16 +213,20 @@ NAME:
USAGE:
helmfile [global options] command [command options] [arguments...]
VERSION:
v0.52.0
COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update)
charts sync releases from state file (helm upgrade --install)
charts DEPRECATED: sync releases from state file (helm upgrade --install)
diff diff releases from state file against env (helm diff)
template template releases from state file against env (helm template)
lint lint charts from state file (helm lint)
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 releases from state file (helm delete)
delete DEPRECATED: delete releases from state file (helm delete)
destroy deletes and then purges releases
test test releases from state file (helm test)
GLOBAL OPTIONS:
@@ -249,7 +270,15 @@ The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds tha
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
### destroy
The `helmfile destroy` sub-command deletes and purges all the releases defined in the manifests.
`helmfile --interactive destroy` instructs Helmfile to request your confirmation before actually deleting releases.
`destroy` basically runs `helm delete --purge` on all the targeted releases. If you don't want purging, use `helmfile delete` instead.
### delete (DEPRECATED)
The `helmfile delete` sub-command deletes all the releases defined in the manifests.
@@ -730,6 +759,12 @@ Use the [Helmfile Best Practices Guide](/docs/writing-helmfile.md) to write adva
- Default values
- Layering
We also have dedicated documentation on the following topics which might interest you:
- [Shared Configurations Across Teams](/docs/shared-configurations-across-teams.md)
Or join our friendly slack community in the [`#helmfile`](https://slack.sweetops.com) channel to ask questions and get help. Check out our [slack archive](https://archive.sweetops.com/helmfile/) for good examples of how others are using it.
## 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:
@@ -742,7 +777,7 @@ Please see #203 for more context.
## Running helmfile interactively
`helmfile --interactive [apply|delete]` requests confirmation from you before actually modifying your cluster.
`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster.
Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts.
-3
View File
@@ -83,9 +83,6 @@ func GetArgs(args string, state *state.HelmState) []string {
}
}
if state.HelmDefaults.TillerNamespace != "" {
argsMap.SetArg("--tiller-namespace", state.HelmDefaults.TillerNamespace, false)
}
if state.HelmDefaults.KubeContext != "" {
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
}
+152
View File
@@ -0,0 +1,152 @@
# Shared Configuration Across Teams
Assume you have a two or more teams, each works for a different internal or external service, like:
- Product 1
- Product 2
- Observability
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: product2-api
chart: saas-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
This works, but what if you wanted to a separate cluster per service to achieve smaller blast radius?
Let's start by creating a `helmfile.yaml` for each service.
`product1/helmfile.yaml`:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
`product2/helmfile.yaml`:
```yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
You will (of course!) notice this isn't DRY.
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
`observability/helmfile.yaml`:
```yaml
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
`product1/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
```
`product2/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
```
## Using sub-helmfile as a template
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
`shared/helmfile.yaml`:
```yaml
releases:
- name: product{{ env "PRODUCT_ID" }}-api
chart: product{{ env "PRODUCT_ID" }}-charts/api
# snip
- name: product{{ env "PRODUCT_ID" }}-web
chart: product{{ env "PRODUCT_ID" }}-charts/web
# snip
```
Then you only need one single product helmfile
`product/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
- ../shared/helmfile.yaml
```
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
```console
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
```
+4 -4
View File
@@ -103,16 +103,16 @@ func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
return err
}
func (helm *execer) ReleaseStatus(name string) error {
func (helm *execer) ReleaseStatus(name string, flags ...string) error {
helm.logger.Infof("Getting status %v", name)
out, err := helm.exec(append([]string{"status", name})...)
out, err := helm.exec(append([]string{"status", name}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) List(filter string) (string, error) {
func (helm *execer) List(filter string, flags ...string) (string, error) {
helm.logger.Infof("Listing releases matching %v", filter)
out, err := helm.exec(append([]string{"list", filter})...)
out, err := helm.exec(append([]string{"list", filter}, flags...)...)
helm.write(out)
return string(out), err
}
+2 -2
View File
@@ -14,9 +14,9 @@ type Interface interface {
TemplateRelease(chart string, flags ...string) error
Fetch(chart string, flags ...string) error
Lint(chart string, flags ...string) error
ReleaseStatus(name string) error
ReleaseStatus(name string, flags ...string) error
DeleteRelease(name string, flags ...string) error
TestRelease(name string, flags ...string) error
List(filter string) (string, error)
List(filter string, flags ...string) (string, error)
DecryptSecret(name string) (string, error)
}
+39 -2
View File
@@ -117,7 +117,7 @@ func main() {
},
{
Name: "charts",
Usage: "sync releases from state file (helm upgrade --install)",
Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
@@ -435,7 +435,7 @@ Do you really want to apply?
},
{
Name: "delete",
Usage: "delete releases from state file (helm delete)",
Usage: "DEPRECATED: delete releases from state file (helm delete)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
@@ -476,6 +476,43 @@ Do you really want to delete?
})
},
},
{
Name: "destroy",
Usage: "deletes and then purges releases",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
names := make([]string, len(state.Releases))
for i, r := range state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(helm, true)
}
return nil
})
},
},
{
Name: "test",
Usage: "test releases from state file (helm test)",
+21 -16
View File
@@ -110,6 +110,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f string) error {
content, err := a.readFile(f)
if err != nil {
@@ -153,8 +154,6 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
}
}
errs := []error{}
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for _, m := range st.Helmfiles {
@@ -170,26 +169,25 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
}
}
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
} else {
var err error
st, err = st.ExecuteTemplates()
if err != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, err)
}
var processed bool
processed, errs = converge(st, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
}
return clean(st, errs)
templated, tmplErr := st.ExecuteTemplates()
if tmplErr != nil {
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, tmplErr)
}
processed, errs := converge(templated, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
return clean(templated, errs)
})
if err != nil {
return err
}
if noMatchInHelmfiles {
return &NoMatchingHelmfileError{selectors: a.Selectors, env: a.Env}
}
return nil
}
@@ -204,13 +202,20 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
}
}
releaseNameCounts := map[string]int{}
type Key struct {
TillerNamespace, Name string
}
releaseNameCounts := map[Key]int{}
for _, r := range st.Releases {
releaseNameCounts[r.Name]++
tillerNamespace := st.HelmDefaults.TillerNamespace
if r.TillerNamespace != "" {
tillerNamespace = r.TillerNamespace
}
releaseNameCounts[Key{tillerNamespace, r.Name}]++
}
for name, c := range releaseNameCounts {
if c > 1 {
return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)}
return false, []error{fmt.Errorf("duplicate release \"%s\" found in \"%s\": there were %d releases named \"%s\" matching specified selector", name.Name, name.TillerNamespace, c, name.Name)}
}
}
+14 -1
View File
@@ -247,6 +247,8 @@ releases:
chart: stable/prometheus
`,
"/path/to/helmfile.d/b.yaml": `
helmDefaults:
tillerNamespace: zoo
releases:
- name: grafana
chart: stable/grafana
@@ -258,6 +260,16 @@ releases:
chart: charts/foo
labels:
duplicated: yes
- name: bar
chart: charts/foo
tillerNamespace: bar1
labels:
duplicatedOK: yes
- name: bar
chart: charts/foo
tillerNamespace: bar2
labels:
duplicatedOK: yes
`,
}
@@ -272,7 +284,8 @@ releases:
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
{label: "name", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
// See https://github.com/roboll/helmfile/issues/193
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found: there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
}
for _, testcase := range testcases {
+12
View File
@@ -114,6 +114,18 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm
state.Env = *e
state.readFile = c.readFile
state.removeFile = os.Remove
state.fileExists = func(path string) (bool, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
return &state, nil
}
+4
View File
@@ -67,3 +67,7 @@ func (r ReleaseSpec) Clone() (*ReleaseSpec, error) {
return &deserialized, nil
}
func (r ReleaseSpec) Desired() bool {
return r.Installed == nil || *r.Installed
}
+169 -34
View File
@@ -47,6 +47,9 @@ type HelmState struct {
readFile func(string) ([]byte, error)
removeFile func(string) error
fileExists func(string) (bool, error)
runner helmexec.Runner
}
@@ -68,6 +71,11 @@ type HelmSpec struct {
Force bool `yaml:"force"`
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
Atomic bool `yaml:"atomic"`
TLS bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert"`
TLSKey string `yaml:"tlsKey"`
TLSCert string `yaml:"tlsCert"`
}
// RepositorySpec that defines values for a helm repo
@@ -121,6 +129,13 @@ type ReleaseSpec struct {
ValuesPathPrefix string `yaml:"valuesPathPrefix"`
TillerNamespace string `yaml:"tillerNamespace"`
TLS *bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert"`
TLSKey string `yaml:"tlsKey"`
TLSCert string `yaml:"tlsCert"`
// generatedValues are values that need cleaned up on exit
generatedValues []string
}
@@ -187,7 +202,14 @@ type syncPrepareResult struct {
// SyncReleases wrapper for executing helm upgrade on the releases
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
releases := st.Releases
releases := []*ReleaseSpec{}
for i, _ := range st.Releases {
if !st.Releases[i].Desired() {
continue
}
releases = append(releases, &st.Releases[i])
}
numReleases := len(releases)
jobs := make(chan *ReleaseSpec, numReleases)
results := make(chan syncPrepareResult, numReleases)
@@ -200,7 +222,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
numReleases,
func() {
for i := 0; i < numReleases; i++ {
jobs <- &releases[i]
jobs <- releases[i]
}
close(jobs)
},
@@ -221,8 +243,11 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
errs = append(errs, &ReleaseError{release, err})
}
if _, err := os.Stat(valfile); os.IsNotExist(err) {
ok, err := st.fileExists(valfile)
if err != nil {
errs = append(errs, &ReleaseError{release, err})
} else if !ok {
errs = append(errs, &ReleaseError{release, fmt.Errorf("file does not exist: %s", valfile)})
}
flags = append(flags, "--values", valfile)
}
@@ -252,8 +277,8 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
return res, errs
}
func isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, error) {
out, err := helm.List("^" + release.Name + "$")
func (st *HelmState) isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, error) {
out, err := helm.List("^"+release.Name+"$", st.tillerFlags(&release)...)
if err != nil {
return false, err
} else if out != "" {
@@ -266,8 +291,8 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
detected := []*ReleaseSpec{}
for i, _ := range st.Releases {
release := st.Releases[i]
if release.Installed != nil && !*release.Installed {
installed, err := isReleaseInstalled(helm, release)
if !release.Desired() {
installed, err := st.isReleaseInstalled(helm, release)
if err != nil {
return nil, err
} else if installed {
@@ -304,8 +329,8 @@ func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []st
flags := prep.flags
chart := normalizeChart(st.basePath, release.Chart)
var relErr *ReleaseError
if release.Installed != nil && !*release.Installed {
installed, err := isReleaseInstalled(helm, *release)
if !release.Desired() {
installed, err := st.isReleaseInstalled(helm, *release)
if err != nil {
relErr = &ReleaseError{release, err}
} else if installed {
@@ -439,6 +464,10 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
}
for _, release := range st.Releases {
if !release.Desired() {
continue
}
flags, err := st.flagsForTemplate(helm, &release)
if err != nil {
errs = append(errs, err)
@@ -495,6 +524,10 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
}
for _, release := range st.Releases {
if !release.Desired() {
continue
}
flags, err := st.flagsForLint(helm, &release)
if err != nil {
errs = append(errs, err)
@@ -550,12 +583,14 @@ type diffPrepareResult struct {
}
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) {
releases := []ReleaseSpec{}
for _, r := range st.Releases {
if r.Installed == nil || *r.Installed {
releases = append(releases, r)
releases := []*ReleaseSpec{}
for i, _ := range st.Releases {
if !st.Releases[i].Desired() {
continue
}
releases = append(releases, &st.Releases[i])
}
numReleases := len(releases)
jobs := make(chan *ReleaseSpec, numReleases)
results := make(chan diffPrepareResult, numReleases)
@@ -568,7 +603,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
numReleases,
func() {
for i := 0; i < numReleases; i++ {
jobs <- &releases[i]
jobs <- releases[i]
}
close(jobs)
},
@@ -697,18 +732,31 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec) error {
return helm.ReleaseStatus(release.Name)
if !release.Desired() {
return nil
}
flags := []string{}
flags = st.appendTillerFlags(flags, &release)
return helm.ReleaseStatus(release.Name, flags...)
})
}
// DeleteReleases wrapper for executing helm delete on the releases
func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error {
return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error {
if !release.Desired() {
return nil
}
flags := []string{}
if purge {
flags = append(flags, "--purge")
}
installed, err := isReleaseInstalled(helm, release)
flags = st.appendTillerFlags(flags, &release)
installed, err := st.isReleaseInstalled(helm, release)
if err != nil {
return err
}
@@ -722,11 +770,17 @@ func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error
// TestReleases wrapper for executing helm test on the releases
func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int, concurrency int) []error {
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec) error {
if !release.Desired() {
return nil
}
flags := []string{}
if cleanup {
flags = append(flags, "--cleanup")
}
flags = append(flags, "--timeout", strconv.Itoa(timeout))
flags = st.appendTillerFlags(flags, &release)
return helm.TestRelease(release.Name, flags...)
})
}
@@ -737,7 +791,7 @@ func (st *HelmState) Clean() []error {
for _, release := range st.Releases {
for _, value := range release.generatedValues {
err := os.Remove(value)
err := st.removeFile(value)
if err != nil {
errs = append(errs, err)
}
@@ -929,6 +983,47 @@ func findChartDirectory(topLevelDir string) (string, error) {
return topLevelDir, errors.New("No Chart.yaml found")
}
func (st *HelmState) appendTillerFlags(flags []string, release *ReleaseSpec) []string {
adds := st.tillerFlags(release)
for _, a := range adds {
flags = append(flags, a)
}
return flags
}
func (st *HelmState) tillerFlags(release *ReleaseSpec) []string {
flags := []string{}
if release.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", release.TillerNamespace)
} else if st.HelmDefaults.TillerNamespace != "" {
flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
}
if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
flags = append(flags, "--tls")
}
if release.TLSKey != "" {
flags = append(flags, "--tls-key", release.TLSKey)
} else if st.HelmDefaults.TLSKey != "" {
flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey)
}
if release.TLSCert != "" {
flags = append(flags, "--tls-cert", release.TLSCert)
} else if st.HelmDefaults.TLSCert != "" {
flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert)
}
if release.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", release.TLSCACert)
} else if st.HelmDefaults.TLSCACert != "" {
flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
}
return flags
}
func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Version != "" {
@@ -967,6 +1062,8 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--atomic")
}
flags = st.appendTillerFlags(flags, release)
common, err := st.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
@@ -995,6 +1092,8 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec)
flags = append(flags, "--devel")
}
flags = st.appendTillerFlags(flags, release)
common, err := st.namespaceAndValuesFlags(helm, release)
if err != nil {
return nil, err
@@ -1020,23 +1119,25 @@ func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
return r.RenderToBytes(path)
}
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
}
for _, value := range release.Values {
func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingFileHandler *string) ([]string, error) {
generatedFiles := []string{}
for _, value := range values {
switch typedValue := value.(type) {
case string:
path := st.normalizePath(release.ValuesPathPrefix + typedValue)
path := st.normalizePath(typedValue)
if _, err := os.Stat(path); os.IsNotExist(err) {
if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" {
return nil, err
} else if *release.MissingFileHandler == "Warn" {
ok, err := st.fileExists(path)
if err != nil {
return nil, err
}
if !ok {
if missingFileHandler == nil || *missingFileHandler == "Error" {
return nil, fmt.Errorf("file does not exist: %s", path)
} else if *missingFileHandler == "Warn" {
st.logger.Warnf("skipping missing values file \"%s\"", path)
continue
} else if *release.MissingFileHandler == "Info" {
} else if *missingFileHandler == "Info" {
st.logger.Infof("skipping missing values file \"%s\"", path)
continue
} else {
@@ -1060,8 +1161,7 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
}
st.logger.Debugf("successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes))
flags = append(flags, "--values", valfile.Name())
generatedFiles = append(generatedFiles, valfile.Name())
case map[interface{}]interface{}:
valfile, err := ioutil.TempFile("", "values")
if err != nil {
@@ -1073,14 +1173,49 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
if err := encoder.Encode(typedValue); err != nil {
return nil, err
}
release.generatedValues = append(release.generatedValues, valfile.Name())
flags = append(flags, "--values", valfile.Name())
generatedFiles = append(generatedFiles, valfile.Name())
default:
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
}
}
return generatedFiles, nil
}
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
}
values := []interface{}{}
for _, v := range release.Values {
switch typedValue := v.(type) {
case string:
path := st.normalizePath(release.ValuesPathPrefix + typedValue)
values = append(values, path)
default:
values = append(values, v)
}
}
generatedFiles, err := st.generateTemporaryValuesFiles(values, release.MissingFileHandler)
if err != nil {
return nil, err
}
for _, f := range generatedFiles {
flags = append(flags, "--values", f)
}
release.generatedValues = append(release.generatedValues, generatedFiles...)
for _, value := range release.Secrets {
path := st.normalizePath(release.ValuesPathPrefix + value)
if _, err := os.Stat(path); os.IsNotExist(err) {
ok, err := st.fileExists(path)
if err != nil {
return nil, err
}
if !ok {
if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" {
return nil, err
} else if *release.MissingFileHandler == "Warn" {
+592 -10
View File
@@ -8,6 +8,7 @@ import (
"errors"
"strings"
"fmt"
"github.com/roboll/helmfile/helmexec"
)
@@ -129,6 +130,10 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
}
}
func boolValue(v bool) *bool {
return &v
}
func TestHelmState_flagsForUpgrade(t *testing.T) {
enable := true
disable := false
@@ -424,6 +429,79 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "tiller",
defaults: HelmSpec{},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
TLS: boolValue(true),
TillerNamespace: "tiller-system",
TLSKey: "key.pem",
TLSCert: "cert.pem",
TLSCACert: "ca.pem",
},
want: []string{
"--version", "0.1",
"--tiller-namespace", "tiller-system",
"--tls",
"--tls-key", "key.pem",
"--tls-cert", "cert.pem",
"--tls-ca-cert", "ca.pem",
},
},
{
name: "tiller-override-defaults",
defaults: HelmSpec{
TLS: false,
TillerNamespace: "a",
TLSKey: "b.pem",
TLSCert: "c.pem",
TLSCACert: "d.pem",
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
TLS: boolValue(true),
TillerNamespace: "tiller-system",
TLSKey: "key.pem",
TLSCert: "cert.pem",
TLSCACert: "ca.pem",
},
want: []string{
"--version", "0.1",
"--tiller-namespace", "tiller-system",
"--tls",
"--tls-key", "key.pem",
"--tls-cert", "cert.pem",
"--tls-ca-cert", "ca.pem",
},
},
{
name: "tiller-from-defaults",
defaults: HelmSpec{
TLS: true,
TillerNamespace: "tiller-system",
TLSKey: "key.pem",
TLSCert: "cert.pem",
TLSCACert: "ca.pem",
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
},
want: []string{
"--version", "0.1",
"--tiller-namespace", "tiller-system",
"--tls",
"--tls-key", "key.pem",
"--tls-cert", "cert.pem",
"--tls-ca-cert", "ca.pem",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -553,10 +631,18 @@ func Test_normalizeChart(t *testing.T) {
// mocking helmexec.Interface
type listKey struct {
filter string
flags string
}
type mockHelmExec struct {
charts []string
repo []string
releases []mockRelease
deleted []mockRelease
lists map[listKey]string
diffed []mockRelease
}
type mockRelease struct {
@@ -602,20 +688,22 @@ func (helm *mockHelmExec) SyncRelease(name, chart string, flags ...string) error
return nil
}
func (helm *mockHelmExec) DiffRelease(name, chart string, flags ...string) error {
helm.diffed = append(helm.diffed, mockRelease{name: name, flags: flags})
return nil
}
func (helm *mockHelmExec) ReleaseStatus(release string) error {
func (helm *mockHelmExec) ReleaseStatus(release string, flags ...string) error {
if strings.Contains(release, "error") {
return errors.New("error")
}
helm.releases = append(helm.releases, mockRelease{name: release, flags: []string{}})
helm.releases = append(helm.releases, mockRelease{name: release, flags: flags})
return nil
}
func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error {
helm.deleted = append(helm.deleted, mockRelease{name: name, flags: flags})
return nil
}
func (helm *mockHelmExec) List(filter string) (string, error) {
return "", nil
func (helm *mockHelmExec) List(filter string, flags ...string) (string, error) {
return helm.lists[listKey{filter: filter, flags: strings.Join(flags, "")}], nil
}
func (helm *mockHelmExec) DecryptSecret(name string) (string, error) {
return "", nil
@@ -726,6 +814,18 @@ func TestHelmState_SyncReleases(t *testing.T) {
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{}}},
},
{
name: "with tiller args",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
TillerNamespace: "tillerns",
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
},
{
name: "escaped values",
releases: []ReleaseSpec{
@@ -806,6 +906,298 @@ func TestHelmState_SyncReleases(t *testing.T) {
}
}
func TestHelmState_DiffReleases(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *mockHelmExec
wantReleases []mockRelease
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{}}},
},
{
name: "with tiller args",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
TillerNamespace: "tillerns",
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
},
{
name: "escaped values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "someList",
Value: "a,b,c",
},
{
Name: "json",
Value: "{\"name\": \"john\"}",
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
},
{
name: "set single value from file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo",
Value: "FOO",
},
{
Name: "bar",
File: "path/to/bar",
},
{
Name: "baz",
Value: "BAZ",
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
},
{
name: "set single array value in an array",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []string{
"A",
"B",
},
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
}
_, err := state.DiffReleases(tt.helm, []string{}, 1, false, false, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(tt.helm.diffed, tt.wantReleases) {
t.Errorf("HelmState.DiffReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.wantReleases)
}
})
}
}
func TestHelmState_SyncReleasesCleanup(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *mockHelmExec
expectedNumRemovedFiles int
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 0,
},
{
name: "inline values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []interface{}{
map[interface{}]interface{}{
"someList": "a,b,c",
},
},
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 1,
},
{
name: "inline values and values file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []interface{}{
map[interface{}]interface{}{
"someList": "a,b,c",
},
"someFile",
},
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
logger: logger,
readFile: func(f string) ([]byte, error) {
if f != "someFile" {
return nil, fmt.Errorf("unexpected file to read: %s", f)
}
someFileContent := []byte(`foo: bar
`)
return someFileContent, nil
},
removeFile: func(f string) error {
numRemovedFiles += 1
return nil
},
fileExists: func(f string) (bool, error) {
return true, nil
},
}
if errs := state.SyncReleases(tt.helm, []string{}, 1); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if errs := state.Clean(); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if numRemovedFiles != tt.expectedNumRemovedFiles {
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
}
})
}
}
func TestHelmState_DiffReleasesCleanup(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *mockHelmExec
expectedNumRemovedFiles int
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 0,
},
{
name: "inline values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []interface{}{
map[interface{}]interface{}{
"someList": "a,b,c",
},
},
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 1,
},
{
name: "inline values and values file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []interface{}{
map[interface{}]interface{}{
"someList": "a,b,c",
},
"someFile",
},
},
},
helm: &mockHelmExec{},
expectedNumRemovedFiles: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
logger: logger,
readFile: func(f string) ([]byte, error) {
if f != "someFile" {
return nil, fmt.Errorf("unexpected file to read: %s", f)
}
someFileContent := []byte(`foo: bar
`)
return someFileContent, nil
},
removeFile: func(f string) error {
numRemovedFiles += 1
return nil
},
fileExists: func(f string) (bool, error) {
return true, nil
},
}
if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if errs := state.Clean(); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if numRemovedFiles != tt.expectedNumRemovedFiles {
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
}
})
}
}
func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{
basePath: "/src",
@@ -870,12 +1262,62 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
helm: &mockHelmExec{},
wantErr: true,
},
{
name: "complain missing values file for desired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []interface{}{
"foo.yaml",
},
},
},
helm: &mockHelmExec{},
wantErr: true,
},
{
name: "should not complain missing values file for undesired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []interface{}{
"foo.yaml",
},
Installed: boolValue(false),
},
},
helm: &mockHelmExec{},
wantErr: false,
},
{
name: "with tiller args",
releases: []ReleaseSpec{
{
Name: "releaseA",
TillerNamespace: "tillerns",
},
},
helm: &mockHelmExec{},
want: []mockRelease{{"releaseA", []string{"--tiller-namespace", "tillerns"}}},
},
}
for _, tt := range tests {
i := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
fileExists: func(f string) (bool, error) {
if f != "foo.yaml" {
return false, fmt.Errorf("unexpected file: %s", f)
}
return true, nil
},
readFile: func(f string) ([]byte, error) {
if f != "foo.yaml" {
return nil, fmt.Errorf("unexpected file: %s", f)
}
return []byte{}, nil
},
}
errs := state.ReleaseStatuses(tt.helm, 1)
if (errs != nil) != tt.wantErr {
@@ -892,12 +1334,13 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
tests := []struct {
name string
cleanup bool
releases []ReleaseSpec
helm *mockHelmExec
want []mockRelease
wantErr bool
name string
cleanup bool
releases []ReleaseSpec
helm *mockHelmExec
want []mockRelease
wantErr bool
tillerNamespace string
}{
{
name: "happy path",
@@ -930,6 +1373,17 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
helm: &mockHelmExec{},
wantErr: true,
},
{
name: "with tiller args",
releases: []ReleaseSpec{
{
Name: "releaseA",
TillerNamespace: "tillerns",
},
},
helm: &mockHelmExec{},
want: []mockRelease{{"releaseA", []string{"--timeout", "1", "--tiller-namespace", "tillerns"}}},
},
}
for _, tt := range tests {
i := func(t *testing.T) {
@@ -996,3 +1450,131 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
t.Run(tt.name, i)
}
}
func TestHelmState_Delete(t *testing.T) {
tests := []struct {
name string
deleted []mockRelease
wantErr bool
desired *bool
installed bool
purge bool
flags string
tillerNamespace string
}{
{
name: "desired and installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []mockRelease{{"releaseA", []string{}}},
},
{
name: "desired(default) and installed (purge=false)",
wantErr: false,
desired: nil,
installed: true,
purge: false,
deleted: []mockRelease{{"releaseA", []string{}}},
},
{
name: "desired and installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: true,
deleted: []mockRelease{{"releaseA", []string{"--purge"}}},
},
{
name: "desired but not installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: false,
deleted: []mockRelease{},
},
{
name: "desired but not installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: true,
deleted: []mockRelease{},
},
{
name: "installed but filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: false,
deleted: []mockRelease{},
},
{
name: "installed but filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: true,
deleted: []mockRelease{},
},
{
name: "not installed, and filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: false,
deleted: []mockRelease{},
},
{
name: "not installed, and filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: true,
deleted: []mockRelease{},
},
{
name: "with tiller args",
wantErr: false,
desired: nil,
installed: true,
purge: true,
tillerNamespace: "tillerns",
flags: "--tiller-namespacetillerns",
deleted: []mockRelease{{"releaseA", []string{"--purge", "--tiller-namespace", "tillerns"}}},
},
}
for _, tt := range tests {
i := func(t *testing.T) {
release := ReleaseSpec{
Name: "releaseA",
Installed: tt.desired,
TillerNamespace: tt.tillerNamespace,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
Releases: releases,
logger: logger,
}
helm := &mockHelmExec{
lists: map[listKey]string{},
deleted: []mockRelease{},
}
if tt.installed {
helm.lists[listKey{filter: "^releaseA$", flags: tt.flags}] = "releaseA"
}
errs := state.DeleteReleases(helm, tt.purge)
if (errs != nil) != tt.wantErr {
t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
if !reflect.DeepEqual(tt.deleted, helm.deleted) {
t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.deleted)
}
}
t.Run(tt.name, i)
}
}
+16 -3
View File
@@ -110,11 +110,11 @@ func TestRenderTemplate_Defaulting(t *testing.T) {
}
}
func renderTemplateToString(s string) (string, error) {
func renderTemplateToString(s string, data ...interface{}) (string, error) {
ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
}}
tplString, err := ctx.RenderTemplateToBuffer(s)
tplString, err := ctx.RenderTemplateToBuffer(s, data...)
if err != nil {
return "", err
}
@@ -125,6 +125,7 @@ func Test_renderTemplateToString(t *testing.T) {
type args struct {
s string
envs map[string]string
data interface{}
}
tests := []struct {
name string
@@ -177,6 +178,18 @@ func Test_renderTemplateToString(t *testing.T) {
want: "7",
wantErr: false,
},
{
name: "get",
args: args{
s: `{{ . | get "Foo" }}, {{ . | get "Bar" "2" }}`,
envs: map[string]string{},
data: map[string]interface{}{
"Foo": "1",
},
},
want: "1, 2",
wantErr: false,
},
{
name: "env var not set",
args: args{
@@ -225,7 +238,7 @@ func Test_renderTemplateToString(t *testing.T) {
t.Error("renderTemplateToString() could not set env var for testing")
}
}
got, err := renderTemplateToString(tt.args.s)
got, err := renderTemplateToString(tt.args.s, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
+39 -8
View File
@@ -14,36 +14,67 @@ func (e *noValueError) Error() string {
return e.msg
}
func get(path string, obj interface{}) (interface{}, error) {
func get(path string, varArgs ...interface{}) (interface{}, error) {
var defSet bool
var def interface{}
var obj interface{}
switch len(varArgs) {
case 1:
defSet = false
def = nil
obj = varArgs[0]
case 2:
defSet = true
def = varArgs[0]
obj = varArgs[1]
default:
return nil, fmt.Errorf("unexpected number of args pased to the template function get(path, [def, ]obj): expected 1 or 2, got %d, args was %v", len(varArgs), varArgs)
}
if path == "" {
return obj, nil
}
keys := strings.Split(path, ".")
var v interface{}
var ok bool
switch typedObj := obj.(type) {
case map[string]interface{}:
v, ok := typedObj[keys[0]]
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
return get(strings.Join(keys[1:], "."), v)
case map[interface{}]interface{}:
v, ok := typedObj[keys[0]]
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
return get(strings.Join(keys[1:], "."), v)
default:
maybeStruct := reflect.ValueOf(typedObj)
if maybeStruct.NumField() < 1 {
if maybeStruct.Kind() != reflect.Struct {
return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])}
} else if maybeStruct.NumField() < 1 {
return nil, &noValueError{fmt.Sprintf("no accessible struct fields for key \"%s\"", keys[0])}
}
f := maybeStruct.FieldByName(keys[0])
if !f.IsValid() {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)}
}
v := f.Interface()
return get(strings.Join(keys[1:], "."), v)
v = f.Interface()
}
if defSet {
return get(strings.Join(keys[1:], "."), def, v)
}
return get(strings.Join(keys[1:], "."), v)
}
func getOrNil(path string, o interface{}) (interface{}, error) {
+37
View File
@@ -4,6 +4,9 @@ import (
"testing"
)
type EmptyStruct struct {
}
func TestGetStruct(t *testing.T) {
type Foo struct{ Bar string }
@@ -23,6 +26,12 @@ func TestGetStruct(t *testing.T) {
if err == nil {
t.Errorf("expected error but was not occurred")
}
_, err = get("foo", EmptyStruct{})
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetMap(t *testing.T) {
@@ -44,6 +53,34 @@ func TestGetMap(t *testing.T) {
}
}
func TestGet_Default(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{}, "foo": 1}
v1, err := get("Foo.Bar", "Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := get("Baz", "Baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != "Baz" {
t.Errorf("unexpected value for path Baz in %v: expected=Baz, actual=%v", obj, v2)
}
_, err = get("foo.Bar", "fooBar", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetOrNilStruct(t *testing.T) {
type Foo struct{ Bar string }