Compare commits

...
532 Commits
Author SHA1 Message Date
Radon Rosborough 77241607fa Fix Helmfile swallowing secrets decryption errors (#1849) 2021-05-14 09:19:32 +09:00
Quan TRAN f38958d6d7 Fix cache path issue on Windows (#1846) 2021-05-13 09:00:43 +09:00
Yusuke Kuoka 212e6acf23 Fix helmfile-lint not to stop on fisrt error (#1836)
Fixes #1826
2021-05-09 13:33:36 +09:00
Yusuke Kuoka efa404e275 Make --skip-needs=true the default option (#1835)
Ref https://github.com/roboll/helmfile/issues/1830#issuecomment-835632716
Ref #1692
Follow-up for #1772
2021-05-09 12:09:58 +09:00
Yusuke Kuoka 794ab9fc66 Fix lint and status not to fail with selector and needs (#1834)
Fixes #1830
2021-05-08 21:23:09 +09:00
Yusuke Kuoka 48bd5c0979 Update go.sum for successful build 2021-05-08 08:49:57 +00:00
Quan TRAN e39a0b3639 bump Go to 1.16.3 and cover helm 3.5.4 (#1825)
* bump go to 1.16.3

* add helm 3.5.4 to integration tests
2021-05-08 17:37:58 +09:00
katsew 58954109fb fix kubeContext does not globally defaulting its value (#1829)
Ref #1810
2021-05-08 17:35:46 +09:00
Yusuke Kuoka cb7fa6bf63 Bump vals to 0.14.0 (#1832)
This release includes new `ref+tfstates3://` and `ref+tfstategs://` backends for reading terraform states stored in s3 and google cloud storage, and features a new aws-sdk-go that supports AWS SSO, and the fix to allow referencing a local tfstate file by the absolute path.

See https://github.com/variantdev/vals/releases/tag/v0.14.0 for all the changes.
2021-05-08 17:33:37 +09:00
Yusuke Kuoka 44fb68b7e0 Use the incubator chart repo instead of chartcenter (#1833)
As ChartCenter has been shut down.

Ref https://github.com/roboll/helmfile/pull/1829#issuecomment-833330609
2021-05-08 17:33:24 +09:00
Quan TRAN ded0f1049a Fix filepath handling on Windows (#1754) 2021-05-02 09:22:25 +09:00
Yusuke Kuoka 08db073958 Fix broken selector and DAG calculation logic after --{include,skip}-needs addition with correct Release IDs (#1823)
#1772 broke `--selector` with `needs` in many ways.

The two biggest problems I've encountered were:

- duplicate releases even if you've provided a proper `selector` to deduplicate
- sync/deletion ordering broken when you have `needs`

For the first issue, we had to update `getSelectedReleases` function to also calculate the "selected releases and releases needed by the selected releases", and use that to calculate the DAG. That should have been done in #1772.

The latter started happening after I've fixed the first issue. The source of the issue was that `needs` turned out to be ambiguous in a few cases.

Previously, `needs: ["foo/bar"]` had two meanings. One for "needs release bar in kubecontext foo", another for "needs release bar in namespace foo".

Moreover, `needs: ["foo/bar/baz"]` had three meanings.

- `needs release baz in tiller namespace foo and namespace baz`
- `needs release baz in namespace bar in kubecontext foo`
- `needs release baz in tiller namespace bar in kubecontext foo`.

Especially, the first meaning doesn't make sense at all. Helm 2 solely use tillerNamespace for namespacing the release and Helm 3 uses namespace for that.

This fix sorts all the bugs and issues I've found so far around that, by changing the meanings of the above two examples as follows:

- `foo/bar` means `namespace=foo,name=bar` for Helm 3 and `tillerNamespace=foo,name=bar` for Helm 2
  - `needs release bar in kubecontext foo` is now `foo//bar`. Notice the extra `/` between `foo` and `bar`.
- `foo/bar/baz` means `kubecontext=foo,namespace=bar,name=baz` for Helm 3 and `kubecontext=foo,tillerNamespace=bar,name=baz` in Helm 2

Fixes #1818
2021-05-01 21:59:25 +09:00
Yusuke Kuoka de8644a504 Fix --selector results to correctly deduplicate releases (#1822)
Fixes #1818
2021-05-01 15:06:07 +09:00
katsew f28ad5af23 Fix defaulting and overriding kubeContext (#1814)
Ref #1810

* fix defaulting and overriding kubeContext

* make defaulting kubeContext side-effect free

* checkDuplicates should check releases with overriden values
2021-05-01 12:42:29 +09:00
dudicoco 77ee239db2 Lock helm plugin versions in Dockerfile (#1821) 2021-05-01 12:26:54 +09:00
d9e15754b7 Add documentation about re-using the environment state from parent in sub-helmfile (#1808)
Ref #762

Co-authored-by: Vladimir Avdoshka <vavdoshka@kyriba.com>
Co-authored-by: Quan TRAN <itscaro@users.noreply.github.com>
2021-05-01 12:26:00 +09:00
Yusuke Kuoka dabd7ad99f Fix repo sync to work on repos duplicated between helm v2 and v3 (#1816)
Fixes #1815
2021-04-30 09:27:23 +09:00
Yusuke Kuoka 204f78c8ff Fix --set and --set-file flags are not passed to chartify (#1803)
Fixes #1681
Ref https://github.com/variantdev/chartify/commit/286ed08a273d4229989aea5ac5c901612b43944c
2021-04-24 22:27:53 +09:00
Yusuke Kuoka 59b91a45a0 Fix adhoc dependencies not working with remote chart (#1802)
Fixes #1680

Ref https://github.com/variantdev/chartify/commit/8ff30374cb2a65af5167f3d47df81fe45d37b01d
2021-04-24 21:11:07 +09:00
Yusuke Kuoka 5a3bd7d649 Fix destory, delete, and test to work with chartify on local chart with dependencies that are not yet downloaded (#1801)
Fixes #1800
2021-04-24 20:50:16 +09:00
Yusuke Kuoka ce6a621414 Bump chartify to 0.8.3 to fix it not to break when two or more releases sharing the same chart and the namespace, but kubeContext (#1799)
Fixes #1695
2021-04-24 19:03:13 +09:00
Yusuke Kuoka 20f6c68108 Fix code format 2021-04-24 07:47:27 +00:00
astorathandAndrey Tuzhilin 3367822e89 feat: Move chartify logs to Debug (#1794)
Co-authored-by: Andrey Tuzhilin <andrey@zelf.co>
2021-04-24 16:06:21 +09:00
katsew 8702639510 fix: spec.KubeContext does not fallback to HelmDefaults.KubeContext (#1789)
* fix: spec.KubeContext does not fallback to HelmDefaults.KubeContext

Fixes #1782

* Add testcases to test without default kubecontext
2021-04-24 16:02:58 +09:00
katsew 41cbc25c35 fix: can't evaluate field KubeContext in type state.releaseTemplateDataRelease (#1797)
Fixes #1796
2021-04-24 15:28:20 +09:00
astorathandAndrey Tuzhilin 83189dbad3 Enable helmfile-diff to pass the output format to helm-diff (#1784)
* tests: fix vagrant test run

* feat: added an option to specify the different diff output format

* renamed diff-output to output

* renamed diff-output to output

Co-authored-by: Andrey Tuzhilin <andrey@zelf.co>
2021-04-24 08:07:24 +09:00
Yusuke Kuoka 28ade19419 Bump chartify to 0.8.2 (#1793)
* Bump chartify to 0.8.2

This version fixes charitfy not to fail when you used the combination of (1)helm 3 and (2)strategicMergePatches/jsonPatches/transformers etc that triggers chartify on (3)a chart that contains CRDs.

See https://github.com/roboll/helmfile/issues/1778#issuecomment-824451990 for details of the issue.

The chartify-side of this fix is https://github.com/variantdev/chartify/commit/55b23f9e9d43ae1105a536a659fa35f004806b2a

Fixes #1778
2021-04-23 09:53:52 +09:00
Yujun Zhang 378ae4ad72 Fix typo (#1788) 2021-04-23 09:27:37 +09:00
Yusuke Kuoka 30e0356ad6 Bump chartify to 0.8.1 (#1787)
This version fixes that chartify not to fail when the target chart misses `templates` directory.
2021-04-23 09:16:59 +09:00
Yusuke Kuoka eabda4cf28 Fix delete on release of uninstalling status (#1786)
* Fix helmfile destroy/delete not deleting `uninstalling` release

Ref https://github.com/roboll/helmfile/issues/1750#issuecomment-823677950

* Cover helm3 in helmfile-destroy test
2021-04-21 09:39:14 +09:00
Yusuke Kuoka 5617b593c8 Add test for helmfile-destroy with selector (#1785)
* Add test for helmfile-destroy with selector

I was curious if destroy with a selector is working at all while reading https://github.com/roboll/helmfile/issues/1750#issuecomment-823521572. So I added this test for verification. From the test result, it is working, though.

* Add one more test case for destroying disabled but installed release
2021-04-21 09:07:08 +09:00
Yusuke Kuoka 5d43b30a7c Add --{include,skip}-needs to various helmfile commands (#1772)
* Add --{include,skip}-needs to helmfile-sync and helmfile-apply

* Add --include-needs to helmfile-template

* Add TODO related to #1018

* Add a few new test files to cover new functionalities

* Update apply test to incorporate the change that the destroy and sync steps target affected releases only
2021-04-20 23:06:51 +09:00
K1Hyve d6db4b53e7 Added The Hyve to USERS.md (#1779) 2021-04-20 13:29:44 +09:00
Yusuke Kuoka 368dd13e16 Fix destroy not reversing the order of releases (#1777)
Fixes #1775
2021-04-20 13:27:45 +09:00
Yusuke Kuoka ae942c5288 Fix --skip-crds not working with chartify (#1774)
Follow-up for #1771
Ref #1770
Ref https://github.com/roboll/helmfile/pull/1771#issuecomment-817399338
2021-04-13 09:09:58 +09:00
Yusuke Kuoka a111e89b27 Add --skip-crds to helmfile sync and helmfile apply (#1771)
Resolves #1770
2021-04-11 10:38:23 +09:00
Yusuke Kuoka 589b26aaad Fix some hook log not honoring log level (#1769)
Fixes #1742
2021-04-10 16:50:42 +09:00
Yusuke Kuoka 1d7608f42c Fix helmfile repos to not fail with forceNamespace
Fixes #1752
2021-04-10 16:26:40 +09:00
Yusuke Kuoka efad26641d Add documentation about adhoc chart dependencies 2021-04-10 16:09:27 +09:00
Yusuke Kuoka 81b04fcb8b Add support for adhoc dependency to local chart
Helmfile has been providing a feature called "adhoc chart dependency" that basially enabled you to add Chart.yaml `dependencies` entry adhocly without forking or modifying the chart.

It was missing the support for using a local chart as the adhoc dependency. This patch adds that.

Usage:

`releases[].dependencies[].chart` is enhanced to accept the fs path to the local chart:

```
releases:
- name: foo
  chart: ./path/to/foo
  dependencies:
  - chart: ./path/to/bar
```

Resolves #1762
2021-04-10 16:09:27 +09:00
Yusuke Kuoka 74cd2bc147 Enable helmfile-deps to limit helm-dep-up by selector (#1764)
Ref https://github.com/roboll/helmfile/issues/809#issuecomment-814423653

This is not what had been requested originally in #809 but anyway- This enables you to limit helm-dep-up run by helmfile-deps by selector. For example, `helmfile -l name=foo deps` should result in only the release named `foo` to be helm-dep-uped.
2021-04-09 09:49:57 +09:00
Yusuke Kuoka 2ff06a8abd Fix chartify to take --include-crds into account (#1761)
Fixes #1760
2021-04-08 10:06:34 +09:00
Yusuke Kuoka d703e17239 Fix chartify not to fail on a chart with dependencies (#1759)
Manually verified to work by using the following example from @damoon (https://github.com/roboll/helmfile/issues/1655#issuecomment-801122431):

```
repositories:
  - name: prometheus-community
    url: https://prometheus-community.github.io/helm-charts
releases:
- name: metrics
  namespace: monitoring
  chart: prometheus-community/kube-prometheus-stack
  version: 13.13.0
  jsonPatches:
    - target:
        version: v1
        kind: Service
        name: metrics-kube-prometheus-st-kube-etcd
        namespace: kube-system
      patch:
        - op: replace
          path: /spec/ports/0/port
          value: "2381"
        - op: replace
          path: /spec/ports/0/targetPort
          value: "2381"
```

Before this fix:
```
$ helmfile template
(snip)
COMBINED OUTPUT:
  Error: found in Chart.yaml, but missing in charts/ directory: kube-state-metrics, prometheus-node-exporter, grafana
```

After this fix:

```
$ helmfile template | grep 2381
(snip)
Removing /tmp/chartify718204429/monitoring-metrics-f5479c8b8/kube-prometheus-stack/strategicmergepatches
Removing /tmp/chartify718204429/monitoring-metrics-f5479c8b8/kube-prometheus-stack/kustomization.yaml
Templating release=metrics, chart=/tmp/chartify718204429/monitoring-metrics-f5479c8b8/kube-prometheus-stack
    port: "2381"
    targetPort: "2381"
```

Fixes #1655

Ref https://github.com/variantdev/chartify/commit/07c3054d66021fa190464a998ade18607312c715
2021-04-08 07:03:18 +09:00
Yusuke Kuoka 3bfcc1ea42 Add support for kustomize v4 (#1758)
We use kustomize in two places. One for turning kustomize into a chart, and another for patching manifests and the chart.

This fixes the former to work with kustomize v4.

Fixes #1688
2021-04-08 06:15:16 +09:00
Quan TRANandYusuke Kuoka c65bdff62c Respect release filter in lint and status (#1672)
Ref #1190
Fixes #1651

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2021-04-06 15:47:29 +09:00
Yusuke Kuoka f614e8b1bf feat: Helmfile renders *.yaml.gotmpl in a K8s manifests/kustomization directory (#1745)
Related to #494

This feature is mostly a built-in alternative to the `incubator/raw` chart without external dependency and has
access to helmfile's own template functions and template data.

The expected use-case of this feature is to add arbitrary K8s resources to your deployment.

Unlike the original issue raised in #494 this doesn't enable you to add arbitary resources to a release. That's another story. But this would be a good foundation for that, too.
2021-04-06 14:22:34 +09:00
Philipp HossnerandYusuke Kuoka 85accf7330 Add helm-secrets-encrypted values template file (#1701)
Secret files ending with .gotmpl are now also rendered as a gotemplate.

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

Note that currently, .gotmpl files must be valid YAML files as well.

The expected use-case of this feature is to compose a YAML array from values and encrypted secrets.

Without this feature, you would have tried to do something like the below, which didn't work.

**Example (doesn't work!)**

`values.yaml.gotmpl`:

```
environment:
  -   name: MY_EXTERNAL_IP
      value: |
          {{ exec "./get-external-ip.sh" (list "") }}
```

`secrets.yaml`:
```
_sops:
  #...
environment:
  - name: MY_SECRET_VALUE
    value: (encrypted by sops)
```

`helmfile.yaml`:

```
releases:
- name: foo
  values:
  - values.yaml
  secrets:
  - secrets.yaml
```

This doesn't work because `values.yaml` and the decrypted `secrets.yaml` are passed to `helm` to be merged, and helm overrides the array instead of merging or concatenating the arrays.

**Example (works!)**

Instead of `values.yaml` and `secrets.yaml`, you provide a single `secrets.yaml.gotmpl` that is a valid YAML and encrypted by sops:

```
_sops:
  #...
environment:
  -   name: MY_EXTERNAL_IP
      value: |
          {{ exec "./get-external-ip.sh" (list "") }}
  - name: MY_SECRET_VALUE
    value: (encrypted by sops)
```

`helmfile.yaml`:

```
releases:
- name: foo
  secrets:
  - secrets.yaml.gotmpl
```

Helmfile decrypts the gotmpl by handing it over to helm-secrets and then renders the result as a gotmpl file. The end result is that you have a two-element array `environments` that can be just passed to helm.

Resolves #1700

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2021-04-06 14:20:42 +09:00
Yujun Zhang a161796dc4 feat: Allow overriding chart via flag (#1751)
Adds `--chart` flag for overriding the selected release's chart ad-hoc-ly like `helmfile --chart $CHART template`.
This is handy when e.g. you want to have an ArgoCD application per each release in your helmfile.yaml, while also providing the ability to customize the release's chart without touching helmfile.yaml.

See https://github.com/roboll/helmfile/issues/1690#issuecomment-812321354 for more context.

Closes #1690
2021-04-06 13:20:41 +09:00
Javier Palacios 261367e7e9 Add kubectl hooks for applying file(s) or kustomize (#1736)
This enables you to write a `kubectl-apply` hook more declaratively than writing `command` and `args`:

```
releases:
- name: myapp
  chart: mychart
  hooks:
  - events: ["presync"]
    kubectlApply:
      filename: path/to/manifests
      #kustomize: path/to/kustomize
```
2021-04-06 13:16:35 +09:00
Quan TRAN 5cd0afcfa0 Add documentation about helmfile-fetch and StateValues alias (#1746)
Ref #1743, #1744, #1734
2021-04-01 09:48:58 +09:00
Quan TRAN 1748cbfcea Add .StateValues as alias of .Values in releaseTemplateDataRelease (#1744)
Ref #1743
2021-04-01 09:47:12 +09:00
Nenad Strainovic 200cae2a68 feat: --show-secrets on diff and apply commands (#1749)
Resolves #1674
2021-04-01 09:41:53 +09:00
astorathandAndrey Tuzhilin b1b7831a90 feat: make --kube-context override helmDefaults (#1673)
This would allow cli flag `--kube-context` to override value in helmDefaults allowing to use different values in local development and CI context.

Co-authored-by: Andrey Tuzhilin <andrey@3adigital.ru>
2021-03-30 16:53:19 +09:00
Quan TRAN 53c6d2f988 Add helmfile-fetch command to downloading and generating charts (#1734) 2021-03-30 16:26:31 +09:00
Yusuke Kuoka faa74962e6 Fix helmfile-template not writing to stdout (#1737)
This fixes #1691 by redirecting helm-template output to our logger when and only when --output-dir is being passed to helm-template.

See https://github.com/roboll/helmfile/pull/1691#issuecomment-805636021 for more context.
2021-03-25 10:16:52 +09:00
Gregoire MenuelandGregoire Menuel 28cf300ef2 Add support for kubeContext in environments (#1675)
Resolves #898
Resolves #1689

Co-authored-by: Gregoire Menuel <gregoire.menuel@veolia.com>
2021-03-23 18:06:53 +09:00
Chris Mellard e9d1c8534c feat: change to helmfile template so it respects logging levels (#1691) 2021-03-23 18:02:13 +09:00
astorath aafe74a3e1 Fix/secrets ordering (#1726)
Fixes issue introduced in #790: the order of secrets merged is not defined now, leading to unpredictable results in `helmfile apply`.
2021-03-23 18:00:57 +09:00
Andrey Tuzhilin db9d31ac06 test: fix DecryptSecret output 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 4a9d753441 feat: make integration/vagrant 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 4efaa656f9 test: return all tests 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 59f4043fc5 fix: fixed secrets decryption failed issue 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin d04cd1b1a7 test: fix "Ensure helmfile fails when no helm-secrets is installed" test 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 781c4e9ebe test: fix "Ensure helmfile fails when no helm-secrets is installed" test 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 0c9934c357 test: ensure bash -eo pipefail (as in circleci) 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 2983f5cd28 test: fix vault provisioning code 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 7610d8d3c6 test: add secrets integration tests 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 5cd853b773 test: add vault and sops for integration secret testing 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin f1c07879ad ci: simplify integration tests ci code for helm2 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin d1a2435ce1 ci: simplify integration tests ci code 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 45b8eb6942 test: fix helm2 integration tests 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 17a969ce83 ci: verify new integration tests 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 4f02196dad test: reusable integration test 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 85f19b6abd fix: gitignore *.lock 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 42138d88bf test: vagrant for integration tests 2021-03-23 17:19:40 +09:00
Andrey Tuzhilin 2ef09d45ee test: sync HELM_VERSION and KUSTOMIZE_VERSION for tests 2021-03-23 16:58:56 +09:00
Andrey Tuzhilin 9b2a770dbe feat(Dockerfile): pin helm-secrets version 2021-03-23 16:58:56 +09:00
Andrey Tuzhilin 6085730a91 fix(Makefile): static-linux mod 2021-03-23 16:58:56 +09:00
Andrey Tuzhilin 94a8658635 feat(Dockerfile): bump helm version 2021-03-23 16:58:56 +09:00
Jason Witkowski 2618cfb38b Enable --wait-for-jobs flag introduced in helm 3.5 (#1715)
Fixes #1650
Fixes #785

This change introduces:

- `--wait-for-jobs` CLI override option to helmfile binary
- `waitForJobs` helmDefaults parameter (default `false`)
- `waitForJobs` release parameter (default `false`)

Note that `--wait-for-jobs` was introduced in Helm 3.5 (https://github.com/helm/helm/pull/8363)
2021-03-23 16:53:57 +09:00
Rein van 't Veer fcf9a7273f Improve error message on deps resolution error (#1727)
Fixes #1356
2021-03-23 16:44:59 +09:00
Quan TRAN 5ce755e4d8 Add chart & version to list cmd output (#1731) 2021-03-23 16:29:48 +09:00
Quan TRAN b7d6639b25 (fix) remove usage of charts.yaml in error message (#1732) 2021-03-23 16:27:51 +09:00
Nick Perry 95759958b5 Add William Hill to USERS.md (#1735) 2021-03-23 16:03:48 +09:00
kupnu4xandYusuke Kuoka 8ed8a9eab3 allow to access oci repos without auth (#1711)
* allow to access oci repos without auth

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2021-03-09 10:23:36 +09:00
14a83e7cbe Update reference for helm-secrets to latest maintained fork (#1676)
* Update referance for helm-secrets to latest maintained fork

* Missed a referance in the docs to an older edition of helm secrets

* Removed docker2 changes back to original method

Co-authored-by: Nathan Flynn <nflynn@williamhill.co.uk>
Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2021-03-09 10:17:16 +09:00
Thomas Loubiou bf9f36b2bb refactor: simplify pull channel from #1706 (#1707) 2021-03-09 10:16:04 +09:00
Quan TRAN 3215eaf710 panic with clear message (#1709) 2021-03-09 10:13:24 +09:00
Justin Chizer 927bd30fa0 Update README.md to show current CLI Output (#1712) 2021-03-09 10:12:41 +09:00
Yusuke Kuoka 247ae9108c Disable darwin/386 build to avoid release failure 2021-03-04 10:27:39 +09:00
Nathan Flynn 97f0e59cfd Update README.md (#1677)
Added docs to Readme for disableValidationOnInstall
2021-03-04 09:33:22 +09:00
Quan TRAN 56405e7445 panic with a clear message (#1693) 2021-03-04 09:32:50 +09:00
Mark Ayers 42c97cd919 Update README.md (#1696)
- replace deprecated helm-secrets with the currently active helm-secrets
```plain
Deprecation Info
  Please note, this project is no longer being maintained.
  Link to active helm-secret plugin could be found in helm documentation: https://helm.sh/docs/community/related/#helm-plugins
```
2021-03-04 09:31:40 +09:00
Thomas Loubiou 65317e96f6 Fix incorrect chart bug in multi-cluster setup (#1698)
When the same release name is used accross namespaces/kubecontexts
a bad chart name could be used

Fixes #1694
2021-03-04 09:29:44 +09:00
Thomas Loubiou 453b498ccb fix: pull OCI charts one by one (#1706)
* fix: pull OCI charts one by one

* fix: gofmt
2021-03-04 09:24:22 +09:00
RaymondKYLiu 19cbea49f7 Update USERS.md (#1702)
Add Trend Micro to user list
2021-03-02 14:28:54 +09:00
Jason Witkowski bd3838c059 Add ability to specify and override helmDefaults.wait via cli (#1678) 2021-02-24 12:47:27 +09:00
Yusuke Kuoka f24b61f100 Fix error on concurrent go-getter on same URL (#1669)
Fixes #1660
2021-02-05 09:02:21 +09:00
Yusuke Kuoka 257c1f62d2 Fix OCI support (#1667)
* Fix OCI support

I have seen various issues related to the OCI repository support recently added to Helmfile.
This is the patch that should fix all the issues Im aware of until now.
2021-02-04 09:33:35 +09:00
Yusuke Kuoka 4e1ecb5890 Bump variantdev/vals to 0.13.0 (#1666)
Ref https://github.com/variantdev/vals/pull/42
2021-02-03 09:00:46 +09:00
Wi1dcard ad5fba56e0 Bump Helm to v3.5.0. (#1656) 2021-01-29 09:06:29 +09:00
Caleb Collins-Parks 0ad62b504a docs: update helm command name (#1635)
helm delete is now helm uninstall. See https://helm.sh/docs/helm/helm_delete/
2021-01-29 09:05:55 +09:00
Cameron Braid 8487970a2e fix: dont sent RegistryLogin password via args (#1662) 2021-01-29 09:05:03 +09:00
Chris Mellard 8212b630ff fix: fixed up OCI repos so they support paths and URLs that differ from chart names contained there (#1661) 2021-01-28 19:11:12 +09:00
Chris Mellard 2a71640095 feat: added in oci repository flag and added helm methods to pull and export charts (#1629) 2021-01-28 09:02:00 +09:00
Shunsuke Suzuki 33880dab77 docs: fix typo (#1645)
* docs: fix typo

* docs: syntax highlight
2021-01-12 10:23:21 +09:00
Sven Schliesing 87fcd83275 Fix typo "verion" (#1641) 2021-01-12 10:22:35 +09:00
jjangga0214 600721ca09 docs(readme): fix typo (#1633) 2021-01-12 10:22:04 +09:00
Yusuke Kuoka f7a1d09be7 Bump variantdev/vals to 0.12.0 (#1644)
For Azure KeyVault support for the fix for AWS SSM provider.
2021-01-12 10:20:57 +09:00
Yusuke Kuoka e5d982ed33 Fix "No valid platforms to build for" build error on release
Follow-up for #1612
May be related to https://github.com/mitchellh/gox/issues/92
2021-01-01 21:23:04 +09:00
Yusuke Kuoka c815d0192a Bump helm versions in Dockerfile to deal with stable and incubator repositories change 2021-01-01 12:16:41 +09:00
Yusuke Kuoka 9569d73e2f Bump helm versions in CI to deal with stable and incubator repositories change 2021-01-01 10:44:46 +09:00
Damiano AlbaniandDamiano Albani dd5bc46ead Include kustomize binary in helm3-based Docker image (#1626)
Co-authored-by: Damiano Albani <damiano.albani@geodan.nl>
2020-12-28 09:28:59 +09:00
Gastón Acosta Ramas fc3567ddda Adding documentation for PR #6819 (#1623)
Adding documentation for this feature: https://github.com/helm/helm/pull/6819
not being aware of this, make turn helm2 to 3 migrations very complicated.
2020-12-28 09:24:38 +09:00
Yusuke Kuoka 9b64d65d94 feat: Stabilize helmfile-diff output (#1622)
This finishes the work started at #1619 by introducing the following changes:

- You can now set `HELMFILE_TEMPDIR` and `CHARTIFY_TEMPDIR` as the directory to persist temporary values files rendered by helmfile and temporary charts generated by chartify. Note that chartify is used internally when you use kustomize integration or raw K8s manifests as a chart.
- Helmfile uses the hash sum of the release config and the values file content in the file name of the temporary values file.

  In combination with `HELMFILE_TEMPDIR` and `CHARTIFY_TEMPDIR`, this enables helmfile to use the stable file names for temporary values files, which contribute to stabilize log messages like `Comparing release=appset, chart=PATH/TO/TEMPORARY/CHART`, where the `PATH/TO/TEMPORARY/CHART` had been randomized due to formerly random helmfile tempdir and temporary values file names, and random chartify tempdir.

You can try this feature with a script like:

```
tempdir=$(mktemp -d -t helmfile)

HELMFILE_TEMPDIR=${tempdir} CHARTIFY_TEMPDIR=${tempdir} helmfile diff

rm -rf ${tempdir}
```
2020-12-15 10:24:27 +09:00
54eb73b423 Use ghodss/yaml for yaml marshaling & unmarshaling in template (#1556)
Fixes #1555

Co-authored-by: Yoann Ciabaud <yoann@linxo.com>
Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2020-12-13 11:03:20 +09:00
jayme-github 4c0987a618 Don't append --tiller-namespace flag when running helm3 (#1594) 2020-12-13 10:21:12 +09:00
Chris Mellard 0637973e40 fix: allow helmfiles to be corrected marshalled to YAML so they can be manipulated by other programs (#1604) 2020-12-13 10:08:03 +09:00
Felipe Santos 3690bde94c Add documentation on how to use the docker image (#1607)
* Add documentation on how to use the docker image

* Remove default workdir and add it to snippet
2020-12-13 10:00:36 +09:00
Life ChristianandLife Lofranco b92ff2baff Fix selector syntax example (#1614)
Co-authored-by: Life Lofranco <life@dealgrocer.com>
2020-12-13 09:58:24 +09:00
Chris Mellard afe82f6453 chore: suppress empty values on serializing several helmfile values (#1616) 2020-12-13 09:57:35 +09:00
Yusuke Kuoka 1c7b872476 Stabilize helmfile-diff output (#1619)
`helmfile-diff` sorts multiple and concurrent helm-diff outputs and stabilizes writes to stdout.

It's required to use the stdout from helmfile-diff to detect if there was another change(s) between 2 points in time.

For example, terraform-provider-helmfile runs a helmfile-diff on `terraform plan` and another on `terraform apply`. `terraform`, by design, fails when helmfile-diff outputs were not equivalent. Stabilized helmfile-diff output rescues that.
2020-12-11 09:51:26 +09:00
James Strachan 28e7ebb4a4 fix: support arm64 images (#1612) 2020-12-11 09:44:07 +09:00
Yusuke Kuoka 1ef9b29f6d Improve handling of releases being newly installed by helmfile-apply (#1618)
This improves helmfile-apply with two things:

- Some users had timing-out issues or annoyed by huge output from helm-diff run as part of helmfile-apply on first install. `--skip-diff-on-install` skips running helm-diff for releases being newly installed, so that you can avoid those issues.
- Some users had difficultly or found it not straight-forward to install CRDs and custom resources from separate charts in one helmfile-apply (#1353). The new helmfile.yaml release field `disableValidationOnInstall: true` adds `--disable-validation` to helm-diff only for releases being newly released, which should mostly resolve the issue.

Resolves #1353
2020-12-11 09:09:35 +09:00
Yusuke Kuoka b910591e1d Add support for Kustomize Transformers (#1592)
Please see the updated `advanced-features.md` for more details.

This is often used for adding common labels and annotations to any resources rendered from a Helm chart.
2020-11-19 09:33:05 +09:00
Krzysztof Pawlik d807510dd7 Enable shell completion (#1559)
* feat: enable EnableBashCompletion to get --generate-bash-completion command line flag

* feat: add scripts for bash and zsh

* feat: document shell completion
2020-11-19 09:32:33 +09:00
Wi1dcard 4e485219d7 Fix the logic of helmfile deps and add tests. (#1588) 2020-11-19 09:29:59 +09:00
Wi1dcard b0e042988c Fix incorrect help text of some CLI options. (#1587) 2020-11-19 07:20:08 +09:00
manfred-h f877de7778 Fix openSUSE repo URL. (#1584) 2020-11-19 07:19:00 +09:00
Baryczka 655d5934a2 Mention about helmBinary in readme (#1590) 2020-11-19 07:17:55 +09:00
Yusuke Kuoka f6bf885fb7 Fix helmfile template --include-crds not to break with chartify (#1583)
* Fix `helmfile template --include-crds` not to break with `chartify`

This bumps variantdev/chartify to 0.4.9 so that we can incorporate https://github.com/variantdev/chartify/commit/fed8bb29533904b1af3044a266220652231afe87

* Fix integration test
2020-11-12 09:26:43 +09:00
James Strachan 2857382006 fix: support arm binaries (#1580)
fixes #1433
2020-11-12 08:58:54 +09:00
manfred-h 972667f115 Mention openSUSE packages for helmfile (#1581)
* Mention openSUSE.

* Add example how to add a repo.
2020-11-12 08:57:26 +09:00
Brian Choy 3c88145329 Update readme stable/incubator URLs (#1582)
Small update to the readme repository examples since Google is no longer officially supporting hosting the stable/incubator helm charts. The official helm docs are using these repos now, eg. https://helm.sh/docs/intro/quickstart/.
2020-11-12 08:56:34 +09:00
Yusuke Kuoka 709913b5ea Bump dependencies (#1573)
Bumps dependencies mainly to fix possible go mod error that is fixed via https://github.com/variantdev/vals/commit/0edd5343228b96e1ca5f3599f923fee0ff1fd0cb, but also bumps all the other dependencies because they looked very outdated :)
2020-11-09 08:50:00 +09:00
Yusuke Kuoka 88884b68dc feat: helmfile template --skip-cleanup (#1570)
* feat: `helmfile template --skip-cleanup`

Resolves #1517
2020-11-06 09:23:49 +09:00
Yusuke Kuoka 5a15b65b94 feat: Add helmfile apply --skip-cleanup (#1571)
This deprecates the old `--retain-values` which was not working as intended.

Also see #1570 - there's now `--skip-cleanup` for `helmfile-template`, too.
2020-11-05 10:18:30 +09:00
Yusuke Kuoka 3899680672 feat: Add helmfile test --logs (#1569)
When `--logs` is provided, Helmfile runs `helm test --logs` so that it can stream test logs

Ref #1541
2020-11-05 10:17:18 +09:00
Yusuke Kuoka 6b86408500 feat: Add helmfile template --include-crds (#1568)
This allows you to use helmfile-template output as a GitOps source, when the template output contains CRDs and you use Helm 3.

Helm 3 by default removes CRDs from the template output. If you want to git-commit helmfile-template containing CRDs for GitOps and you use Helm 3 for templating, the only way is provide this newly added `--include-crds` flag.
2020-11-05 10:13:25 +09:00
Yusuke Kuoka bdbaa00628 Fix redundant helm-dep-build and prepare hook regression (#1566)
Fixes #1511
Fixes #1497
2020-11-05 09:43:42 +09:00
Yusuke Kuoka 87e86dcfd2 Fix helmfile --selector x=y template with needs (#1564)
I had mistakenly inverted the necessary flag to turn off the legacy processing on selectors that should be used only for helmfile commands that does not support DAGs/needs.

Fixes #1552
2020-11-04 09:29:23 +09:00
Christian Rebischke 995b201e47 mention Arch Linux support (#1549) 2020-10-26 08:59:05 +09:00
Yusuke Kuoka 0663831dd5 Disable dependency update while running helm-x/chartify in more cases (#1548)
`helm dep up` is now skipped while running helm-x/chartify when the chart/directory is obtained by running go-getter, or `skipDeps` is configured using a command-line flag, helmDefaults, or release configuration.

Resolves #1547
2020-10-25 11:25:59 +09:00
isindir afb2653452 fix: printing error to stdout together with templated resources (#1550)
Fixes #1551
2020-10-25 09:14:07 +09:00
isindir 9ec4a85821 Minor README improvement (#1543)
Fixes #1544
2020-10-19 08:48:39 +09:00
Wi1dcard 4d868794fd Improve capitalization and formats of README.md. (#1545) 2020-10-19 08:47:19 +09:00
Jake Hill 1aca099457 feat: Upgrade to kubectl version supported by latest stable release and upgrade helm3 to 3.3.4 (#1498)
Changes:

* Bump helm to v3.3.4.
* Bump kubectl to v1.18.9
* Update Dockerfile to be a little more verbose to help understand which command in a chain failed.

Signed-off-by: Jake Hill <jake@naphta.uk>
2020-10-19 08:45:44 +09:00
Wi1dcard f5d098876a Retain the option helm repo add --force-update when Helm is v3.3.4 or above. (#1546)
See: https://github.com/roboll/helmfile/pull/1542#issuecomment-710082201
2020-10-19 08:44:09 +09:00
Yusuke Kuoka 080080f57f Bump chartify to 0.4.4 (#1540)
To incorporate the fix for unconventional chart templates like seen in the Datadog chart https://github.com/variantdev/chartify/commit/5443ca1a1db36d2626522f39130ac1157a69c3f7
2020-10-15 10:17:37 +09:00
Wi1dcard cd4afc3269 Remove duplicated logs from helm.exec. (#1538)
While using the `--debug` option, the output of helm seems to be printed triple times:

```
Templating release=test, chart=charts/test
exec: helm template test charts/test --namespace staging --values /tmp/values266548685 --output-dir outputs//helmfile-test-staging-ed5e3c94-test --debug
helm:MRAjW> install.go:172: [debug] Original chart version: ""
helm:MRAjW> install.go:189: [debug] CHART PATH: /app/charts/test
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/secret.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/configmap.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/health-configmap.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/headless-svc.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-svc.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-svc.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-statefulset.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/ingress.yaml
helm:MRAjW> wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-prometheus.yaml
helm:MRAjW>
exec: helm template test charts/test --namespace staging --values /tmp/values266548685 --output-dir outputs//helmfile-test-staging-ed5e3c94-test --debug: wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/secret.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/configmap.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/health-configmap.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/headless-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-statefulset.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/ingress.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-prometheus.yaml


wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/secret.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/configmap.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/health-configmap.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/headless-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-svc.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/redis-master-statefulset.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/templates/ingress.yaml
wrote outputs//helmfile-test-staging-ed5e3c94-test/test/charts/redis/templates/metrics-prometheus.yaml
```

- The first with the prefixes `helm:MRAjW>` comes from <https://github.com/roboll/helmfile/blob/563fce4adf7abf759ec31b3971d1195704621052/pkg/helmexec/runner.go#L59-L65>

- The second one with the format `exec: helm ... --debug: ...` comes from <https://github.com/roboll/helmfile/blob/563fce4adf7abf759ec31b3971d1195704621052/pkg/helmexec/exec.go#L369>

- And the last are the logs of non-debug mode.

It sometimes makes the log a bit confusing, while I first time read the log, I thought helmfile executed helm command multiple times 😅.

Since we already piped helm outputs to stdout (the first), we remove the second logs.
2020-10-15 08:50:56 +09:00
Javier Palacios 8f8669778c Support for azure acr helm repositories (#1526)
Adds a basic support for Helm repositories hosted on Azure Container Registry (not OCI but classic ones). Add a new field to RepositorySpec to state that is externally managed and runs the `az-cli` command instead of the helm one to manage the repository.
2020-10-15 08:45:45 +09:00
Yusuke Kuoka 563fce4adf Bump vals (#1537)
To incorporate unexpected SSM secret exposure issue when the parameter version is specified.
See https://github.com/variantdev/vals/pull/37
2020-10-14 09:56:17 +09:00
Karl Stoney b287101625 Update USERS.md (#1535)
Adding AT as a User :-) 
Thanks for your OSS efforts!
2020-10-14 08:38:30 +09:00
Yusuke Kuoka ab9fb2c9dc Fix concurrent-map-iteration-and-write errors while running release hooks (#1534)
Fixes #1495
2020-10-13 14:49:01 +09:00
Yusuke Kuoka c170b5a621 Minor tweaks for helmfile write-values (#1533)
See 1 and 3 in https://github.com/roboll/helmfile/issues/1460#issuecomment-691863465
2020-10-13 09:33:49 +09:00
Yusuke Kuoka d9286ed8f6 Fix intermittent failures while building deps on local chart (#1532)
Fixes #1521
2020-10-13 09:14:45 +09:00
Yusuke Kuoka 3018e82902 Fix multiple --set in --args messed up in helmfile template (#1529)
Fixes #1520
2020-10-13 08:38:06 +09:00
Yusuke Kuoka 34acf14d7c Bump chartify to v0.4.3 (#1528)
This fixes the bug that generated Chart.yaml misses the `apiVersion` field which resulted in `helm lint` always failing on the generated chart.

Fixes #1527
2020-10-12 09:23:58 +09:00
Wi1dcard 5d8eba9b29 Append --force-update for specific helm versions. (#1494)
* Parse and process helm version using github.com/Masterminds/semver/v3.

* Add --force-update only when Helm version >= 3.3.2, < 3.3.4.

See: https://github.com/helm/helm/pull/8777.

* Add test cases.
2020-10-12 09:20:55 +09:00
Roderik van der Veer b284b7be83 chore: add SettleMint to the USERS list (#1516) 2020-10-12 09:11:14 +09:00
Allie b293ac1f83 Fix typo (#1523) 2020-10-12 08:58:05 +09:00
Matthias Baur 8d9d635b8b Clarify namespace prefix in needs (#1500)
Before this, the documentation didn't mention that the release needs to
be prefixed with the namespace if the release had a namespace configured.
2020-10-05 08:51:39 +09:00
mdaisuke 954673d742 Fix: forceNamespace needs shouldRun is true (#1510) 2020-10-02 08:27:39 +09:00
Yusuke Kuoka 942b9a64c0 Add support for ChartCenter (#1492)
Resolves #1326
2020-09-21 15:31:24 +09:00
Yusuke Kuoka b176408eb2 Enable helmfile test testing only enabled and selected releases (#1486)
Resolves #1483
2020-09-21 09:44:05 +09:00
Yusuke Kuoka ff81b2f279 Enable release hook template to access .Values (#1485)
Fixes #1484
2020-09-21 09:43:30 +09:00
Andrii Dembitskyi 8db03f979c Drop duplicated entry in extra functions list from readme (#1490) 2020-09-21 09:42:47 +09:00
Wi1dcard 988c218096 Support the latest Helm (>=v3.3.2) and bump the Helm version in Docker image. (#1488)
Changes:

* Bump Helm to v2.16.12 and v3.3.3.
* Add --force-update only when using Helm 3.
2020-09-21 09:41:49 +09:00
Yusuke Kuoka 028bcc51dc Treat selector with multiple conditions an AND (#1478)
Fixes #1477
2020-09-15 09:56:05 +09:00
Yusuke Kuoka 5dd65e8d9a Bump vals (#1475)
Ref https://github.com/variantdev/vals/issues/34
2020-09-14 20:37:51 +09:00
Or Biran 134d5beba2 Add tailormed to users (#1467)
* Update USERS.md
2020-09-13 09:47:18 +09:00
Jason Witkowski 87d04f0202 Append KubeContext to temporary directory path (#1471)
Fix bug where KubeContext is not taken into account in temporary directories
2020-09-13 09:46:08 +09:00
vugardzhamalov 2d7c8e7525 Update README.md (#1472)
Made a small change to Global Hooks example in order to remove release name reference - this was probably copy/pasted from Hooks section
2020-09-13 09:41:21 +09:00
Yusuke Kuoka 0fad9f0544 Add experimental write-values command for writing values files only (#1469)
Ref #1460
2020-09-11 22:19:36 +09:00
Yusuke Kuoka 832dcf47a5 Re-add Release.Namespace in release values.yaml templates (#1466)
It was accidentally removed in #1424, and had been unexpectedly unavailable between v0.126.0 and v0.128.1.

Fixes #1464
2020-09-10 21:08:20 +09:00
Harshal Shah 1d2c8b8a3a Adding hellofresh as a helmfile user (#1465) 2020-09-10 09:41:53 +09:00
Yusuke Kuoka 0482ba3b6f Fix index out of range [1] with length 1 error on env2map (#1463)
This happened only when helmfile is run in an environment that any environment variable definition does not include `=` and the right side.
2020-09-08 11:23:36 +09:00
9d2c0d4285 Bump sprig to v3.1.0 and mergo 3.11 (#1456)
* Bump sprig to v3.1.0
test for mergeOverwrite

* Let mergo not (accidentally) try to merge unexported fields

This is also a good chance separate `HelmState` with the config loaded from YAML, which I had been wanting to do for a long time.

Co-authored-by: Johannes Alkjær <johannes.alkjaer@wunderman.com>
Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2020-09-04 09:58:54 +09:00
Yusuke Kuoka efd26f288a Bump sprig to v3 (#1452)
Resolves #1294
Resolves #1447
2020-09-01 19:10:29 +09:00
Yusuke Kuoka 5f1698d0ed Fix wrong deletion order (#1451)
Fixes #1450
2020-09-01 17:36:11 +09:00
Yusuke Kuoka 2ecf5780f3 Revise GitOps doc a bit 2020-09-01 12:11:26 +09:00
Yusuke Kuoka 3e6542e341 Fix test flake for commonLabels (#1449)
Ref https://github.com/roboll/helmfile/pull/1415#issuecomment-684139831
2020-09-01 10:52:08 +09:00
Yusuke Kuoka d3daea3137 Fix panic on hook since v0.126.0 (#1448)
Fixes #1445
2020-09-01 10:37:21 +09:00
Yusuke Kuoka 19d79427a1 Add experimental "forceNamespace" in helmfile.yaml (#1444)
This is an experimental feature to support a potential use-case that you need to set namespaces in manifests rendered by `helmfile template`, WHEN the chart is unconventional hence does not have `namespace: {{ .Namespace }}`.
Rather than using this, you should usually fork/maintain or update/pull-request the chart to have `namespace: {{ .Namespace }}`.
Use this only when you CAN NOT do so, but still need to use `helmfile template`.
2020-09-01 10:13:27 +09:00
Yusuke Kuoka cd0ecc5263 Fix build error after helmfile build --embed-values addition
This is a follow-up for #1436
2020-08-29 15:01:22 +09:00
AdamClarkandYusuke Kuoka dc6c59dc14 Print command output in line (#1354)
This is an attempt to log helm exec output as the helm exec command is running. It only prints if the --debug flag is set.

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2020-08-29 14:47:22 +09:00
Yusuke Kuoka 0fc0869671 feat: helmfile build --embed-values to embed release values and secrets into the output (#1436) 2020-08-29 13:18:17 +09:00
Maksym Lushpenko 5ca7ce15bc feat: common labels for all releases in a helmfile (#1415)
This adds `comonLabels` option to helmfile by:

- Adding `CommonLabels` to HelmState
- Changing `markExcludedReleases` and `ListReleases` functions to merge common labels into release labels

Resolves #1266
2020-08-29 13:14:58 +09:00
Pawel PrzeszloandPawel Przeszlo 6b4b76e2bc HELMFILE_ENVIRONMENT variable (#1425)
Co-authored-by: Pawel Przeszlo <pawel.przeszlo@comarch.pl>
2020-08-29 13:10:07 +09:00
Joshua HansenandYusuke Kuoka 7b11ce851a feat: Allow .Release.Name to be used in gotmpl values templates (#1424)
This adds the ability to utilize `.Release` inside of gotmpl files as discussed [here](https://github.com/roboll/helmfile/issues/760).

Resolves: https://github.com/roboll/helmfile/issues/760

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2020-08-29 13:07:46 +09:00
Yusuke Kuoka c575587e1f Merge pull request #1442 from roboll/fix-duplicate-with-kubectx-per-release
Fix kubeContext not taken into account for release uniqueness
2020-08-29 11:45:13 +09:00
Yusuke Kuoka f2dfa7ce64 Fix kubeContext not taken into account for release uniqueness
Fixes #1440
2020-08-29 11:33:05 +09:00
Yusuke Kuoka 41cd1fe4a6 Fix race while running helm dep build on local chart (#1439)
* Fix race while running `helm dep build` on local chart

Fixes #1438
2020-08-29 10:24:48 +09:00
Yusuke Kuoka 9e54af5c2c fixup! fixup! Fix race while running helm dep build on local chart 2020-08-29 10:14:42 +09:00
Yusuke Kuoka 8a43e8f782 fixup! Fix race while running helm dep build on local chart 2020-08-29 10:04:09 +09:00
Yusuke Kuoka 94e01b79e0 Bump chartify to 0.4.2 (#1437)
To fix the issue that adhoc json patches were not working on kustomize/raw manifests.

Note that regular kustomize project was working. In other words, this only affetcts `chart: path/to/dir` combined with `jsonPatches: ...` when the `path/to/dir` points to a kustomize project or a local directory containing raw K8s manifests.

Ref https://github.com/roboll/helmfile/issues/1434#issuecomment-682247709
2020-08-28 21:13:04 +09:00
Ben Mathews 85e77982bf Update to the latest version of helm-diff plugin (#1430)
This is needed to support the --disable-validation flag added into
helmfile
2020-08-28 21:12:44 +09:00
Yusuke Kuoka b42e847154 Fix race while running helm dep build on local chart
Resolves #1438
2020-08-28 20:08:44 +09:00
Yusuke Kuoka 14e2b9ed93 Fix regression in helmfile deps (#1431)
Fixes #1421
2020-08-27 09:22:02 +09:00
Anatoly Rugalev 90a41222e6 fix: panic when using helm v3.3 (#1427) (#1428) 2020-08-24 08:57:17 +09:00
KUOKA Yusuke a9aa7af572 Fix regression installing raw K8s manifests (#1417)
Fixes #1413
2020-08-14 19:52:00 +09:00
KUOKA Yusuke 738409ee0e Fix regression for helmfile template with helm v2 since v0.125.4 (#1416)
This fixes the regression introduced by #1406 that resulted in `helmfile template` with helm v2 failing when there is any release with `version`.

Fixes #1414
2020-08-14 09:57:10 +09:00
KUOKA Yusuke ec87a39015 Revert filtering on repositories to be updated (#1412)
This reverts a part of #1383 so that repository updates are done in the pre-0.125.0 way, which tries to update any repositories only once regardless of they are referenced by selected releases or not.

Ref #1404
2020-08-13 10:10:30 +09:00
KUOKA Yusuke ab1c118022 Bump chartify to v0.4.0 (#1409)
* Bump chartify to v0.4.0

Should fix #1338

* Bump minimum Helm 3 version to v3.1.0 for `helm template --include-crds`
2020-08-12 09:52:51 +09:00
KUOKA Yusuke c8c444debc Add test case (#1410)
To ensure that `helmfile -l foo!=bar` does match against release without the label at all.
2020-08-12 09:07:22 +09:00
KUOKA Yusuke 9a03d79a84 Fix chart fetched by go-getter not to fail due to missing dependencies (#1408)
Ref https://github.com/roboll/helmfile/issues/1401#issuecomment-670915272
2020-08-08 21:22:57 +09:00
KUOKA Yusuke 0ef7e65f02 Fix race on/sometimes missing postsync and cleanup hooks (#1407)
So that those hooks are fully executed as intended. The documentation about hooks are updated as well to clarify the intended behaviour.

Fixes #1398
2020-08-08 16:45:21 +09:00
KUOKA Yusuke 61b61d3009 Fix helmfile template with Helm 3 ignoring chart version (#1406)
This fixes a regression introduced in #1399
Fixes #1377
2020-08-08 15:26:40 +09:00
KUOKA Yusuke 2710cb382f Fix go-getter URL in chart to actually work (#1405)
Fixes #1401
2020-08-08 11:01:47 +09:00
KUOKA Yusuke 4d7fcd846e doc: ArgoCD integration (#1402)
Relates to #1110
Relates to #1060
Relates to #780
2020-08-06 09:51:11 +09:00
KUOKA Yusuke b85243a6b4 Fix various issues in chart preparation (#1400)
In #1172, we accidentally changed the meaning of prepare hook that is intended to be called BEFORE the pathExists check. It broke the scenario where one used a prepare hook for generating the local chart dynamically. This fixes Helmfile not to fetch local chart generated by prepare hook.

In addition to that, this patch results in the following fixes:

- Fix an issue that `helmfile template` without `--skip-deps` fails while trying to run `helm dep build` on `helm fetch`ed chart, when the remote chart has outdated dependencies in the Chart.lock file. It should be up to the chart maintainer to update Chart.lock and the user should not be blocked due to that. So, after this patch `helm dep build` is run only on the local chart, not on fetched remote chart.
- Skip fetching chart on `helmfile template` when using Helm v3. `helm template` in helm v3 does support rendering remote charts so we do not need to fetch beforehand.

Fixes #1328
May relate to #1341
2020-08-06 09:06:25 +09:00
KUOKA Yusuke 53c3fe9f37 Add support for selector on helmfile template (#1399)
Fixes #1377
2020-08-05 09:31:58 +09:00
Kyrylo Lebediev 2ce57ba2ee Handle --disable-openapi-validation parameter in helm upgrade (#1396) 2020-08-04 08:36:18 +09:00
KUOKA Yusuke cc9b7f3942 Fix regression on template failing due to duplicate fetch target (#1395)
This fixes a regression introduced in #1388
Fixes #1394
2020-08-04 02:28:58 +09:00
KUOKA Yusuke 85a2024669 Fix helmfile lint failure when installed: false (#1391)
Fixes #1344
2020-08-01 13:47:57 +09:00
KUOKA Yusuke 87573089e8 Fix false-positive duplicate release with kubeContext (#1390)
Helmfile has been incorrectly showing releases with the same name but in different kubeContexts as duplicates. This fixes that.
2020-08-01 12:12:54 +09:00
KUOKA Yusuke 4fb47a833a Add version sub-command (#1389)
Resolves #1369
2020-08-01 10:37:34 +09:00
KUOKA Yusuke 9cffef50b0 Update documentation to mention Renovate integration (#1387) 2020-08-01 10:36:15 +09:00
KUOKA Yusuke b4857937fb Fix lint to not fail for duplicate release names across ns (#1388)
Fixes #1384
2020-08-01 10:36:01 +09:00
KUOKA Yusuke 1e260e4a5e Fix and enhancement to repository update (#1383)
Changes:

- Prevent Helmfile from unnecessarily running `helm repo add` and `helm repo up` against repositories for unused repositories(repositories of releases filtered out by selector)
- Fixes #1330
2020-07-28 10:17:43 +09:00
KUOKA Yusuke ee8ea507a6 Add pre/postuninstall hooks (#1375)
Exmple:

```
releases:
- name: test2
  chart: stable/mysql
  installed: false
  hooks:
  - events:
    - prepare
    - preuninstall
    - postuninstall
    - cleanup
    showlogs: true
    command: "echo"
    args:
    - "event name = {{` {{ .Event.Name `}} }}"
```

Output:

```
helmfile apply
Listing releases matching ^test2$
test2   default         1               2020-07-23 00:51:44.975478 +0900 JST    deployed        mysql-1.6.6     5.7.30

helmfile.yaml: basePath=.

hook[preuninstall] logs | event name =  preuninstall
hook[preuninstall] logs |
Deleting test2
release "test2" uninstalled

helmfile.yaml: basePath=.

hook[postuninstall] logs | event name =  postuninstall
hook[postuninstall] logs |
helmfile.yaml: basePath=.

hook[cleanup] logs | event name =  cleanup
hook[cleanup] logs |

DELETED RELEASES:
NAME
test2
```

Resolves #802
2020-07-23 01:09:36 +09:00
KUOKA Yusuke b5830a3011 Add ability to use go-getter for fetching remote manifests directory as chart (#1374)
This, in combination with #1172, allows you to use `go-getter`-supported URL for K8s manifests on `chart`, so that Helmfile automatically fetches it and then turning it into a temporary local chart, which is then installed by Helmfile as similar as standard Helm charts.

An example usecase of this is to install cert-manager CRDs which is distributed separately from the chart:

```
releases:
- name: cert-manager-crds
  chart: git::http://github.com/jetstack/cert-manager.git@deploy/crds?ref=v0.15.2
```

I'm adding this based on discussion with @lukasmrtvy. He was trying to install cert-manager and prometheus-opreator with Helmfile, and this combined with #1373 should do the job. Thanks for the input!
2020-07-23 00:30:16 +09:00
KUOKA Yusuke 4fde6e13db Add disableValidation and disableOpenAPIValidation per release (#1373)
`disableOpenAPIValidation: true` might be useful for workaround for broken CRDs that is known to be exist in older OpenShift versions, and `disableValidation: true` is confirmed to allow installing charts like prometheus-operator that tries to install CRDs and CRs in the same chart.

Strictly speaking, for the latter case I believe you only need `disableValidation: true` set during the first installation, but for the ease of operation I shall suggest you to always set it.

Obviously turning validation mostly(disableOpenAPIValidation) or entirely(disableValidation) result in deferring any real error until sync time. We need completely client-side validation that is able to read CRDs and use it for validating any CRs to catch any error before sync. But it worth an another (big) issue.

Fixes #1124
2020-07-22 23:10:22 +09:00
Olblak a5e790caf1 Add Jenkins OSS to the list of users (#1372) 2020-07-22 22:34:44 +09:00
KUOKA Yusuke 1e956ae8a5 Fix list failure when patches are used (#1371)
Fixes #1368
2020-07-22 22:33:45 +09:00
KUOKA Yusuke 1671b8ba7d Bump variantdev/vals for better Variant KV v2 support (#1367)
Ref https://github.com/variantdev/vals/pull/33
2020-07-21 07:17:40 +09:00
KUOKA Yusuke 608ba19c3c Bump chartify to 0.3.11 (#1366)
Fixes #1365
2020-07-21 04:56:27 +09:00
Prasanna daf7d3b876 fix the issue in the documentation (#1364) 2020-07-20 10:09:54 +09:00
KUOKA Yusuke 34f6d4d568 Bump chartify to 0.3.10 (#1363)
Fixes #1360
2020-07-19 20:08:18 +09:00
Yusuke Kuoka 1784b02d03 Add --retry-connrefused for reliable image builds 2020-07-18 22:29:58 +09:00
KUOKA Yusuke 68c51d8c16 Bump variantdev/vals to 0.9.3 (#1361) 2020-07-18 22:28:37 +09:00
KUOKA Yusuke 4bbb1699a3 Bump variantdev/chartify to 0.3.8 (#1359)
Fixes #1335
2020-07-17 09:41:05 +09:00
Sergei Ryabkov 753f65a56d Add Cherre to USERS.md (#1358)
We have been meaning to create this PR for a while now...
2020-07-17 09:28:07 +09:00
Yusuke Kuoka 43adbeaaca Update README about loading remote environment values 2020-07-16 09:33:09 +09:00
KUOKA Yusuke df6489ab43 feat: helmfile template --output-dir-template for customizing output dirs (#1357)
This is useful for e.g. removing state file names and their hash values out of output dirs so that it can be used easily in a gitops setup. For example, `--output-dir-template mybasedir/{{.Release.Name}}` produces `mybasedir/RELEASE/CHART/templates/*.yaml` for each release in your helmfile.yaml.
2020-07-16 09:02:26 +09:00
Santiago Hernandez 31d0fce6fd Update USERS.md (#1352) 2020-07-14 08:44:24 +09:00
KUOKA Yusuke cd427d9714 Bump variantdev/vals to 0.7.1 (#1332)
To incorporate the tfstate provider fix

Ref https://github.com/roboll/helmfile/issues/505#issuecomment-653848769
2020-07-06 08:30:19 +09:00
Alex VoronaandAlex Vorona 6d4f0984b6 Fix kubeContext per release override (#1312)
* Move kube-context from helmDefaults to 1'st argument to let kube-context per release override it

* fix tests

Co-authored-by: Alex Vorona <av@dysnix.com>
2020-06-27 17:19:05 +09:00
Santiago Hernandez ac40eec423 Add PedidosYa :) (#1322) 2020-06-27 17:17:30 +09:00
KUOKA Yusuke 8de9b70b33 Bump variantdev/vals to 0.6.0 (#1311)
- Add `version` parameter for Vault provider
- Add `profile` parameter for AWS SSM/SecretsManager provider
= Add `version` parameter for AWS SSM Parameter Store provider
- Add support for app-role authentication when using Vault provider
2020-06-16 09:07:04 +09:00
ento face92536c Add integration test for Kustomize inetgration (#1288)
Summary of changes:

* Output any error from Mkdir in `helmfile template`

* Add failing test for .Release.Name interpolation

* Add golden files for testing

* Parse resources with kustomize to compare them structure by structure

* Decode resources into plain maps

The RNode type from kustomize uses yaml.Node under the hood,
which carries extra information like line numbers, which
become noisy when comparing with deep.Equal.
2020-06-16 09:06:52 +09:00
KUOKA Yusuke 3a2a460fe7 Do cleanup decrypted env secrets files (#1304)
* Do cleanup decrypted env secrets files

Resolves #503
2020-06-16 08:59:48 +09:00
Max Audron f16d96bc8f Add global hooks (#1301)
Changes:

* Add global hooks
* Add top level hooks field to yaml spec
* Add functions for global prepare and cleanup events
* Call global prepare and cleanup events in withPreparedCharts function
* Update README
* Add helmfileCommand variable to withPreparedCharts
  Pass the information on what helmfileCommand has been run down from the
  top level functions through withReposAndPreparedCharts and withPreparedCharts.
2020-06-11 10:05:38 +09:00
Kevin J. Qiu 0f86cc9b87 Add the ability to load a remote environment values file (#1296)
Enables the user to specify a remote path for an environment values file, e.g.,

```yaml
environments:
  cluster-azure-us-west:
    values:
      - git::https://git.company.org/helmfiles/global/azure.yaml?ref=master
      - git::https://git.company.org/helmfiles/global/us-west.yaml?ref=master
  cluster-gcp-europe-west:
    values:
      - git::https://git.company.org/helmfiles/global/gcp.yaml?ref=master
      - git::https://git.company.org/helmfiles/global/europe-west.yaml?ref=master

releases:
  - ...
```

This is particularly useful when you co-locate helmfiles within your project repo but want to reuse the definitions in a global repo.
2020-06-11 10:04:01 +09:00
KUOKA Yusuke ca844aa044 Fix patching not using release namespace (#1300)
Ref #1299
2020-06-09 16:40:13 +09:00
Rene HernandezandRene Hernandez Remedios 604af3f1d2 Replace getOrNil with get function in documentation (#1268)
* New output flag for list command

Support output as json
Add new formatters file to handle extrac formatting to its own concern
New config interface to support list command specification

* Fix usage message

* Add error handling for formatters

* Replace usage of getOrNil with get function

* Add nil in get function

Co-authored-by: Rene Hernandez Remedios <rene.hernandez@fullscript.com>
2020-06-08 08:10:27 +09:00
Ilya Vassyutovich 28e13c3a18 typo: Fix message for detected diff (#1284) 2020-06-06 09:31:25 +09:00
KUOKA Yusuke 7db31846cf fix: Do not include stderr content in result of {{ exec }} (#1298)
Seems like this was a regression since https://github.com/roboll/helmfile/commit/f676c6142520364553c6a574fb976eeb0c31a456 and #1159

Fixes #1258
2020-06-06 09:29:12 +09:00
KUOKA Yusuke 63c8df9873 Fix missing resources in hemlfile template with jsonPatches (#1290)
Probably this has been affecting strategicMergePatches and dependencies as well, as the source of the problem is the underlying common feature called helm-x that previsouly used `helm template --output-dir`, which turned out to be not working as intended.

Ref #1279
2020-06-01 22:54:11 +09:00
KUOKA Yusuke eff2a7bf84 Fix repo update timing (#1287)
Fixes #1283
2020-05-30 18:01:14 +09:00
KUOKA Yusuke 3284df2752 Bump variantdev/chartify to fix kustomize support (#1280)
Fixes #1279
2020-05-29 10:08:09 +09:00
KUOKA Yusuke e607bae5ff Fix ignored chart version on patched release (#1276)
See https://sweetops.slack.com/archives/CE5NGCB9Q/p1590607083301900?thread_ts=1590237129.290300&cid=CE5NGCB9Q
2020-05-28 20:52:56 +09:00
KUOKA Yusuke 930f91a70d Merge pull request #1273 from roboll/fix-chartify
fix: Follow-up fixes for #1172
2020-05-27 22:15:41 +09:00
Yusuke Kuoka 1cfce32e0d Add documentation for releases[].{strategicMergePatches,jsonPatches} 2020-05-27 22:15:12 +09:00
Yusuke Kuoka 29fdb57add fix: Support for kustomization as chart
A kustomization has not been properly chartified when a kustomization is specified on `releases[].chart` but there were no `releases[].strategicMergePatches` and `jsonPatches` specified.
This fixes that.
2020-05-27 22:07:52 +09:00
Yusuke Kuoka 68432b1848 fix: Do not skip passing values files when adhocDependencies/jsonPatches/jsonPatches exist
This is a follow-up for #1172
2020-05-27 21:13:05 +09:00
KUOKA Yusuke 16288dfa7d feat: GA of Kustomize and K8s manifests support (#1172)
This is the GA version of the helm-x integration #673 developed last year.

You get all the following benefits without an extra helm plugin:

- Ability to add ad-hoc chart dependencies/aliases, without forking the chart (Fixes #876 )
- Ability to patch resulting K8s resources before installing the helm chart
- Ability to install a kustomization as a chart (Requires `kustomize` binary to be available in `$PATH`
- Ability to install a directory of K8s manifests as a chart
- etc.
2020-05-27 11:42:43 +09:00
Brian Choy 969de1ebc9 Fix typo in writing-helmfiles.md example (#1263) 2020-05-19 10:04:38 +09:00
Danny Shemesh ceb108a24f Fixed part splitting for CLRF separated files (#1265)
Fixes https://github.com/roboll/helmfile/issues/1142

desired_state_file_loader.go
- Will now normalize the content before splitting it to parts

context:
Me & and a fellow dev have tried to figure out why helmfile didn't fill in certain values on his machine; 
turns out, he'd mistakenly checked out our project w/ CRLF line endings, which had caused part splitting to not work (as it's hard coded to look for '\n').

The following was acted on as a single part, causing values from the bases not to be available in the next yaml part:

```
bases:\r\n
  - base.yaml\r\n
---\r\n

releases:
  - name: external-secrets-crd
    ... some templated yaml ...
```

I've thought about regex-ing it out instead of replace-all, but benchmarks had shown that a plain replace is faster.

I've also considered splitting by "\n---" instead of "\n---", but that would break if the dashes were to continue with some other text.
2020-05-19 10:04:12 +09:00
hacker65536 576a07f497 fix: typo in README (#1254) 2020-05-12 09:42:07 +09:00
Craig Furman eb8ab54865 Optionally error when subhelmfiles do not exist (#1245)
This prevents typos in helmfiles from silently preventing operations
from running. The default behavior is to print a warning, to preserve
backwards compatibility.
2020-05-12 09:41:39 +09:00
Yusuke Kuoka 9e4bc6e7fc chore: Add retries for downloading kubectl in docker builds 2020-05-09 14:31:52 +09:00
James F McMahon ef2d8853c2 README: Add note about environment variable types (#1251)
Fixes https://github.com/roboll/helmfile/issues/1248
2020-05-09 14:12:52 +09:00
machine424andayoub mrini 66f34580c3 feat: Add global --debug flag for passing --debug to helm (#1236)
Add a global flag --debug that sets the logging level to debug and passes the global flag --debug to Helm through the implementation of subcommands’ --args.

Resolves #1104

Co-authored-by: ayoub mrini <amrini@wiremind.fr>
2020-05-09 14:11:54 +09:00
Guillaume Perrin 6d5f8c71cb Allow skipping chart-related operations when installed: false (#1233)
Resolves #1232

CHANGES:

* Disable repo update when installed is false

When install is false, we do not need to update the repositories and get
the chart.

Signed-off-by: Guillaume Perrin <guillaume28.perrin@gmail.com>
2020-05-09 14:03:56 +09:00
刘相轩 23be9af2db Show executed command when errors occurred (#1252) 2020-05-09 13:35:31 +09:00
Cédric de Saint Martin 1e9e228a9a Dockerfile: Upgrade to Helm v3.2.1. (#1253) 2020-05-09 13:34:46 +09:00
Rick StokkingreefandRick Stokkingreef c72cab80b5 Bump variantdev/vals for Vault AppRole support (#1239)
Co-authored-by: Rick Stokkingreef <rick.stokkingreef@takeaway.com>
2020-05-02 09:40:16 +09:00
Victor Noël 923bd54db0 Use namespace for release unicity with helm3 (#1213) (#1235)
This is for #1213 so that the releases are considered scoped to their namespace and not to tiller namespace.

Fixes #1213
2020-05-02 09:35:51 +09:00
nejtr0nandAlexey Shishkin a0e30c5193 Correct ordering between releases and sub-helmfiles on destroy/delete (#1234)
Fixes #1192

Co-authored-by: Alexey Shishkin <a.shishkin@directheroes.com>
2020-05-01 09:06:13 +09:00
Cédric Lamalle daf8c2f12b Upgrade helm to v3.2.0 (#1231)
* Upgrade helm to v3.2.0
2020-04-29 15:00:49 +09:00
Craig Dunford d71ac15748 Enhance createNamespace error handling (#1227)
- An error is raised if createNamespace is set explicitly and the
  helm version being used is not 3.2+
2020-04-27 08:59:44 +09:00
Craig Dunford eeb61e6174 Support for createNamespace (#1226)
- createNamespace is a new attribute that can be added to helmDefaults
  or an individual release to enforce the creation of a release namespace
  during sync if the namespace does not exist. This leverages helm's
  (3.2+) --create-namespace flag for the install/upgrade command. If
  running helm < 3.2, the createNamespace attribute has no effect.

Resolves #891
Resolves #1140
2020-04-26 10:41:40 +09:00
Marcin Kaciuba b1190508b2 feat(template): added secret template function (#1221)
* feat(tmpl): added fetchSecretValue template function

This adds a tmpl `fetchSecretValue` and `expandSecretRefs` function by:
- Adding:
    - `expandSecretRefs` function in tmpl package that uses vals
    package to fetch secrets
    - `fetchSecretValue` function in tmpl package like below but for
    single string value
    - gomock for tests purpose
- Changing:
    - move init of vals package to function (so the same instance can be used for template values and rendering the whole template)

* doc(secret): added doc how to use new tmpl methods

Added example usage of `fetchSecretValue` and `expandSecretRefs`
2020-04-25 21:10:02 +09:00
Ash Caire 3a19a394ab Add condition release attribute (#1190) 2020-04-23 08:43:34 +09:00
Jesse Glick dcb19f942b Typo: kustommize (#1224) 2020-04-23 08:38:33 +09:00
Eric Bailey 458385d971 Amend examples which broke lock files in README.md (#1225)
Prior to this change, these two examples suggested enclosing a release spec in
an if expression, which breaks the lock file when switching environments.

This change parameterizes `installed` instead of the entire release, thus
preserving the dependency locking functionality.
2020-04-23 08:38:01 +09:00
chenrui b89fba1cc2 Bump golang to v1.14.2 (#1132)
* Bump golang to v1.14.2

* Use gotest.tools/v3
2020-04-21 09:06:29 +09:00
KUOKA Yusuke 6b6b86a0b5 Bump variantdev/vals to v0.4.0 (#1217)
For GCP Secrets Manager support
2020-04-18 23:22:23 +09:00
Rene Hernandez e0a793b7c5 New output flag for list command (#1215)
* New output flag for list command

Support output as json
Add new formatters file to handle extrac formatting to its own concern
New config interface to support list command specification

* Fix usage message

* Add error handling for formatters
2020-04-18 21:11:12 +09:00
Danny Shemesh 71bb7354e7 Fix: populate .Values regardless of prestate success (#1202)
This commit proposes a potential solution for
https://github.com/roboll/helmfile/issues/1201

The gist is that, if prestate rendering fails, for any reason,
we do not populate the .Values in the second pass renderer.

I think that what have been expected in this case is to populate the
.Values irregardless.

pkg/app/two_pass_renderer.go
- Migrated to use finalEnv.GetMergedValues()

pkg/environment/environment.go
- Introduced GetMergedValues, which merges the environment's defaults
and current values, and then casts the keys to string;
This was previously defined in HelmState.Values() - however, as this
method is only concerned with the environment, I think it's more
appropriate for it to sit here.

pkg/state/state_exec_tmpl.go
- Extracted out HelmState.Values() to environment.go, see above
2020-04-17 09:18:01 +09:00
KUOKA Yusuke 3a34d28d7c fix: helmDefaults.timeout and releases[].timeout not working for helmfile test (#1211)
Fixes #1191
2020-04-17 09:08:21 +09:00
Douglas Hellinger f39d7928bf Put tiller on path for use in helm-tiller plugin (#1198)
Put tiller binary on the path in helm v2.X container image so that helm-tiller plugin will use it instead of duplicating the download of helm tarball and performing version check.
2020-04-14 09:18:18 +09:00
Vadim Bauer e0811dffa2 Update helmfile.yaml example in README (#1189)
- Adds default comments into helmfile.yaml example

- Fixes some typos

Signed-off-by: Vadim Bauer <vb@8gears.com>
2020-04-10 08:27:09 +09:00
RaymondKYLiuandRaymond Liu 7d11f5dedc feat: add tmpl function required (#1188)
Co-authored-by: Raymond Liu (RD-TW) <raymond_liu@trend.com.tw>
2020-04-10 08:23:42 +09:00
KUOKA Yusuke 870cc03c70 feat: helmfile diff --detailed-exitcode should also detect deletions (#1186)
Resolves #499
Resolves #1072
2020-04-10 08:22:33 +09:00
KUOKA Yusuke 98886df5d2 fix: .Values is missing keys with zero values (#1185)
Ref #1184
2020-04-10 08:21:23 +09:00
RaymondKYLiuandRaymond Liu 71635caace feat: add option --include-tests for diff and apply command (#1179)
Co-authored-by: Raymond Liu (RD-TW) <raymond_liu@trend.com.tw>
2020-04-05 17:43:54 +09:00
KUOKA Yusuke 486be0970d Bump vals to 0.3.0 (#1175)
vals v0.3.0 introduces the terraform output source that can be accessed by `ref+tfstate://path/to/tfstatefile/type.resourcename.prop` syntax. Please see the updated README section of vals for more info.
2020-04-04 19:50:01 +09:00
KUOKA Yusuke 9d7d2de6f5 Fix misleading helmfile diff output (#1174)
Fixes #749
2020-04-04 17:39:20 +09:00
KUOKA Yusuke 04c963d050 feat: Complete helmfile diff output when diff exists (#1173)
Fixes #874
2020-04-04 16:24:01 +09:00
KUOKA Yusuke a2c62c9e73 feat: Emit clear error message instead of panic on empty chart name (#1171)
Resolves #999
2020-04-04 15:32:22 +09:00
KUOKA Yusuke 1654ce4c18 Add regression tests for environment values merge (#1170)
This is a follow-up for #1169 and it also relates to #1168
2020-04-04 14:27:29 +09:00
KUOKA Yusuke 25aae679b1 Merge pull request #1169 from elodani/master
fix: empty map merge was not working after mergo bump
2020-04-04 14:00:41 +09:00
Daniel Elo d404356230 feat: fix empty map merge after mergo bump
since mergo had been bumped to 0.3.9 an environment value like:
```
affinity: {}
```
was not merged properly (not merged at all) instead it threw an error
that it cannot find the key "affinity" in the environment values even
though it was outputted in debug output as read in properly.
2020-04-03 15:01:27 +02:00
KUOKA Yusuke 51ecd12360 Fix panic while loading environment secrets (#1164)
This fixes a regression in #1160
2020-03-30 20:13:40 +09:00
KUOKA Yusuke 6643a41ea3 fix: merge environment values by ovewriting with empty values (#1162)
Fixes #1154
2020-03-29 20:47:23 +09:00
KUOKA Yusuke 35e5454994 Change the helmfile list header "INSTALLED" -> "ENABLED" (#1161)
Resolves #1076
2020-03-29 19:11:58 +09:00
KUOKA Yusuke 69feadc360 feat: helmBinary in helmfile.yaml (#1160)
* feat: `helmBinary` in helmfile.yaml

Resolves #1083

* Add regression test for `helmfile destroy`
2020-03-29 17:51:07 +09:00
KUOKA Yusuke f676c61425 feat: Better exec error reporting (#1159)
Enhances Helmfile to print more helpful message on error while calling `exec` template function.

Helmfile has been printing error messages like the below:

```
in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at <exec "./exectest.sha" (list)>: error calling exec: exit status 1
```

Adding captured stdout and stderr, with some indentation to make it readable, it now produces the following message on missing executable:

```
$ make build && ./helmfile build
go build
in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at <exec "./exectest.sha" (list)>: error calling exec: fork/exec ./exectest.sha: no such file or directory

COMMAND:
  ./exectest.sha

ERROR:
  fork/exec ./exectest.sha: no such file or directory
```

On non-zero exit status without output:

```
$ make build && ./helmfile build
go build
in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at <exec "./exectest.sh" (list)>: error calling exec: exit status 1

COMMAND:
  ./exectest.sh

ERROR:
  exit status 1
```

On non-zero exit status with output:

```
$ make build && ./helmfile build
go build
in ./helmfile.yaml: error during helmfile.yaml.part.0 parsing: template: stringTemplate:5:8: executing "stringTemplate" at <exec "./exectest.sh" (list)>: error calling exec: exit status 2

COMMAND:
  ./exectest.sh

ERROR:
  exit status 2

COMBINED OUTPUT:
  out1
  err1
```

Resolves #1158
2020-03-29 10:49:02 +09:00
KUOKA Yusuke 20a39e9412 Bump mergo to 0.3.8 (#1153)
Fixes #1150
2020-03-20 18:56:43 +09:00
KUOKA Yusuke 048c79ad34 Bump variantdev/vals to 0.2.0 (#1152)
For support for the advanced AWS profile usage in retrieving AWS SSM params/secrets https://github.com/variantdev/vals/issues/19
2020-03-20 16:00:21 +09:00
KUOKA Yusuke 994e4b66fc fix: exec template func should not throw away stdout when stdin is non empty (#1151)
Fixes #1149
2020-03-20 12:23:34 +09:00
KUOKA Yusuke 499f6ec6e3 Add Sportradar to USERS.md (#1133) 2020-03-05 08:09:17 +09:00
KUOKA Yusuke bf22502a2f feat: add helmfile template --validate (#1135)
This adds the ability for helmfile to call `helm template --validate` introduced in helm 3.

fixes #1105
2020-03-05 08:07:02 +09:00
Eric Bailey 7c80a859fa Fix awkward dependency error message (#1130) 2020-03-02 08:23:03 +09:00
Gleidson Nascimento 00c4422a64 Add validation for empty repository names (#1128)
Resolves #1112
2020-02-28 19:42:15 +09:00
KUOKA Yusuke af44965949 feat: helmfile --log-level=debug apply --retain-values-files (#1127)
`--retain-values-files` prevents temporary values files that were passed to Helm commands run by Helmfile for debugging purpose.

With that, you can manually rerun helm commands that were logged when `--log-level=debug` is enabled.

Resolves ##1117
2020-02-28 19:39:01 +09:00
KUOKA Yusuke 0186254e79 feat: --detailed-exitcode for helmfile apply (#1120)
Resolves #1113
2020-02-26 21:09:05 +09:00
jfreeland 8acbbc596d new here, fixing very minor typo (#1121) 2020-02-26 10:33:35 +09:00
KUOKA Yusuke f1bdb65777 fix: delete/destroy ordering within directory (#1119)
Fixes #979
2020-02-25 09:46:01 +09:00
Mark Henneman 322ed4d1d8 fix explanation about secrets in .Environment.Values (#1116) 2020-02-24 17:00:11 +09:00
刘相轩 c4e6160220 Remove unneeded env HELMFILE_HELM3 since #963 (#1109) 2020-02-21 00:12:26 +09:00
KUOKA Yusuke 10f2a576c5 fix: helmfile -n ns statuses should set ns for helm 3 (#1108)
This fixes the issue reported in Slack https://sweetops.slack.com/archives/CE5NGCB9Q/p1582088381195000
2020-02-20 22:30:02 +09:00
chenrui 2579a876a6 Upgrade deps (#1100)
* Upgrade helm3 to v3.1.0

* Upgrade helm2 to v2.16.1

* Use golang v1.13.7
2020-02-17 08:43:14 +09:00
Alex Jones 6b3277ce71 Added AMEX to USERS.md (#1101) 2020-02-17 08:41:41 +09:00
Anton Trifonov 27425bc4d2 Fix status command for helm 3 (#1098) 2020-02-13 08:40:20 +09:00
Emil 05add478c1 Add option to suppress diff on apply (#1092)
* Add option to suppress diff on apply

Add --supress-diff option on apply. Usable for fresh installs when a
lot of output is produces by diff.

Resolves #458

* fix tests for suppress-diff
2020-02-05 21:29:55 +09:00
abdennour 757e748e3f Feature/autobuild docker image for each release thru dockerhub auto builds (#886)
* feat: build hook for dockerhub autobuilds

* release new image

* Create pre_build

* Update push
2020-02-03 08:45:51 +09:00
chenrui e6f1db128c Upgrade alpine to 3.11 (#1085) 2020-02-03 08:43:25 +09:00
Daniel Poelzleithner 5fb037c8e3 Fix crash on missing chart (#1087)
When chart is not set, an exception without any hints occure due
normalizeChart tries to access the first character of a 0 length string.
Properly inform user of missing chart.
2020-02-03 08:42:54 +09:00
Guillaume Lecerf 5abbdea059 Fix "Merging Arrays in Layers" doc example (#1084) 2020-02-03 08:42:16 +09:00
Christoph Hösler 9fab6851f0 fix test command for helm 3 (#1080) 2020-02-03 08:41:38 +09:00
Matt L c8ba676e19 Update writing-helmfile.md (#1073)
nitpick spelling
2020-02-03 08:39:40 +09:00
a-hat fc75f25293 do not pass --api-versions to "helm diff" (#1061) 2020-01-08 08:48:29 +09:00
KUOKA Yusuke b044820607 fix: helmfile destroy does not delete helm releases in status : PENDING_INSTALL (#995)
Fixes #539
2019-12-27 09:33:28 +09:00
KUOKA Yusuke 0e1aeb43ce Fix deps error for helm 3.0.2 (#1056)
Fixes #1052
2019-12-27 09:15:07 +09:00
Andrew Nazarov 88adc56551 Upgrade to the latest Helm 3 release (#1049) 2019-12-27 08:31:10 +09:00
a-hat 9cf6b59cd8 feat: Option to pass apiVersions to helm diff and helm template (#1046)
This makes it possible to pass the API Capabilities to helmfile when executing a task that does not render against an actual cluster (diff, template, apply).

Resolves #1014
2019-12-27 08:30:39 +09:00
Jake Hill fb7cc60afe Upgrade to latest stable Helm 3 (#1024)
Signed-off-by: Jake Hill <jake@naphta.uk>
2019-12-17 10:38:02 +09:00
Yujun Zhang 90c7b04dbf Fix invalid link in README.md (#1026) 2019-12-17 08:29:13 +09:00
刘相轩 ac534a897b Add historyMax (#1022)
Resloves #868
2019-12-17 08:27:42 +09:00
Maria Kotlyarevskaya 314557c824 Fix typo in README (#1032) 2019-12-17 08:25:11 +09:00
eddycharly 4b2d6946be Add indexed key support to --state-values-set (#1042)
Adds support for indexed key arguments and escaped dot arguments to `--state-values-set`.
Things like `--state-values-set config\.yml.something=abc` or `--state-values-set config.something[0]=abc` can be passed in the command line.

Resolves #899
2019-12-17 07:31:42 +09:00
Jake Hill 03898b7a98 Fix Helm test having run subcommand with helm3 (#1023)
It looks like during the beta phase the testing functionality was tweaked / changed back during the beta phase but has since been returned back to how it worked originally.

RE: https://github.com/helm/helm/commit/a6f4bc1bc0fc721aa501f7b8667928b2ec4f0016
Signed-off-by: Jake Hill <jake@naphta.uk>
2019-12-11 09:20:41 +09:00
刘相轩 4b1b19f8a6 feat: Persist Helmfile version in helmfile.lock for version compatibility check (#1016)
Reslove #698
2019-12-11 09:19:18 +09:00
KUOKA Yusuke 07c42474cc Do not fail due to missing env in base helmfile (#1009)
When helmfile is run with `--environment NAME` and there was a base hemlfile that misses `environments`, helmfile had been trying to load env values for NAME and failing.

A base helmfile is allowed to reference values from within itself, but that's optional. In other words, a base helmfile that misses the env is okay as long as it doesn't self-reference env values.

So, this change allows missing env and env values while loading base helmfile. After loading, a base helmfile can fail due to referencing missing env values, but that's okay.

Fixes #1008
2019-12-01 11:37:56 +09:00
KUOKA Yusuke 27c098c17c Fix capitalization of cleanup-on-fail config key (#1005)
It is how `cleanupOnFail` to be consistent with other keys.

Ref #969
2019-11-29 09:29:26 +09:00
ldealmei e745a2602b Fix typos (#1003) 2019-11-29 08:55:13 +09:00
Harshal Shah 04379cee80 Add the cleanup-on-fail flag to upgrade command (#969)
* Adding the cleanup-on-fail flag to upgrade command

* Adding tests and making the flag optional

* Updating readme with new flag details

* go fmt
2019-11-29 08:47:19 +09:00
KUOKA Yusuke 25599eae89 Fix ref url in values key to actually work (#998)
Sorry it was not working at all :(

See https://github.com/roboll/helmfile/issues/990#issuecomment-557756343 for how this is supposed to work.

Fixes #990
2019-11-23 11:36:44 +09:00
KUOKA Yusuke 8ea144e31f Validate on recursion (#993)
Fixes #983
2019-11-21 22:40:53 +09:00
KUOKA Yusuke f508b9091e Fix regression in the order of processed releases when concurrency is 1 (#992)
Fixes #988
2019-11-21 22:29:05 +09:00
KUOKA Yusuke 7ec92c0520 Bump variantdev/vals (#991)
For the new feature that allows you to merge-in YAML/JSON objects stored in a secret store.

Resolves #990
2019-11-21 22:10:21 +09:00
刘相轩 06a3b558e0 Disable tillerless for helm3 (#987) 2019-11-21 21:53:38 +09:00
KUOKA Yusuke 8a2311d033 Bump variantdev/vals (#985)
To accomodate the fix for https://github.com/variantdev/vals/issues/13
2019-11-20 08:32:01 +09:00
shikhar7 312ca76751 Added the plugin with right version for plugin (#984) 2019-11-19 22:00:52 +09:00
chenrui 80dcdfb821 Update README per v0.92.1 release (#975) 2019-11-18 08:51:10 +09:00
Andrew Nazarov f08da43e6c Remove a helm-tiller plugin from a Helm 3 docker image (#977)
Resolves #976
2019-11-18 08:47:06 +09:00
KUOKA Yusuke 468b9b659d Fix random "expansion errors" in large values contained in values (#974)
Those are not actually random but would have looked like so. We use an external go pkg `variantdev/vals` to expand urls like `ref+vault://foo/bar` contained in release values into their respective secret values.

There was a bug in `vals` that it tries to expand unintended types of strings which resulted in confusing errors like reported in #973.

`vals` fixed the issue in https://github.com/variantdev/vals/commit/ba4c7a29875ab5fea5df80d7c25b5d6ccdce07c0. This commit upgrades `vals` to accomodate that.

Fixes #973
2019-11-17 09:37:45 +09:00
Jeroen Demeyer 3ccb43fbb6 Log "could not deduce environment: block" message at debug level (#967) 2019-11-15 08:52:49 +09:00
Jeroen Demeyer b99daa3605 Allow combining valuesTemplate and values, same for setTemplate (#938)
* Allow combining valuesTemplate and values, same for setTemplate

* Add testcase for valuesTemplate and setTemplate
2019-11-15 08:52:10 +09:00
KUOKA Yusuke 990d31c667 Merge pull request #963 from frontapp/auto-helm3
feat: Automatically enable Helm v3 mode
2019-11-15 08:40:37 +09:00
KUOKA Yusuke 45943f9306 Bump Helm to v3.0.0 (#965) 2019-11-15 08:40:20 +09:00
Andrew Drake dc2961a493 Add test for IsHelm3 logic. 2019-11-14 11:27:16 -08:00
Andrew Drake d15c4aad2d Replace usage of ShellRunner in test suite with mockRunner. 2019-11-14 11:15:03 -08:00
Andrew Drake c099f69d94 feat: Automatically enable Helm v3 mode
Runs `helm version` in helmexec.New, and exposes a method on Interface to allow other packages to use the detected version. Preserves compatibility with previous HELMFILE_HELM3 mechanism.

Resolves #923
2019-11-14 10:50:18 -08:00
Yusuke Kuoka 43f1188325 Bump Helm to v3.0.0 2019-11-14 21:13:11 +09:00
KUOKA Yusuke f466800e1a Fix regression since 0.90.0 that Helmfile becomes too slow when there are many releases (#964)
* Fix regression since 0.90.0 that Helmfile becomes too slow when there are many releases

Fixes #959

* Ensure that the up-to-date helm-diff is installed and used in integration tests
2019-11-14 20:57:04 +09:00
KUOKA Yusuke ada5f05b36 Merge pull request #942 from roboll/fix-rel-not-found-on-del-by-sync
fix: release not found on uninstall through sync/apply
2019-11-07 21:43:09 +09:00
Yusuke Kuoka a136c46c06 fix the bug that helmfile template is unable to render anything when --namespace is specified 2019-11-07 21:23:36 +09:00
Yusuke Kuoka e2e4e8440d fix the bug that resulted in helmfile sync not delete releases with --namespace 2019-11-07 20:48:55 +09:00
Yusuke Kuoka 77082cef58 fix regression that --namespace breaks delete/destroy and possibly sync/apply as well
The problem was that `--namespace NS` had been not taken into account while deleting releases, that resulted in releases that should be deleted are not deleted.
2019-11-07 19:53:27 +09:00
Yusuke Kuoka 8d7c79a323 fix: release not found on uninstall through sync/apply
The recent addition of the DAG support(`needs`) and the fixes on it broke the delete-on-sync functionality. And there were two more bugs. One is that it was not correctly running `helm delete` when needed and the another is that it was failing when `--selector` is specified and the releases to delete by sync found, but nothing actually got deleted. This fixes all of them.

Fixes #941
2019-11-07 10:09:54 +09:00
KUOKA Yusuke ed7a6d9051 Port the needs fix for helmfile apply to sync, and make template DAG-aware (#940)
This ports the fix for `helfmile apply` to `sync`, so that specifying `--selector` doesn't break `helmfile sync`.

Also make `helmfile template` DAG-aware, so that the manifests are rendered in the order of dependency.

Ref #919
2019-11-06 23:16:57 +09:00
KUOKA Yusuke 35adb8ae22 Merge pull request #939 from roboll/fix-needs-ported-to-delete
Port the `needs` fix for `helmfile apply` to `delete` and `destroy`
2019-11-06 22:41:37 +09:00
Yusuke Kuoka a0c902d6d1 Remove unnecessary code from pkg/app.Run 2019-11-06 22:33:05 +09:00
Yusuke Kuoka ebad0ad20f Remove unnecessary code 2019-11-06 22:30:08 +09:00
Yusuke Kuoka 4751613f39 Port the fix for needs to helmfile delete and destroy
This ports the fix for `helfmile apply` to `delete` and `destroy`, so that specifying `--selector` does not break those commands anymore.

Ref #919
2019-11-06 22:27:53 +09:00
KUOKA Yusuke f41fe86452 fix: Fix --selector to not break needs (#934) 2019-11-06 17:05:25 +09:00
chenrui dad8e72bc5 chore(deps): upgrade to go v1.13.4 (#935) 2019-11-06 07:56:15 +09:00
KUOKA Yusuke d0dd4a29af Build alternative container image for use with Helm v3 (#931)
Adds the Dockerfile for Helm v3(Dockerfile.helm3) that contains both Helmfile and Helm v3(currently 3.0.0-rc.2).

Changes the CircleCI workflow to include a job to build the v3 image on every PR. The release job is enhanced to build and push the image with a tag prefixed with `helm3-`. So the resulting image should be available via e.g. `quay.io/roboll/helmfile:helm3-v0.90.5`.
2019-11-02 16:30:18 +09:00
KUOKA Yusuke 8fc546ae86 Merge pull request #930 from roboll/helm-v3-fixes-and-integration-test
Make helmfile + helm v3 pass the integration test suite
2019-11-02 16:06:48 +09:00
Yusuke Kuoka fbbd7630e7 v3 compatibility: Fix helmfile delete to not fail when there was no release to be deleted
`helmfile delete` is designed to be idempotent. That is, it is safe to be run when there is nothing to delete. A change in helm v3 broke that behavior. This enhances Helmfile to be able to behave the same for helm v2 and v3.
2019-11-02 15:52:07 +09:00
Yusuke Kuoka 30751e94f7 Fix helmfile --namespace ns delete to not fail consistently
Until now `helmfile delete` with helm v3 had been failing when `--namespace` is provided. Helmfile was missing the fact that in helm v3 namespace must be passed instead of tiller-namespace to scope releases to be deleted.

```
err: release "b-1" failed: helm3rc2 exited with status 1:
  Error: uninstall: Release not loaded: b-1: release: not found
in ./helmfile.yaml: release "b-1" failed: helm3rc2 exited with status 1:
  Error: uninstall: Release not loaded: b-1: release: not found
```
2019-11-02 15:52:07 +09:00
Yusuke Kuoka f47d8ea256 Integration tests with Helm v3.0.0-rc.2 2019-11-02 15:52:01 +09:00
Jan Möller 6955d07ea8 fix: Set HelmBinary in execer constructor (#928)
This overrides the default helm command, if provided, as soon as possible.
This way it is already used in `visitStates`.


I ran into an issue using `HELM3` and `--helm-binary` together with helm-secrets. 
I previously used tillerless, which i could now remove. This however caused `DecryptSecret` to fail, as it would still use the helm2 binary; because it runs before the first `helm.SetHelmBinary` call.

While helm-secrets is not fully helm3 compatible yet, its uses within helmfile are, and i was able to just install it as a helm3 plugin.
2019-11-02 14:15:33 +09:00
KUOKA Yusuke 3f02b86640 fix: Fix needs to work for upgrades and when selectors are provided (#922)
* fix: Fix `needs` to work for upgrades and when selectors are provided

Fixes #919

* Add test framework for `helmfile apply`

* Various enhancements and fixes to the DAG support

- Make the order of upgrades/deletes more deterministic for testability
- Fix the test framework so that we can validate log outputs and errors
- Add more test cases for `helmfile apply`, along with bug fixes.
- Make sure it fails with an intuitive error when you have non-existent releases referenced from witin "needs"
2019-11-02 14:04:16 +09:00
chenrui e258052933 helm v2.15.2 (#924)
fix sha256

update sha256 again
2019-11-01 08:28:17 +09:00
KUOKA Yusuke 4c892edb7e Fix docker-login error on docker-build test against PR from fork (#921) 2019-10-31 08:48:41 +09:00
Tom Duffield 464e6bc782 Run vals against values files that contain the syntax (#920)
If we specify ref+ syntax in a values file, run vals against that file.

Signed-off-by: Tom Duffield <tom@chef.io>
2019-10-31 08:48:30 +09:00
KUOKA Yusuke 78bc481675 Fix SSM integration (#915)
The fragment variant of the URIs e.g. `foo: ref+awsssm://path/prefix#key` was not working. This change fixes that by bumping `vals` which is the library providing the SSM support, along with other dependencies.
2019-10-30 16:57:56 +09:00
Yusuke Kuoka a42c098727 Ensure the docker build to pass 2019-10-28 13:39:30 +09:00
Yusuke Kuoka fc0a7b68a8 fix unexpectedly high concurrency since the dag support 2019-10-28 13:25:25 +09:00
Yusuke Kuoka 88caa7514f fix potential race in sync 2019-10-28 13:24:27 +09:00
Yusuke Kuoka 2c7810a623 Test the docker build on every PR 2019-10-28 13:04:11 +09:00
KUOKA Yusuke 7666e95690 feat: Add needs: [NS/NAME] for controlling installation/deletion order declaratively (#914)
Introduces DAG-aware installation/deletion ordering to Helmfile.

`needs` controls the order of the installation/deletion of the release:

```yaml
relesaes:
- name: somerelease
  needs:
  - [TILLER_NAMESPACE/][NAMESPACE/]anotherelease
```

All the releases listed under `needs` are installed before(or deleted after) the release itself.

For the following example, `helmfile [sync|apply]` installs releases in this order:

1. logging
2. servicemesh
3. myapp1 and myapp2

```yaml
  - name: myapp1
    chart: charts/myapp
    needs:
    - servicemesh
    - logging
  - name: myapp2
    chart: charts/myapp
    needs:
    - servicemesh
    - logging
  - name: servicemesh
    chart: charts/istio
    needs:
    - logging
  - name: logging
    chart: charts/fluentd
```

Note that all the releases in a same group is installed concurrently. That is, myapp1 and myapp2 are installed concurrently.

On `helmdile [delete|destroy]`, deleations happen in the reverse order.

That is, `myapp1` and `myapp2` are deleted first, then `servicemesh`, and finally `logging`.

Resolves #715
2019-10-28 12:57:25 +09:00
KUOKA Yusuke b8f24948bb Bump vals to fix Vault integration (#911)
- `replace` for thrift had to be removed to avoid the error `go: github.com/apache/thrift@v0.12.0 used for two different module paths (git.apache.org/thrift.git and github.com/apache/thrift)`
- Other dependencies are updated by running `go get -u github.com/variantdev/vals`
2019-10-28 08:22:04 +09:00
chenrui 730ae35c73 Bump to Helm 2.15.1 (#909)
* Bump to helm v2.15.1

* `go mod tidy` dependencies
2019-10-26 20:16:04 +09:00
Kyrylo Lebediev 4680010c60 Add integration with Hashicorp Vault, AWS SSM, SecretsManager (#906)
* feat: Add integration with Hashicorp Vault, AWS SSM, SecretsManager

Fields which are rendered: Release.Values, Release.SetValues.Value, Release.SetValues.Values

Example:
```
values:
- foo: ref+vault://mykv/foo?address=http://127.0.0.1:8200#/mykey
set:
- name: xyz
  values:
  - ref+vault://mykv/foo?address=http://127.0.0.1:8200#/mykey3
```

Resolves #881

* feat: Update integration with variantdev/vals

New ref+.\* secret formats are used:
https://github.com/variantdev/vals/tree/6565695a031f407a1066bdd31ae313db6a33ec0a#suported-backends

Resolves #881
2019-10-25 22:55:26 +09:00
sethp-nr 849ed63999 Run "helm init --client-only" in Dockerfile (#907)
In trying to use the base quay.io/roboll/helmfile image to deploy a chart that depended on an external repository, I ran into this error:

```
Adding repo bitnami https://charts.bitnami.com/bitnami
in ./helmfile.yaml: in .helmfiles[0]: in dirac/helmfile.yaml: helm exited with status 1:
  Error: Couldn't load repositories file (/root/.helm/repository/repositories.yaml).
  You might need to run `helm init` (or `helm init --client-only` if tiller is already installed)
```

The minimum to work around that error would be approximately:

```
mkdir /root/.helm/repository && echo apiVersion: v1 > /root/.helm/repository/repositories.yaml
```

Though there might be other things that I'm unwittingly depending on in the base `helm init`, it should be more friendly if we just run `helm init --client-only` while building.
2019-10-24 08:39:43 +09:00
chenrui 9990242d60 Upgrade to go v1.13.3 (#903) 2019-10-23 05:25:19 +09:00
Jarryd Tilbrook 4294a5de06 feat: Enhance postsync event to include .Error (#882)
* Refactor postsync event to include success flag

* Change success boolean to error

Fixes #826
2019-10-17 08:27:12 +09:00
Liam Dawson d6bd317c9c Update templating example typo (#895) 2019-10-17 08:25:24 +09:00
KUOKA Yusuke fdca10678b fix(helm3): Error: unknown flag: --purge on deletion via helmfile sync (#890)
Fixes #888
2019-10-10 16:54:15 +09:00
Martin Devlin f51949ac3f Add ACR notes (#889) 2019-10-10 16:52:32 +09:00
chenrui 76b3a17427 Upgrade to go v1.13.1 (#880) 2019-10-10 16:51:52 +09:00
bitsofinfo 2264da3ac9 updated to production use (#885) 2019-10-05 16:44:59 +09:00
bitsofinfo cf9bbc7603 upgrade sprig 2.22.0 #883 (#884) 2019-10-05 16:43:40 +09:00
Aaron Batilo 921f69bae7 Warn users when no repositories are defined (#879)
At the moment, if you have a helmfile.yaml like so:
```
releases:
  - name: metrics-server
    namespace: kube-system
    chart: stable/metrics-server
```

If you try to run `helmfile deps`, you will get a 0 exit code and no log
output at whatsoever, signaling that there weren't any problems, but no
lock file will get created.

For example:
```
root@316073d4a104:/# helmfile deps
root@316073d4a104:/#
```
This behavior doesn't appear to be documented and is unintuitive to the
user.

This change adds a warning output for this same use case:
```
root@316073d4a104:/# helmfile deps
There are no repositories defined in your helmfile.yaml.
This means helmfile cannot update your dependencies or create a lock file.
See https://github.com/roboll/helmfile/issues/878 for more information.
root@316073d4a104:/#
```

Fixes #878
2019-09-29 16:11:06 +09:00
Rajat Goyal 10a9a16f3d Fix: Change use of tmpl to gotmpl in README (#870)
This adds clarity in docs by:

- Changing references to the supported file extension
- Previously, using `values.tmpl` in helmfile.yaml would throw errors. `values.gotmpl` gives expected output
2019-09-26 09:02:21 +09:00
art konandKUOKA Yusuke ba2e522617 doc: Added some detail on how to use override values in helmfiles section (#861)
* Added some detail on how to use override values in helmfiles section

Co-Authored-By: KUOKA Yusuke <ykuoka@gmail.com>
2019-09-20 10:21:53 +09:00
KUOKA Yusuke 2e98e907b0 fix: invalid duration passed to helm 3 upgrade (#864)
Fixes #863
2019-09-19 08:30:29 +09:00
Mike Splain b762ab0b78 Fix delete/destroy (#859) 2019-09-17 15:04:04 +09:00
Theo Meneau 216c228c0b feat: helm repo add --ca-file via repositories definition (#856)
Resolves #855
2019-09-14 06:23:54 +02:00
KUOKA Yusuke ef63a05513 fix(helm3): delete/destroy/apply/sync unable to detect releases to be deleted (#857)
Fixes #853
2019-09-14 06:23:22 +02:00
art kon 06b0c99a0b Fix recursion for helmfiles pulled from git (#854) 2019-09-14 05:49:59 +02:00
eddycharly fd0133e10a Update documentation and tests for .Values (#839)
Resolves #816
2019-09-14 05:35:16 +02:00
KUOKA Yusuke 9d851cda3b feat: --skip-repos for helmfile deps (#851)
Resolves #661
2019-09-12 19:33:18 +09:00
KUOKA Yusuke f79db2ec8d feat(diff,apply,lint,sync,template): --set k=v for setting adhoc chart values (#850)
Resolves #840
2019-09-12 19:24:43 +09:00
KUOKA Yusuke 94a6fcfb9f feat(diff,apply): --context=N for limiting diff context (#849)
Resolves #787
2019-09-12 18:36:13 +09:00
KUOKA Yusuke fb2041555e feat(diff,apply): --no-color for removing color from output (#848)
Resolves #788
2019-09-12 18:32:30 +09:00
刘相轩 cbf5b8b1e7 Fix helm2 lock file does not get updated (#847)
Ref: https://github.com/helm/helm/issues/2731
2019-09-12 17:58:16 +09:00
matthias-kloeckner 267e0fa1fe adding info about kloeckner-i to users list (#843) 2019-09-11 12:48:00 +09:00
Shane Starcher 5488198d6d fix: allow empty pattern matching and move on (#827)
Ref #778
2019-09-11 12:47:00 +09:00
KUOKA Yusuke 4bbd09ccb2 fix(cmd/deps): make helmfile deps to work w/ helm 3.0.0-beta.3 (#842)
Ref https://github.com/roboll/helmfile/issues/668#issuecomment-529054165
2019-09-07 22:06:29 +09:00
KUOKA Yusuke 4e4f1bee59 feat: Experimental Helm v3 mode (#841)
Set `HELMFILE_HELM3=1` and run `helmfile` like `HELMFILE_HELM3=1 helmfile ...`.

When `HELMFILE_HELM3` is set, `test`, `template`, `delete`, `destroy` behave differently so that it works with Helm 3.

Note that `helmfile diff` doesn't work as `helm-diff` called under the hood doesn't support Helm v3 yet.

Ref #668
2019-09-07 09:40:44 +09:00
a-hat 6dfc9be0d1 doc: Update documentation about layering release values (#837)
Closes #836
2019-09-06 09:29:08 +09:00
chenrui 1823bb1e5b Fix apache thrift location issue (#835)
Relates to Homebrew/homebrew-core#43728
Fixe #834
2019-09-04 12:19:14 +09:00
KUOKA Yusuke 4bc99337b2 Make advanced templating backward-compatible (#833)
This is a follow-up for #823
Ref https://github.com/roboll/helmfile/pull/823#discussion_r319712283
2019-08-31 22:37:46 +09:00
Tibo Beijen 01ae59fedd add helm-tiller plugin to docker image (#825) 2019-08-31 14:38:36 +09:00
Shane Starcher 2a6bd24e3c move context outside to limit duplicate repo updates (#828) 2019-08-31 14:32:48 +09:00
astorath 11d0abba6e feat: Advanced Templating (#823)
1. Added `helmfile build` command to print final state
Motivation: useful for debugging purposes and some CI scenarios

Ref #780 

2. Template interpolation is now recursive (you can cross-reference release fields) like:
```yaml
templates:
  release:
    name: {{`app-{{ .Release.Namespace }}`}}
    namespace: {{`{{ .Release.Labels.ns }}`}}
    labels:
      ns: dev
```
3. Experimental: Added some boolean release fields interpolation in templates:
```yaml
templates:
  release:
    name: {{`app-{{ .Release.Namespace }}`}}
    namespace: dev
    installedTemplate: {{`{{ eq .Release.Namespace "dev" }}`}}
```

Resolves #818

4. Added more template interpolations: Labels, SetValues
5. Added template interpolation for inline Values
6. Added `helmfile list` command to print target releases in simple tabular form
7. Added release names in some `helm` output messages, e.g.: `Comparing release=%v, chart=%v`
2019-08-31 14:31:31 +09:00
Yusuke Kuoka dd58badf81 release: skip building against s390x
Please notice me if anyone is using Helmfile on it
2019-08-24 10:50:18 +09:00
Yusuke Kuoka e24c15324b Revert "release: skip downloading go modules in container image builds (#821)"
This reverts commit ba0891b7b7.
2019-08-24 10:37:01 +09:00
KUOKA Yusuke ba0891b7b7 release: skip downloading go modules in container image builds (#821)
So that the release process can be (probably) 2x faster
2019-08-24 10:29:26 +09:00
KUOKA Yusuke b58e082a8f Merge pull request #820 from roboll/fix-incorrect-remote-cache-on-miss
fix: clean up invalid remote state file cache
2019-08-24 10:01:48 +09:00
Yusuke Kuoka bf9dcc0982 release: Make the release process faster 2019-08-24 09:59:46 +09:00
Yusuke Kuoka cd5d906afb fix: clean up invalid remote state file cache
Fixes #815
2019-08-24 09:47:49 +09:00
Travis Groth a584aeab2e Share helmexec from State Creation (#804)
Closes #444 and #782 

This is the final PR to fully cache and parallelize helm secret decryption.  It threads the shared helmexec.Interface into the StateCreator and HelmState structs to be used during environment secret decryption.  This should effectively cache secrets for the duration of a helmfile run, regardless of where they are first decrypted.
2019-08-15 09:27:55 +09:00
Travis Groth 765bfe6cfd Handle environment secrets concurrently
Ref #782
2019-08-13 09:23:29 +09:00
Jake Hill 622cba9f19 Add kubectl and jq to Dockerfile (#799)
* Add kubectl and jq to Dockerfile

Signed-off-by: Jake Hill <jake@naphta.uk>

* Update kubectl to use checksum verification, and pin to a specific version

Signed-off-by: Jake Hill <jake@naphta.uk>

Resolves #792
2019-08-08 21:24:46 +09:00
Travis Groth 63a337ee57 Base concurrency on items parameter (#798)
Fixes #793
2019-08-08 10:20:05 +09:00
Travis Groth 6baad71b1f Cache secrets and concurrent decryption (#790)
Related to #782 and #444 

- Allows concurrent decryption of different secrets files
- Caches decrypted secrets by original file path and returns decrypted results from memory
- Secrets being run through an instance of helmexec will be cached and run as fast as possible concurrently

NB: This particular PR doesn't make _all_ calls to secrets cached and concurrent.  Environment Secrets in particular seem to not be evaluated with a ScatterGather(), and doesn't use the same helmexec instance as other parts of the code, so it doesn't take advantage of these changes.  Some reworking of the plumbing there would be needed.
2019-08-07 23:00:19 +09:00
OlivierB bce2f4728b fix: pass namespace to helm template command (#771)
Resolves #770
2019-07-30 10:44:42 +09:00
a-hat e5038fb04f print yaml content on error in fromYaml (#765) 2019-07-30 10:43:30 +09:00
bitsofinfo fe5102e777 users update (#750) 2019-07-30 10:42:47 +09:00
KUOKA Yusuke eda961edc2 fix: --state-values-set not setting more than first child in nested paths (#774)
Fixes #773
2019-07-27 22:57:54 +09:00
KUOKA Yusuke b2a6231dcf fix: absolute chart path (#753)
Resolves #743
2019-07-12 22:37:54 +09:00
Marc Sutter 4166b418c9 feat: Adopt existing K8s resources as Helm release on install (#746)
Use with the helm-x support(#673)

This enhances config syntax to accept adopt: [NS/KIND/RESOURCE_NAME] at the release level so that helmfile calls helm-x to transparently import existing resources at the installation time.

Resolves #84
2019-07-11 09:13:34 +09:00
Naseem 408bf7b8bf Update USERS.md (#744) 2019-07-11 09:10:13 +09:00
OlivierB 2f9f52033c feat: add --output-dir on template command (#693)
It generates templates in a subdirectory named "stateFileName-stateFileHash-releaseName"
2019-07-11 09:07:46 +09:00
KUOKA Yusuke 63b5040ec4 fix: fatal error: concurrent map read and map write on concurrency > 1 (#742)
This is a revised version of https://github.com/roboll/helmfile/commit/41e44f74a37ebca0394d9ec4546c27208f8a1adf, which doesn't make CI angry and a more straight-forward(yet smelling) implementation

Fixes #737
2019-07-05 09:20:16 +09:00
bitsofinfo 4cc40cf0f1 Upgrade to sprig 2.20.0 and xstrings 1.2.0 (#735)
Needed for new functions and bug fixes

Resolves #733
Resolves #734
2019-07-02 10:12:14 +09:00
Clément Loiselet 1df9b0e9e9 fix: enabled '=' in a value for --state-values-set (#722)
This adds the possibility to use a "=" in the parameters passed to --state-values-set

previous comportment:
`helmfile --state-values-set 'test="abcde==fg",test2="abcde"'` => outputs 'test="abcde",test2="abcde"'

new comportment:
`helmfile --state-values-set 'test="abcde==fg",test2="abcde"'` => outputs 'test="abcde==fg",test2="abcde"'
2019-06-28 10:12:28 +09:00
KUOKA Yusuke b82f77ee50 fix: Absolute paths for -f not working on Windows (#721)
Fixes #718
2019-06-25 20:06:07 +09:00
KUOKA Yusuke e6bb8282db fix: remote helmfile from git::ssh source (#719)
The remote helmfile feature introduced by #648 was unable to be sourced from private git repositories due to URL parsing issue in helmfile. This fixes that.

Ref https://github.com/roboll/helmfile/issues/469#issuecomment-505236600
2019-06-25 12:45:56 +09:00
Max Williams b6a04d012c Change link in README to newer tag (again) (#717) 2019-06-21 18:31:00 +09:00
Max Williams 9eef44ffe6 Change link in README to newer tag (#716)
Closes #710

Fixed a couple very small typos also.
2019-06-21 18:18:36 +09:00
KUOKA Yusuke cfe309ec21 fix: do not panic on merging env secrets (#713)
Fixes #712
2019-06-21 00:10:11 +09:00
Erik Forsberg 65818cd345 feat: When values.yaml.gotmpl can't be parsed as YAML, print the YAML. (#703)
This helps a lot when debugging go templates.
2019-06-19 08:47:36 +09:00
sgandon ddb5be1b9d feat: optionally show logs for hooks (#699)
Resolves #689 

This adds a new yaml entry for the hook definition to allow the users to specifcy if they want to show the `command` logs or not.

here is an example.
```
releases:
- name: myapp
  chart: mychart
  # *snip*
  hooks:
  - events: ["cleanup"]
    showlogs: true
    command: "kubectl"
    args: ["get", "ingress"]
```
this will display the following output:
```
hook[cleanup] logs | NAME              HOSTS                          ADDRESS   PORTS   AGE
hook[cleanup] logs | catalog-gateway   tdc.foo                                  80      2d6h
hook[cleanup] logs | dataset           foobar.barr.foo.xxxxxxx.com              80      2d6h
hook[cleanup] logs | rating            fooba.barr.foo.xxxxxxx.com               80      2d6h
hook[cleanup] logs | sharing           foobar.barr.foo.xxxxxxx.com              80      2d6h
hook[cleanup] logs | tpsvc-iam-dev     foo.barr.foo.xxxxxxx.com                 80      2d6h
hook[cleanup] logs | tpsvc-iam-front   bar.barr.foo.xxxxxxx.com                 80      2d6h
```
2019-06-19 08:46:32 +09:00
KUOKA Yusuke f61334d9bb fix: helmfile template | kubectl apply -f - should work (#697)
Fixes #685
2019-06-16 16:56:56 +09:00
KUOKA Yusuke 3bf0f7d54e fix: helm diff missing --reset-values (#696)
Fixes #681
2019-06-16 16:52:32 +09:00
KUOKA Yusuke 7dec948950 fix: helm chart referenced by http URL (#695)
We unintentionally broke this since #593.

Fixes #675
Fixes #687
2019-06-16 16:40:17 +09:00
刘相轩 e0d0a1cf7f feat: Disable concurrency if any release is using tillerless (#694)
So that you don't need to remember adding `--concurrency 1`
2019-06-16 12:58:20 +09:00
Cedric Meury 31768b5bac doc: Adding user survey responses while switching to PR style (#692)
* adding survey responses; switch to PR style

* fixing the table

* adding reddit talk
2019-06-14 08:37:42 +09:00
KUOKA Yusuke 1bde201eae feat: --concurrency for delete and destroy (#691)
Resolves #690
2019-06-13 23:14:51 +09:00
KUOKA Yusuke 1831d3375f fix: installed: false not respecting TLS settings and tiller namespace (#683)
It was just missing the appendConnectionFlags call to compose flags passed to `helm delete` there. This fixes it.

Fixes #605
2019-06-12 19:07:31 +09:00
KUOKA Yusuke 2e38f42a40 fix: helmDefaults.kubeContext ignored in helm diff of helmfile apply (#682)
The root cause of this bug was due to that `--kube-context` and `kubeContext` had been treated specifically in code. So on the way I have made it consistent with other per-release settings - by adding `kubeContext` for each release and treating `helmDefaults.kubeContext` as just the default value for per-release setting.

Fixes #674
2019-06-12 16:49:19 +09:00
KUOKA Yusuke 1da3488599 feat: experimental integration with helm-x (#673)
This enhances helmfile so that it can:

- Treat K8s manifests directories and Kustomize projects as charts
- Add adhoc chart dependencies on sync/diff/template without forking or modifying chart(s) (#649)
- Add adhoc patches(JSON Patch or Strategic Merge Patch supported) to be applied to the K8s resources before sync/diff/template, without forking or modifyin chart(s) (#650)

The usage is as outlined in https://github.com/mumoshu/helm-x/tree/master/examples/helmfile.

Add any or all of `dependencies:`, `jsonPatches:` and `strategicMergePatches:` so that it adds additional flags to `helm` calls that is only supported by `helm x`.

```yaml
releases:
- name: kustomize
  chart: ../kustomize
- name: manifests
  chart: ../manifests
- name: foo
  chart: incubator/raw
  dependencies:
  - alias: bar
    chart: incubator/raw
  values:
  - values.yaml
  - bar:
      enabled: true
      resources:
      - apiVersion: v1
        kind: Pod
        metadata:
          name: bar
        spec:
          containers:
          - command:
            - sleep
            - 1000
            image: alpine:3.9.4
            imagePullPolicy: IfNotPresent
            name: bar
  jsonPatches:
  - target:
      version: v1
      kind: Pod
      name: foo
    patch:
    - op: replace
      path: /spec/containers/0/command
      value:
      - sleep
      - "123"
  strategicMergePatches:
  - apiVersion: v1
    kind: Pod
    metadata:
      name: bar
    spec:
      containers:
      - name: bar
        command:
        - sleep
        - "234"
```

You can alternatively provide `source: path/to/patch.yaml` for `jsonPatches` and `strategicMergePatches` items to externalize it. Add `.gotmpl` suffix like you would do for values files for templating.

When running `helmfile` you must point `--helm-binary` to the `helm-x` binary like below:

```
$ helmfile --helm-binary ~/.helm/plugins/helm-x/bin/helm-x --log-level debug apply
```

after installing the [helm-x](https://github.com/mumoshu/helm-x) plugin.

The integration should ideally be automatic. That is, it shouldn't force you to set `--helm-binary`. But I had no other way to not bloat helmfile's codebase to just add this experimental feature.

Resolves #649
Resolves #650
2019-06-12 16:11:34 +09:00
KUOKA Yusuke 78b03e0d92 fix: --state-values-set panic: value of type interface {} is not assignable to type string (#680)
Probably since #647 helmfile has been unable to merge nested maps in environment values if they were loaded from files. This fixes it.

The relevant test is also enhanced so that no further regression like this happens.

Fixes #677
2019-06-12 13:35:04 +09:00
sgandon 11eda66eaa feat: make release version templatized (#671)
Allows the release version to be a Go template.
Resolves #669
2019-06-11 21:52:54 +09:00
KUOKA Yusuke 65d404b276 feat: helmfile -f <go-getter url> (#670)
Extends the remote-helmfile feature to also work when loading the first state file.
This should be useful for people who wants to give helmfile a try without ever opening `$EDITOR`.
2019-06-11 14:28:50 +09:00
Eric Bailey 72425aebfc fix: persist original file path when using bases (#672)
* fix: persist original file path when using bases

Prior to this change, the resulting lock file was called `<bases[0]>.lock`,
instead of `<filename>.lock`.

This change ensures the final, merged state has the correct `.FilePath`.

* test: Assert proper FilePath in layered HelmState
2019-06-11 14:01:27 +09:00
Kevin J. Qiu 34fbfb4fd0 fix: convert path to absolute path when doing helm secret dec (#664)
Fixes #663
2019-06-10 21:43:20 +09:00
sgandon 56c27c2bd9 fix: fixes a regexp issue for release summary (#666)
There was an issue a the regexp reding the version from helm output.

Fixes #665
2019-06-10 09:18:41 +09:00
Vincent Behar 34c793d87e fix: use the same logic as helm-secrets when decoding secrets (#655)
helm-secrets uses the `HELM_SECRETS_DEC_SUFFIX` env var to define the name of the output file
we should have the same logic in helmfile, to come up with the same filename

It only affects people using the `HELM_SECRETS_DEC_SUFFIX` env var

Use-case: if you want to run multiple `helmfile` commands in parallel, without conflicts. in this case, you need to decrypt secrets with different suffixes.
2019-06-06 21:27:04 +09:00
刘相轩 b9862bdae6 Sync repos before update deps (#660) 2019-06-06 15:23:27 +09:00
刘相轩 e3e7905f07 Sort requirements alphabetically by name. (#659) 2019-06-06 12:30:22 +09:00
KUOKA Yusuke 2e57beb448 feat: bundle helm-s3 and helm-git into container images (#651)
Resolves #435 (Git as chart repository)
Resolves #220 (S3 as chart repository)
Resolves #436 (About bundling helm plugins)

A lot of thanks to @aslafy-z for authoring the awesome helm-git plugin and contributing it to the community!
2019-06-04 23:53:11 +09:00
KUOKA Yusuke 3986f6497e Update writing-helmfile.md 2019-06-04 23:41:26 +09:00
KUOKA Yusuke 820abbc06d feat: remote state files (#648)
This change enhances helmfile to accept terraform-module-like URLs in nested state files a.k.a sub-helmfiles.

```yaml
helmfiles:
- # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file
  # The nested-state file is locally checked-out along with the remote directory containing it.
  # Therefore all the local paths in the file are resolved relative to the file
  path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
```

The URL isn't equivalent to terraform module sources. The difference is that we use `@` to distinguish between (1) the path to the repository and directory containing the state file and (2) the path to the state file being loaded. This distinction provides us enough fleibiity to instruct helmfile to check-out necessary and sufficient directory to make the state file works.

Under the hood, it uses [hashicorp/go-getter](https://github.com/hashicorp/go-getter), that is used for [terraform module sources](https://www.terraform.io/docs/modules/sources.html) as well.

Only the git provider without authentication like git-credentials helper is tested. But theoretically any go-getter providers should work. Please feel free to test the provider of your choice and contribute documentation or instruction to use it :)

Resolves #347
2019-06-04 22:59:54 +09:00
KUOKA Yusuke 3710f6233e feat: state values (#647)
This adds `values` to state files as proposed in #640.

```yaml
values:
- key1: val1
- defaults.yaml

environments:
  default:
  - values:
    - environments/default.yaml
  production:
  - values:
    - environments/production.yaml

```

`{{ .Valuese.key1 }}` evaluates to `val1` if and only if it is not overrode via the production or the default env, or command-line args.

Resolves #640
2019-06-04 16:34:02 +09:00
KUOKA Yusuke 2d2b3e486f fix: sprig dict funcs are failing on dicts nested under arrays (#646)
Fixes #643
2019-06-04 13:47:05 +09:00
KUOKA Yusuke 65ee6a2124 fix: "cannot unmarshal !!str <no value> into bool" errors in state templates (#645)
Seems like we are affected by https://github.com/golang/go/issues/24963. That is, even though we internally use the template option `missingkey=zero`, in some cases it still prints `<no value>` instead of zero values, which has been confusing the state yaml parsing.

This fixes the issue by naively replacing all the remaining occurrences of `<no value>` in the rendered text, while printing debug logs to ease debugging in the future when there is unexpected side-effects introduced by this native method.

Fixes #553
2019-06-04 13:23:38 +09:00
KUOKA Yusuke 1d3f5f8a33 feat: override state(former "enviroment") values via command-line args (#644)
The addition of `--set k1=v1,k2=v2` and `--values file1 --values file2` was originally planned in #361.

But it turned out we already had `--values` for existing helmfile commands like `sync`. Duplicated flags doesn't work, obviously.

So this actually add `--state-values-set k1=v1,k2=v2` and `--set-values-file file1 --set-values-file file2`.

They are called "state" values according to the discussion we had at #640

Resolves #361
2019-06-04 11:03:01 +09:00
KUOKA Yusuke e2d6dc4afa feat: helmfile as a go library (#639)
* feat: helmfile as a go library

This removes almost all the dependencies from the helmfile core logic to urfave/cli. `main.go` is now a thin wrapper around the core logic implemented in `pkg/app`.
2019-06-04 09:12:00 +09:00
KUOKA Yusuke f6057a1cca chore: first step towards migration to cobra (#637)
I will start by making as much as possible code in `main.go` independent from urfave/cli, and this illustrates how we can do that by introducing a Config interface that delegates any config value fetch to urfave/cli. We will implement an cobra/pflag impl of the interface when helmfile finally migrates to cobra.
2019-06-01 14:00:26 +09:00
KUOKA Yusuke c68fc5bc50 chore: tidy up pkgs (#636)
for readability and towards potentially making helmfile usable as a go library
2019-06-01 13:36:05 +09:00
KUOKA Yusuke 68b95f14d4 change the nest level of envvals overrides for nested state files(sub-helmfiles) (#635)
We added envvals overrides in the state file via #622 two days ago:

```
helmfiles:
- name: sub.helmfile.yaml
  environment:
    values:
    - mykey: myvalue
```

This change removes the `environment` level in the above cofig, so that it looks like:

```
helmfiles:
- name: sub.helmfile.yaml
  values:
  - mykey: myvalue
``

This is an inevitable breaking change towards #361. But I wanted to break it earlier so that less folks are affected.`

Ref https://github.com/roboll/helmfile/issues/361#issuecomment-497530819
2019-06-01 12:39:31 +09:00
KUOKA Yusuke 9318d8a040 doc: recommend quoting potentially number-like string (#634)
Resolves #608
2019-06-01 11:29:33 +09:00
jonathan cowling msm 44686298f2 Document verify in releases (#632)
Just a one liner in the readme to make it clear there's a `verify` option in the `releases` section 😄
2019-05-31 22:50:45 +09:00
bitsofinfo 206372b7aa feat: upgrade sprig to 2.16.0
Resolves #630
2019-05-31 13:16:49 +09:00
KUOKA Yusuke e1c04971c1 Update README.md 2019-05-30 17:11:42 +09:00
KUOKA Yusuke b3f4586ed8 Fix panic related to env values from files and sprig dict funcs (#625)
* fix: 0.68.0: go panic w/ multiple bases/layers

Fixes #623

* fix: 0.64.1+: getOrNil/hasKey fails on Environment.Values with nested maps

Fixes #624
2019-05-30 15:41:00 +09:00
KUOKA Yusuke 591086dac9 Update README.md 2019-05-29 21:25:59 +09:00
KUOKA Yusuke 1226ea6d1a feat: specify env values from the parent to the nested state (#622)
* feat: specify env values from the parent to the nested state

Adds the `helmfiles[].environment.values` that accepts a mix of file pathes and inline dictes:

```yaml
helmfiles:
- path: path/to/nested/helmfile.yaml
  environment:
    values:
    - key1: val1
    - values.yaml
```

The values files are loaded in the context of the parent state file. For example, in case the above state file is located at `/path/to/helmfile.yaml`,
`values.yaml` is located at `/path/to/values.yaml` instead of `/path/to/nested/values.yaml`.

Resolves #523

* fix: multiple "bases" declarations yields duplicate releases

Fixes #615

* fix regression in double-rendering with env value overrides

The latest commit broke any state files like the below to NOT pass env value overrides at all:

```
helmfiles:
- path: nested/state.yaml
  environment:
    values:
    - overrides.yaml
```

This fixes the issue.
2019-05-29 19:08:51 +09:00
KUOKA Yusuke 681c866ce1 feat: inline environment values (#621)
Resolves #359
2019-05-28 16:26:00 +09:00
KUOKA Yusuke dd70c857a5 Update README.md 2019-05-28 15:38:17 +09:00
KUOKA Yusuke a896f801ab feat: optionally allow missing environment values/secrets files (#620)
```yaml
environments:
  default:
    missingFileHandler: Warn
    values:
    - path/to/values.yaml
    secrets:
    - path/to/secrets.yaml
```

`missingFileHandler` set to `Warn`, `Info`, or `Debug` results in helmfile NOT stop when `path/to/values.yaml` or `path/to/secrets.yaml` is missing.

Resolves #548

While implementing the above feature, I also found a bug that has been causing #559. This also fixes that.

To verify it is actually fixed, create an example helmfile.yaml that looks like the below, and run `helmfile diff`:

```
$ cat helmfile.yaml
environments:
  default:
    secrets:
      - env-secrets.yaml

releases:
  - name: myapp
    chart: nginx
    namespace: default
    secrets: [secrets.yaml]    # Notice this file does not exist
    values:
      - ingress:
          enabled: true

$ helmfile diff
could not deduce `environment:` block, configuring only .Environment.Name. error: failed to read helmfile.yaml.part.0: environment values file matching "env-secrets.yaml" does not exist
in ./helmfile.yaml: failed to read helmfile.yaml: environment values file matching "env-secrets.yaml" does not exist
```

Fixes #559
2019-05-28 15:33:45 +09:00
Raj Perera 4a5996d083 feat: ability to exit zero with no releases match selector (#618)
- Change exit code from 2 to 3 when helmfiles have no releases that match a selector
- Introduces new flag `--allow-no-matching-release` to exit 0 when no releases match a selector.

Resolves: #597
2019-05-27 09:53:24 +09:00
KUOKA Yusuke f6264bfa9d fix: do not complain on missing values files for undesired release (#617)
Fixes #616
2019-05-24 21:40:34 +09:00
KUOKA Yusuke 3b3d3092dd fix scopelint errors / possible races in hooks (#614)
Ref #612
Fixes #514
2019-05-22 18:50:38 +09:00
KUOKA Yusuke 1012256f16 feat: "base" helmfile state gotmpl is rendered with the envvals inherited from the parent (#613)
Resolves #611
2019-05-22 18:28:10 +09:00
KUOKA Yusuke 90390492a3 feat: expand glob pattern in environment values file path (#610)
This enhances helmfile's internal environment values files loader to expand glob patterns (#606)

Fixes the existing bug that helmfile was unable to load environment values file from absolute path (#549)

Resolves #606
Fixes #549
2019-05-21 16:49:57 +09:00
KUOKA Yusuke 4c9c42d3c5 fix: gotmpl layer was unable to reference env values from other layers when env!=default (#609)
Fixes #604
2019-05-21 14:37:03 +09:00
KUOKA Yusuke bd60548f10 feat: add --skip-deps for helmfile lint (#602)
Resolevs #581
2019-05-16 21:31:00 +09:00
KUOKA Yusuke 0534117b62 feat: postsync hooks (#601)
`postsync` events are triggered after each release is applied to the cluster in `helmfile sync` or `helmfile apply`.

This should be a best hook to notify only after each sync failed or succeeded. This can be used for running operations like patching K8s resources managed by helm, but that should be the last-resort. Maybe you should fork/update the chart, or submit a feature request to add `replicated/ship` integration to `helmfile` in that case :)

Resolves #599
2019-05-16 21:24:16 +09:00
KUOKA Yusuke 0104c91fce feat: support for locking the same chart for two or more versions (#600)
Resolves #598
2019-05-16 21:19:30 +09:00
Yusuke KUOKA a769d1a82a fix integration test 2019-05-15 13:21:48 +09:00
KUOKA Yusuke b0179218b9 fix: helmfile panicing on missing lock file (#596)
Fixes #595
2019-05-15 13:16:22 +09:00
KUOKA Yusuke c9a43ad9cb feat: Dependency locking (#593)
In order to maintain predictable deployments, as developer I want to generate and use "lock files" for all chart versions retrieved from a helmfile.

This change solves it by (1)enhancing `helmfile deps` to generate a lock file containing all the direct chart dependencies of each helmfile state file and
(2)making other helmfile sub-commands reads the lock file and merge the locked version numbers to the helmfile state file being processed.

The lock file is named after the helmfile state file being locked, so that you can have multiple set of the helmfile state file and the lock file pairs in a directory.

When `helmfile deps` are not explicitly run before commands like `sync`, all the helmfile behavior should remain as before.

Let's say you have `helmfile.1.yaml`:

```
repositories:
- name: stable
  url: https://kubernetes-charts.storage.googleapis.com

releases:
- name: envoy
  chart: stable/envoy
- name: envoy2
  chart: stable/envoy
```

`helmfile deps` generates `helmfile.1.lock` that looks like:

```
dependencies:
- name: envoy
  repository: https://kubernetes-charts.storage.googleapis.com
  version: 1.5.0
digest: sha256:e43b05c8528ea8ef1560f4980a519719ad2a634658abde0a98daefdb83a104e9
generated: 2019-05-14T16:45:37.78205+09:00
```

Under the hood, `helmfile deps` creates a temporary local helm chart with a dummy `Chart.yaml` and `requirements.yaml` deduced from the `helmfile.yaml` content, then runs `helm dependency update` to produce od update the corresponding `requirements.lock` file.

`helmfile` then renames it to match the name of the targeted helmfile state file and moves it,  so that it becomes adjacent to each `helmfile.yaml`.

Other `helmfile` commands like `sync`, `diiff`, `apply`, `lint` read chart version numbers from the lock file.

Resolves #483
2019-05-15 09:39:12 +09:00
KUOKA Yusuke 9eec72b318 Merge pull request #594 from sstarcher/fetch_devel
fix: Fetch devel

Fixes two issues encountered in https://sweetops.slack.com/archives/CE5NGCB9Q/p1557848927155400
2019-05-15 08:55:54 +09:00
Shane Starcher 081107bb82 error is coming back nil sometimes
this does not fix the root issue that is causing a nil error
2019-05-14 15:51:12 -07:00
Shane Starcher af69d1ba97 fetch should honor devel flag 2019-05-14 15:50:44 -07:00
KUOKA Yusuke 53ad91607a fix: log useful error message when failed walking through the file tree (#592)
Fixes #590
2019-05-14 10:02:50 +09:00
KUOKA Yusuke 255d92064e Merge pull request #587 from roboll/layering-enhancements
feat: helmfile.yaml layering enhancements

The current  [Layering](https://github.com/roboll/helmfile/blob/master/docs/writing-helmfile.md#layering) system didn't work as documented, as it relies on helmfile to template each "part" of your helmfile.yaml THEN merge them one by one.

The reality was that helmfile template all the parts of your helmfile.yaml at once, and then merge those YAML documents. In https://github.com/roboll/helmfile/issues/388#issuecomment-436186278, @sruon was making a GREAT point that we may need to change helmfile to render templates earlier - that is to evaluate a template per each helmfile.yaml part separated by `---`. Sorry I missed my expertise to follow your great idea last year @sruon  😭 

Anyways, this, in combination with the wrong documentation, has made so many people confused. To finally overcome this situation, here's a fairly large PR that introduces the 2 enhancements:

- `bases:` for easier layering without go template expressions, especially `{{ readFunc "path/to/file" }}`s. This is the first commit of this PR.
- `helmfile.yaml` is splited by the separator `---` at first. Each part is then rendered as a go template(double-render applies as before). Finally, All the results are merged in the order of occurence. I assume this as an enhanced version of @sruon's work. This is the second commit of this PR.

Resolves #388
Resolve #584
Resolves #585 (`HELMFILE_EXPERIMENTA=true -f helmfile.yaml helmfile` disables the whole-file templating, treating the helmfile.yaml as a regular YAML file as the file ext. denotes. Use `helmfile.yaml.gotmpl` or `helmfile.gotmpl` to enable)
Fixes #568 (Use `bases` or `readFile` rather than not importing implicitly with `helmfile.d`
2019-05-14 09:48:20 +09:00
Yusuke KUOKA aef366660b feat: split-render-merge helmfile.yaml parts
This splits your helmfile.yaml by the YAML document separator "---" before evaluating go template expressions as outlined in https://github.com/roboll/helmfile/issues/388#issuecomment-491710348
2019-05-13 21:49:59 +09:00
Yusuke KUOKA 1db205de48 feat: "bases" for easier layerina
This adds the new configuration key `baeses` to your helmfile.yaml files, so that you can layer them without the `readFile` template function, which was a bit unintuitive.

Please see https://github.com/roboll/helmfile/issues/388#issuecomment-491710348 for more context
2019-05-13 21:48:00 +09:00
KUOKA Yusuke 4f83e69bf6 Various U/X improvements for helmfile apply (#586)
* Various U/X improvements for `helmfile apply`

This improves the U/X of `helmfile apply`, by allowing you to selectively apply sub-helmfiles.
When you have two or more sub-helmfiles processed, typing `n` to cancel the first doesn't automatically stop the whole helmfile execution.
Instead, it proceeds by diffing the next sub-helmfile, and asks you to apply it, which should be what the user would expect.

To support this workflow, I have suppressed useless exec logs, correct exit status when diff exists in sub-helmfiles but not in the parent helmfile, and made the final error message emitted by helmfile better.

More concretely, this moves more output from `helm` to STDERR and the `debug` log-level.

The overall output from `helmfile` should be a bit more cleaner especially for `apply`, `sync`, `diff` and perhaps other `helmfile` sub-commands, too.

For example, when one of release failed, `helmfile`'s final error message now includes the error message from the failed `helm` execution, like seen in the last line:

```
List of updated releases :
RELEASE   CHART          VERSION
envoy     stable/envoy     1.5.0

List of releases in error :
RELEASE
envoy2
in ./helmfile.yaml: in .helmfiles[0]: in /Users/c-ykuoka/helmfile/helmfile.1.yaml: failed processing release envoy2: helm exited with status 1:
  Error: UPGRADE FAILED: "envoy2" has no deployed releases
```

This way you can better understand what caused helmfile to finally fail.

`helmfile` has been streaminig a lot of stdout and stderr contents from the `helm` commands regardless of the helmfile's log-level. It has been suppressed by default and moved to the `debug` log-level.
You will see that it helps you focus on what was the cause of a failure.

While working on the above, I found an another bug that made `--detailed-exitcode` useless in some case.
That is, `helmfile diff --detailed-exitcode`, when any diff existed only in sub-helmfiles, has been returning an exit code of `1`. It should return `2` when any release had diff and no release had an error, regardless of the target is a sub-helmfile or a parent helmfile. Why? Because that's what `--detailed-exitcode` meant for!

After this PR gets merged, `helmfile diff --detailed-exitcode`  propery return exit code `2` in such cases.

Fixes #543
Resolves #540
2019-05-12 16:26:11 +09:00
KUOKA Yusuke bae842f234 fix: Send log messages to STDERR (#583)
And only useful outputs from helm commands like ones from `helm template` to STDOUT.

Fixes #551
2019-05-09 14:58:15 +09:00
Raj Perera 55c275b3aa Add presync event hook (#580)
* Add presync hook

* Add note to README about new hook
2019-05-09 10:13:31 +09:00
prakharrr-sl 0fc8ac395b typo in example (#582) 2019-05-09 10:12:14 +09:00
KUOKA Yusuke 272d55e31a fix: the merge mistake in imports (#578) 2019-05-07 09:55:34 +09:00
sgandon d683ea1d7b fix(sync): make error log showing when error (#577)
In debug mode, there was an error log display even when there was no error.
This PR fixes this issue.
2019-05-06 23:28:12 +09:00
Andrey Afoninsky 0ea5960ef2 feat: add .yml extension support (#575)
Resolves #565
2019-05-06 23:26:51 +09:00
sgandon 9a820d7bf2 feat: removes dictionary key for subhelm and uses selectorsInherited (#576)
Removed the usage of subhelmfile path as map key.
I also introduced the selectorsInherited key for explicit parent selector inheritance.

Ref #344
2019-05-06 10:06:32 +09:00
sgandon 4581e004b8 feat(#344): add sub helmfiles explicit selectors (#567)
Fixes #344 by allowing explicit selectors to be specified for composed helmfiles using the following structure

```yaml
helmfiles:
- path: helmfile.d/a*.yaml
  selectors:
  - name=prometheus      
  - name!=zipkin      
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
  selectors: {}
```

2 modes here : 
* legacy mode when no the env var HELMFILE_EXPERIMENTAL is not set to true
  * no selector : inherit from the command line.
  * selector:  is specified then it is used (an emty means no inheritance from command line and take everything).
* experimental when the env var HELMFILE_EXPERIMENTAL=true
  * no selector : nothing is inherited from the command line so use all releases.
  * selector:  is specified then it is used (an emty means no inheritance from command line and take everything).
2019-05-05 13:38:52 +09:00
Yusuke KUOKA d2353b5b70 accept go.mod and go.sum to be updated in pristine
I have currently no way to avoid this from happening in the first place
2019-05-04 22:45:31 +09:00
Yusuke KUOKA fb7e0a360c Update go.mod after running go mod vendor
Not sure why `go mod vendor` updates go.mod but `go build .` not
2019-05-04 22:17:16 +09:00
Yusuke KUOKA 59b254ee45 fix automated releases broken due to workspace location misconfig after go mod migration 2019-05-04 22:10:37 +09:00
sgandon 950bd23d86 fix(struct): renamed Error struct field into Failed (#566) 2019-05-02 20:41:49 +09:00
KUOKA Yusuke 8f030d5eab Bump go to 1.12.4 / Switch to go modules (#564)
* Bump go to 1.12.4 / Switch to go modules

Follow-up for https://github.com/roboll/helmfile/pull/560#issuecomment-486516109
2019-05-02 20:41:36 +09:00
sgandon a31077a1c0 feat(#502): display summary of upgraded, deleted and error releases (#560)
* feat(report): display summary of upgraded, deleted and error releases

* feat(#502): adds dep target in makefile

* feat(#502): removes vendor and fixes pristine in makefile
2019-04-25 13:33:34 +09:00
Aaron Gershman 32588ae319 readme example for failing when values file is missing (#561)
* readme example for failing when values file is missing

Believe this would assist with #548

* Update README.md

Using documentation yanked from searching for the key missingFileHandler https://github.com/roboll/helmfile/search?q=missingFileHandler&unscoped_q=missingFileHandler
2019-04-25 13:12:41 +09:00
Yusuke KUOKA f74963f4b6 fix(ci,release): Use existent base image for docker builds 2019-04-13 22:02:22 +09:00
KUOKA Yusuke d93ec77ea3 fix: with environment secrets (#556)
Since tillerless support we unintentionally broke this, and there isn't a real fix to this.
We must accept a limitation that helmfile needs a tiller installed on your cluster just for decrypting environment secrets.

Fixes #550
2019-04-13 22:01:53 +09:00
KUOKA Yusuke 04a9ea1d2a fix: helmfile apply and sync should properly delete releases (#555)
Since #526, `helmfile apply` have been really able to detect deletion of the last release only, and `sync` has been unable to mark releases with `installed: false` for removal.

Fixes #554
2019-04-13 21:47:27 +09:00
KUOKA Yusuke f53c66749e Bump go to 1.12.1 (#542) 2019-04-08 20:47:36 +09:00
Facundo Guerrero 8a13999de3 Fix link to shared-configuration-across-teams (#547) 2019-04-08 20:46:39 +09:00
Patrick Valsecchi 1acd07fa7e Simple implementation of the tillerless mode (#531)
Ref #449
2019-04-05 19:02:37 +09:00
365 changed files with 32322 additions and 36466 deletions
+77
View File
@@ -0,0 +1,77 @@
HELM_VERSION ?= v3.5.3
HELM2_VERSION ?= v2.17.0
KUSTOMIZE_VERSION ?= v3.8.8
K8S_VERSION ?= v1.13.12
MINIKUBE_VERSION ?= v0.30.0
SOPS_VERSION ?= v3.6.1
# ---
CHANGE_MINIKUBE_NONE_USER ?= true
MINIKUBE_WANTUPDATENOTIFICATION ?= false
MINIKUBE_WANTREPORTERRORPROMPT ?= false
VAULT_ADDR := http://127.0.0.1:8200
VAULT_TOKEN := toor
tmp := $(shell mktemp -d)
HELM_FILENAME := helm-${HELM_VERSION}-linux-amd64.tar.gz
HELM2_FILENAME := helm-${HELM2_VERSION}-linux-amd64.tar.gz
KUSTOMIZE_FILENAME := kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz
all: vault sops helm kustomize minikube/destroy minikube
helm:
curl -sSLo $(tmp)/${HELM_FILENAME} "https://get.helm.sh/${HELM_FILENAME}"
tar zxf $(tmp)/${HELM_FILENAME} --directory ${tmp} linux-amd64/helm
chmod +x ${tmp}/linux-amd64/helm
sudo mv ${tmp}/linux-amd64/helm /usr/local/bin/
.PHONY: helm
helm2:
curl -sSLo $(tmp)/${HELM2_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM2_FILENAME}"
tar zxf $(tmp)/${HELM2_FILENAME} --directory ${tmp} linux-amd64/helm
chmod +x ${tmp}/linux-amd64/helm
sudo mv ${tmp}/linux-amd64/helm /usr/local/bin/
.PHONY: helm2
kustomize:
curl -sSLo $(tmp)/${KUSTOMIZE_FILENAME} "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F${KUSTOMIZE_VERSION}/${KUSTOMIZE_FILENAME}"
tar zxf $(tmp)/${KUSTOMIZE_FILENAME} --directory ${tmp} kustomize
chmod +x ${tmp}/kustomize
sudo mv ${tmp}/kustomize /usr/local/bin/
.PHONY: kustomize
minikube/destroy:
sudo -E minikube delete || true
sudo -E rm -rf /etc/kubernetes || true
sudo -E rm -rf $$HOME/.minikube/* || true
.PHONY: minikube/destroy
.EXPORT_ALL_VARIABLES: minikube/destroy
minikube:
curl -sSLo ${tmp}/kubectl https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl
chmod +x ${tmp}/kubectl && sudo mv ${tmp}/kubectl /usr/local/bin/
curl -sSLo ${tmp}/minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
chmod +x ${tmp}/minikube && sudo mv ${tmp}/minikube /usr/local/bin/
sudo -E minikube delete || true
sudo -E rm -rf /etc/kubernetes || true
sudo -E rm -rf $$HOME/.minikube/* || true
sudo -E minikube start --vm-driver=none --kubernetes-version=${K8S_VERSION}
sudo -E minikube update-context
kubectl wait node/minikube --for=condition=Ready
.PHONY: minikube
.EXPORT_ALL_VARIABLES: minikube
vault:
docker kill $$(docker ps -a --filter "name=vault" -q) || true
docker run -d -p8200:8200 --rm --name vault vault:1.2.0 server -dev -dev-root-token-id=toor
docker run --rm --network="host" -e VAULT_ADDR=$$VAULT_ADDR -e VAULT_TOKEN=$$VAULT_TOKEN vault:1.2.0 secrets enable -path=sops transit
docker run --rm --network="host" -e VAULT_ADDR=$$VAULT_ADDR -e VAULT_TOKEN=$$VAULT_TOKEN vault:1.2.0 write sops/keys/key type=rsa-4096
.PHONY: vault
sops:
curl -sSLo $(tmp)/sops "https://github.com/mozilla/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux"
chmod +x $(tmp)/sops
sudo mv ${tmp}/sops /usr/local/bin/
.PHONY: sops
+96 -45
View File
@@ -1,25 +1,76 @@
version: 2
version: 2.1
jobs:
build:
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.16.3
environment:
GOPATH: /go
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- run: go env
- run: make build
- run: make build-test-tools
- save_cache:
key: go-mod-cache-v1-{{ checksum "./go.sum" }}
paths:
- /go/pkg/mod
- persist_to_workspace:
root: /go/src/github.com/roboll/helmfile
root: ./
paths:
- .
image:
docker:
- image: circleci/golang:1.16.3
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- setup_remote_docker
- run:
command: |
make tools
go mod vendor
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make image
image_helm3:
docker:
- image: circleci/golang:1.16.3
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- setup_remote_docker
- run:
command: |
make tools
go mod vendor
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make image/helm3
test:
environment:
GOPATH: /go
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.16.3
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- run: make check
- run: make pristine
- run: make test
@@ -27,78 +78,78 @@ jobs:
# thanks to https://raw.githubusercontent.com/weaveworks/launcher/master/.circleci/config.yml
integration_tests:
machine:
image: circleci/classic:201808-01
environment:
CHANGE_MINIKUBE_NONE_USER: true
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
image: ubuntu-2004:202010-01
parameters:
helm-version:
type: string
steps:
- checkout
- run: mkdir ~/build
- attach_workspace:
at: ~/build
- run: cp ~/build/helmfile ~/project/helmfile
- run:
name: Install helm
environment:
HELM_VERSION: v2.13.0
name: Install test dependencies
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM_FILENAME}"
tar zxf ${HELM_FILENAME} linux-amd64/helm
chmod +x linux-amd64/helm
sudo mv linux-amd64/helm /usr/local/bin/
- run:
name: Deploy minikube
environment:
CHANGE_MINIKUBE_NONE_USER: true
K8S_VERSION: v1.12.3
MINIKUBE_VERSION: v0.30.0
command: |
curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
curl -Lo minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
chmod +x minikube && sudo mv minikube /usr/local/bin/
sudo -E minikube start --vm-driver=none --kubernetes-version=${K8S_VERSION}
sudo -E minikube update-context
- run:
name: Wait for nodes to become ready
command: JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done
cp ~/build/helmfile ~/project/helmfile
cp ~/build/diff-yamls ~/project/diff-yamls
cp ~/build/yamldiff ~/project/yamldiff
if [[ "<< parameters.helm-version >>" == v3* ]]
then
make -C .circleci helm
else
make -C .circleci helm2
fi
make -C .circleci vault
make -C .circleci sops
make -C .circleci kustomize
make -C .circleci minikube
- run:
name: Execute integration tests
environment:
TERM: "xterm"
command: |
export TERM=xterm
make integration
if [[ "<< parameters.helm-version >>" == v3* ]]
then
HELMFILE_HELM3=1 make integration
else
make integration
fi
# GITHUB_TOKEN env var must be setup in circleci console
release:
docker:
- image: circleci/golang:1.11.2
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.16.3
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
- restore_cache:
keys:
- go-mod-cache-v1-{{ checksum "./go.sum" }}
- go-mod-cache-v1-
- setup_remote_docker
- run:
command: |
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io
make tools
BUILD_URL="$CIRCLE_BUILD_URL" make push release
go mod vendor
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make push push/helm3 release
workflows:
version: 2
build_and_test:
jobs:
- build
- image
- image_helm3
- test
- integration_tests:
requires:
- build
filters:
branches:
only:
- master
- /pull.*/
matrix:
parameters:
helm-version: ["v2.17.0", "v3.4.2", "v3.5.4"]
- release:
filters:
branches:
+5
View File
@@ -0,0 +1,5 @@
root = true
[*.md]
insert_final_newline = true
trim_trailing_whitespace = true
+10 -1
View File
@@ -1,3 +1,12 @@
dist/
.idea/
helmfile
helmfile
helmfile.lock
/diff-yamls
/yamldiff
test/integration/tmp
vendor/
*.log
.vagrant/
*.lock
test/integration/.gnupg/
+33 -13
View File
@@ -1,29 +1,49 @@
FROM golang:1.11.2-alpine3.8 as builder
FROM golang:1.16.3-alpine3.13 as builder
RUN apk add --no-cache make git
WORKDIR /go/src/github.com/roboll/helmfile/
COPY . /go/src/github.com/roboll/helmfile/
WORKDIR /workspace/helmfile
COPY . /workspace/helmfile
RUN make static-linux
# -----------------------------------------------------------------------------
FROM alpine:3.8
FROM alpine:3.11
RUN apk add --no-cache ca-certificates git bash curl
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION=v2.13.0
ARG HELM_VERSION="v2.17.0"
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="15eca6ad225a8279de80c7ced42305e24bc5ac60bb7d96f2d2fa4af86e02c794"
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
ARG HELM_SHA256="f3bec3c7c55f6a9eb9e6586b8c503f370af92fe987fcbf741f37707606d70296"
RUN set -x && \
wget ${HELM_LOCATION}/${HELM_FILENAME} && \
echo Verifying ${HELM_FILENAME}... && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
tar zxf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
echo Extracting ${HELM_FILENAME}... && \
tar zxvf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
mv /linux-amd64/tiller /usr/local/bin/ && \
rm ${HELM_FILENAME} && rm -r /linux-amd64
RUN mkdir -p "$(helm home)/plugins"
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
# for now but in a future version of alpine (in the testing version at the time of writing)
# we should be able to install using apk add.
# the sha256 sum can be found at https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256
# maybe a good idea to automate in the future?
ENV KUBECTL_VERSION="v1.18.9"
ENV KUBECTL_SHA256="6a68756a2d3d04b4d0f52b00de6493ba2c1fcb28b32f3e4a0e99b3d9f6c4e8ed"
RUN set -x & \
curl --retry 5 --retry-connrefused -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
sha256sum kubectl | grep ${KUBECTL_SHA256} && \
chmod +x kubectl && \
mv kubectl /usr/local/bin/kubectl
RUN ["helm", "init", "--client-only", "--stable-repo-url", "https://charts.helm.sh/stable"]
RUN helm plugin install https://github.com/databus23/helm-diff && \
helm plugin install https://github.com/futuresimple/helm-secrets
helm plugin install https://github.com/futuresimple/helm-secrets && \
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
helm plugin install https://github.com/aslafy-z/helm-git.git && \
helm plugin install https://github.com/rimusz/helm-tiller
COPY --from=builder /go/src/github.com/roboll/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
CMD ["/usr/local/bin/helmfile"]
CMD ["/usr/local/bin/helmfile", "--help"]
+55
View File
@@ -0,0 +1,55 @@
FROM golang:1.16.3-alpine3.13 as builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
COPY . /workspace/helmfile
RUN make static-linux
# -----------------------------------------------------------------------------
FROM alpine:3.11
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION="v3.5.3"
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="2170a1a644a9e0b863f00c17b761ce33d4323da64fc74562a3a6df2abbf6cd70"
RUN set -x && \
wget ${HELM_LOCATION}/${HELM_FILENAME} && \
echo Verifying ${HELM_FILENAME}... && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
echo Extracting ${HELM_FILENAME}... && \
tar zxvf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
rm ${HELM_FILENAME} && rm -r /linux-amd64
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
# for now but in a future version of alpine (in the testing version at the time of writing)
# we should be able to install using apk add.
# the sha256 sum can be found at https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256
# maybe a good idea to automate in the future?
ENV KUBECTL_VERSION="v1.18.9"
ENV KUBECTL_SHA256="6a68756a2d3d04b4d0f52b00de6493ba2c1fcb28b32f3e4a0e99b3d9f6c4e8ed"
RUN set -x && \
curl --retry 5 --retry-connrefused -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
sha256sum kubectl | grep ${KUBECTL_SHA256} && \
chmod +x kubectl && \
mv kubectl /usr/local/bin/kubectl
ENV KUSTOMIZE_VERSION="v3.8.8"
ENV KUSTOMIZE_SHA256="175938206f23956ec18dac3da0816ea5b5b485a8493a839da278faac82e3c303"
RUN set -x && \
curl --retry 5 --retry-connrefused -LO https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz && \
sha256sum kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz | grep ${KUSTOMIZE_SHA256} && \
tar zxf kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz && \
rm kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz && \
mv kustomize /usr/local/bin/kustomize
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.1.3 && \
helm plugin install https://github.com/jkroepke/helm-secrets --version v3.5.0 && \
helm plugin install https://github.com/hypnoglow/helm-s3.git --version v0.10.0 && \
helm plugin install https://github.com/aslafy-z/helm-git.git --version v0.10.0
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
CMD ["/usr/local/bin/helmfile"]
Generated
-121
View File
@@ -1,121 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:d9141c149f722217d35834972878499a4781fe5ae46a152e8ac843ff15efc80f"
name = "github.com/Masterminds/semver"
packages = ["."]
pruneopts = "UT"
revision = "8d82589cda2d7b5b9167396841975c4535c9bec6"
version = "v1.4.1"
[[projects]]
digest = "1:46a054a232ea2b7f0a35398d682b433d26ba9975fce9197b1784824402059f5b"
name = "github.com/Masterminds/sprig"
packages = ["."]
pruneopts = "UT"
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
version = "v2.15.0"
[[projects]]
digest = "1:8f5416c7f59da8600725ae1ff00a99af1da8b04c211ae6f3c8f8bcab0164f650"
name = "github.com/aokoli/goutils"
packages = ["."]
pruneopts = "UT"
revision = "3391d3790d23d03408670993e957e8f408993c34"
version = "v1.0.1"
[[projects]]
digest = "1:8f8811f9be822914c3a25c6a071e93beb4c805d7b026cbf298bc577bc1cc945b"
name = "github.com/google/uuid"
packages = ["."]
pruneopts = "UT"
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
digest = "1:6b1e7618931d0ed9a17fec1c1d84271907aa61172c50b8272327f6cd4fafb650"
name = "github.com/huandu/xstrings"
packages = ["."]
pruneopts = "UT"
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
version = "v1.0.0"
[[projects]]
digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277"
name = "github.com/imdario/mergo"
packages = ["."]
pruneopts = "UT"
revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4"
version = "v0.3.6"
[[projects]]
digest = "1:55d0f7127962b0657acfd69650634057b3a08958226feee1b64a379666bb915e"
name = "github.com/urfave/cli"
packages = ["."]
pruneopts = "UT"
revision = "6011f165dc288c72abd8acd7722f837c5c64198d"
[[projects]]
digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d"
name = "go.uber.org/atomic"
packages = ["."]
pruneopts = "UT"
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
version = "v1.3.2"
[[projects]]
digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a"
name = "go.uber.org/multierr"
packages = ["."]
pruneopts = "UT"
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
digest = "1:9580b1b079114140ade8cec957685344d14f00119e0241f6b369633cb346eeb3"
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore",
]
pruneopts = "UT"
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
version = "v1.8.0"
[[projects]]
branch = "master"
digest = "1:7b1c378a71f617ec07229a1fcbb90b6a5344295823e1d732398f50fd9ae4bbec"
name = "golang.org/x/crypto"
packages = [
"pbkdf2",
"scrypt",
]
pruneopts = "UT"
revision = "b2aa35443fbc700ab74c586ae79b81c171851023"
[[projects]]
branch = "v2"
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Masterminds/sprig",
"github.com/imdario/mergo",
"github.com/urfave/cli",
"go.uber.org/zap",
"go.uber.org/zap/zapcore",
"gopkg.in/yaml.v2",
]
solver-name = "gps-cdcl"
solver-version = 1
-15
View File
@@ -1,15 +0,0 @@
[[constraint]]
name = "github.com/Masterminds/sprig"
version = "2.15.0"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/imdario/mergo"
version = "0.3.6"
+25 -6
View File
@@ -2,7 +2,7 @@ ORG ?= $(shell basename $(realpath ..))
PKGS := $(shell go list ./... | grep -v /vendor/)
build:
go build ${TARGETS}
go build -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
.PHONY: build
generate:
@@ -17,6 +17,11 @@ check:
go vet ${PKGS}
.PHONY: check
build-test-tools:
go build test/diff-yamls/diff-yamls.go
go build test/yamldiff/yamldiff.go
.PHONY: build-test-tools
test:
go test -v ${PKGS} -cover -race -p=1
.PHONY: test
@@ -25,16 +30,23 @@ integration:
bash test/integration/run.sh
.PHONY: integration
integration/vagrant:
$(MAKE) build GOOS=linux GOARCH=amd64
$(MAKE) build-test-tools GOOS=linux GOARCH=amd64
vagrant up
vagrant ssh -c 'HELMFILE_HELM3=1 make -C /vagrant integration'
.PHONY: integration/vagrant
cross:
env CGO_ENABLED=0 gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
env CGO_ENABLED=0 gox -os 'windows darwin linux' -arch '386 amd64 arm64' -osarch '!darwin/arm64 !darwin/386' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
.PHONY: cross
static-linux:
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
.PHONY: linux
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS=-mod=readonly go build -o "dist/helmfile_linux_amd64" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
.PHONY: static-linux
install:
env CGO_ENABLED=0 go install -ldflags '-X main.Version=${TAG}' ${TARGETS}
env CGO_ENABLED=0 go install -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
.PHONY: install
clean:
@@ -42,7 +54,8 @@ clean:
.PHONY: clean
pristine: generate fmt
git ls-files --exclude-standard --modified --deleted --others | diff /dev/null -
git diff | cat
git ls-files --exclude-standard --modified --deleted --others -x vendor | grep -v '^go.' | diff /dev/null -
.PHONY: pristine
release: pristine cross
@@ -58,6 +71,12 @@ run: image
push: image
docker push quay.io/${ORG}/helmfile:${TAG}
image/helm3:
docker build -f Dockerfile.helm3 -t quay.io/${ORG}/helmfile:helm3-${TAG} .
push/helm3: image/helm3
docker push quay.io/${ORG}/helmfile:helm3-${TAG}
tools:
go get -u github.com/tcnksm/ghr github.com/mitchellh/gox
.PHONY: tools
+12 -10
View File
@@ -1,25 +1,27 @@
# Paths Overivew
Using manifest files in conjunction with command line argument can be a bit confusing.
# Paths Overivew
A few rules to clear up this ambiguity:
Using manifest files in conjunction with command line argument can be a bit confusing.
A few rules to clear up this ambiguity:
- Absolute paths are always resolved as absolute paths
- Relative paths referenced *in* the helmfile manifest itself are relative to that manifest
- Relative paths referenced on the command line are relative to the current working directory the user is in
### Examples
There are several examples that we can go through in the `/examples` folder which demonstrate this.
There are several examples that we can go through in the [`/examples`](examples) folder which demonstrate this.
**Local Execution**
This is an example of a Helmfile manifest referencing a local value directly.
This is an example of a Helmfile manifest referencing a local value directly.
Indirect:
```
helmfile -f examples/deployments/local/charts.yaml sync
```
Direct:
Direct:
```
cd examples/deployments/local/
helmfile sync
@@ -27,14 +29,14 @@ helmfile sync
**Relative Paths in Helmfile**
This is an example of a Helmfile manifest using relative paths for values.
This is an example of a Helmfile manifest using relative paths for values.
Indirect:
```
helmfile -f examples/deployments/dev/charts.yaml sync
```
Direct:
Direct:
```
cd examples/deployments/dev/
helmfile sync
@@ -42,7 +44,7 @@ helmfile sync
**Relative Paths in Helmfile w/ --values overrides**
This is an example of a Helmfile manifest using relative paths for values including an additional `--values` from the command line.
This is an example of a Helmfile manifest using relative paths for values including an additional `--values` from the command line.
NOTE: The `--values` is resolved relative to the CWD of the terminal *not* the Helmfile manifest. You can see this with the `replicas` being adjusted to 3 now for the deployment.
@@ -51,7 +53,7 @@ Indirect:
helmfile -f examples/deployments/dev/charts.yaml sync --values values/replica-values.yaml
```
Direct:
Direct:
```
cd examples/deployments/dev/
helmfile sync --values ../../values/replica-values.yaml
+647 -86
View File
File diff suppressed because it is too large Load Diff
+28 -13
View File
@@ -1,18 +1,33 @@
# Helmfile Users
The following table is compiled from public information or from survey responses. If you are using Helmfile in your
company or organization, we would like to invite you to [participate in our user survey](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform)
to help us extend this list.
Who's actually using Helmfile? The following table is compiled from public information, survey response and PRs. It's
likely this is only a small selection of actual users, but it's a start.
If you are using Helmfile in your company or organization, we would like to invite you to create a PR to add your
information to this file.
<!-- TABLE_START -->
| Organization | Workload | More Info | Location |
|---|---|---|---|
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249) | San Francisco, CA, US |
| [ricardo.ch](https://www.ricardo.ch/) | proof-of-concept | | Zug, Switzerland |
| Organization | Workload | More Info | Location | Added |
|---|---|---|---|---|
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249), [Talk](https://www.slideshare.net/GregoryTaylor11/helm-at-reddit-from-local-dev-staging-to-production) | San Francisco, CA | April 2018 |
| [ricardo.ch](https://www.ricardo.ch/) | production | We're deploying our complete application platform using Helmfile. | Zug, Switzerland | April 2018 |
| [Hellofresh](https://engineering.hellofresh.com/) | production | We've been using helmfile in production since April 2018 for deploying all our infrastructure applications | Berlin, Germany | April 2018 |
| [Cherre](https://cherre.com/) | production | We have no public posts about using Helmfile, but we have been using it for a long time now | New York, NY | October 2018 |
| [Sight Machine](https://sightmachine.com/) | production | We don't have anything publicly posted about it, but have been using it for quite a while in production. | San Francisco, CA and Ann Arbor, MI | December 2018 |
| [TailorMed](https://tailormed.co/) | production | We're deploying all of our platform and all K8s resources using Helmfile. The power of Helmfile are more then amazing. Thank you! | Tel-Aviv, Israel | September 2020 |
| [VSHN – The DevOps Company](https://vshn.ch) | production | | Zurich, Switzerland | March 2019 |
| [Vlocity](https://vlocity.com/) | proof-of-concept | | Melbourne, Australia | March 2019 |
| [transit](https://transit.app/) | production | [Blog post](https://medium.com/@naseem_60378/helmfile-its-like-a-helm-for-your-helm-74a908581599). | Montreal, Canada | March 2019 |
| [uniqkey](https://uniqkey.eu/) | production | [Wiki Page](https://ocd-scm.github.io/ocd-meta/) | Copenhagen, Denmark | April 2019 |
| [bitsofinfo](https://github.com/bitsofinfo/helmfile-deploy) | production | Used with [helmfile-deploy](https://github.com/bitsofinfo/helmfile-deploy) to manage releases for dozens of apps | USA | July 2019 |
| [kloeckner-i](https://www.kloeckner-i.com/) | production | We are deploying our standard tools via helmfile to all our clusters. | Berlin, Germany | September 2019 |
| [American Express](https://www.americanexpress.com) | proof-of-concept | Orchestration of both internal cluster workloads and local developer environments. | London, GB | January 2020 |
| [Sportradar](https://www.sportradar.com) | production | Since mid-2019, we've been deploying our core infrastructure and several application stacks with Helmfile. | St. Gallen, Switzerland | March 2020 |
| [PedidosYa](https://www.pedidosya.com) | production | | Montevideo, Uruguay | June 2020 |
| [Jenkins OSS](https://jenkins.io) | production | [jenkins-infra/charts](https://github.com/jenkins-infra/charts) | * | July 2020 |
| [SettleMint](https://settlemint.com) | production | The SettleMint platform allows enterprises to spin up k8s clusters and deploy production grade blockchain networks and additional services. Helmfile is in charge of deploying these networks and services on demand out of the self service management ui. | Belgium, Singapore, UAE, India | October 2020 |
| [AutoTrader (UK)](https://www.autotrader.co.uk) | production | We've used Helmfile for 2+ years to deploy 400+ services | UK | October 2020 |
| [Trend Micro](https://www.trendmicro.com) | production | We manage 9 k8s clusters in a mono git repository with Helmfile | Taipei, Taiwan | December 2019 |
| [William Hill](https://www.williamhill.com) | production | We have standardised on Helmfile for managing both our application services and our Kubernetes platform components. | International | March 2021 |
| [The Hyve](https://www.thehyve.nl) | production | We've been using Helmfile since 2019 to deploy [Radar-base applications](https://github.com/RADAR-base/RADAR-Kubernetes). [Blog Post](https://www.thehyve.nl/articles/kubernetes-added-to-radar-base) | Utrecht, Netherlands | April 2021 |
<!-- TABLE_END -->
## Contact
Please fill out [the Google form](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform) to add your organization instead of opening a PR.
You can also [reach the maintainer on Twitter](https://twitter.com/DerCed).
Vendored
+19
View File
@@ -0,0 +1,19 @@
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "minikube.box"
config.vm.provision :shell, privileged: false,
inline: <<-EOS
set -e
sudo apt-get update
sudo apt-get install -y make docker.io
sudo systemctl start docker
sudo usermod -G docker $USER
cd /vagrant/.circleci
make all
EOS
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
end
end
+18
View File
@@ -0,0 +1,18 @@
#! /bin/bash
_helmfile_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -o bashdefault -o default -o nospace -F _helmfile_bash_autocomplete helmfile
+23
View File
@@ -0,0 +1,23 @@
#compdef helmfile
_helmfile_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
return
}
compdef _helmfile_zsh_autocomplete helmfile
-100
View File
@@ -1,100 +0,0 @@
package cmd
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"os/exec"
"strings"
"syscall"
)
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
a, fileOrDir, err := InitAppEntry(c, false)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStates(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
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
}
app := app.Init(&app.App{
KubeContext: kubeContext,
Logger: logger,
Reverse: reverse,
Env: env,
Namespace: namespace,
Selectors: selectors,
})
return app, fileOrDir, nil
}
func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
a, fileOrDir, err := InitAppEntry(c, reverse)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *exec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
status := e.Sys().(syscall.WaitStatus)
return cli.NewExitError(e.Error(), status.ExitStatus())
case *state.DiffError:
return cli.NewExitError(e.Error(), e.Code)
default:
return cli.NewExitError(e.Error(), 1)
}
}
return err
}
-37
View File
@@ -1,37 +0,0 @@
package cmd
import (
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
)
func Deps(a *app.App) cli.Command {
return cli.Command{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
errs := state.UpdateDeps(helm)
ok := len(errs) == 0
return ok, errs
})
},
}
}
+299
View File
@@ -0,0 +1,299 @@
## Advanced Features
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
- [Deploy Kustomization with Helmfile](#deploy-kustomizations-with-helmfile)
- [Adhoc Kustomization of Helm Charts](#adhoc-kustomization-of-helm-charts)
- [Adding dependencies without forking the chart](#adding-dependencies-without-forking-the-chart)
### Import Configuration Parameters into Helmfile
Helmfile integrates [vals]() to import configuration parameters from following backends:
- AWS SSM Parameter Store
- AWS SecretsManager
- Vault
- SOPS
See [Vals "Suported Backends"](https://github.com/variantdev/vals#suported-backends) for the full list of available backends.
This feature was implemented in https://github.com/roboll/helmfile/pull/906.
If you're curious how it's designed and how it works, please consult the pull request.
### Deploy Kustomizations with Helmfile
You can deploy [kustomize](https://github.com/kubernetes-sigs/kustomize) "kustomization"s with Helmfile.
Most of Kustomize operations that is usually done with `kustomize edit` can be done declaratively via Helm values.yaml files.
Under the hood, Helmfile transforms the kustomization into a local chart in a temporary directory so that it can be `helm upgrade --install`ed.
The transformation is done by generating (1)a temporary kustomization from various options and (2)temporary chart from the temporary kustomization.
An example pseudo code for the transformation logic can be written as:
```console
$ TMPCHART=/tmp/sometmpdir
$ mkdir -p ${TMPCHART}/templates
$ somehow_generate_chart_yaml ${TMPCHART}/Chart.yaml
$ TMPKUSTOMIZATION=/tmp/sometmpdir2
$ somehow_generate_temp_kustomization_yaml ${TMPKUSTOMIZATION}/kustomization.yaml
$ kustomize build ${TMPKUSTOMIZATION}/kustomization.yaml > ${TMPCHART}/templates/all.yaml
```
Let's say you have a `helmfile.yaml` that looks like the below:
```yaml
releases:
- name: myapp
chart: mykustomization
values:
- values.yaml
```
Helmfile firstly generates a temporary `kustomization.yaml` that looks like:
```yaml
bases:
- $(ABS_PATH_TO_HELMFILE_YAML}/mykustomization
```
Followed by the below steps:
- Running `kustomize edit set image $IMAGE` for every `$IMAGE` generated from your values.yaml
- Running `kustomize edit set nameprefix $NAMEPREFIX` with the nameprefix specified in your values.yaml
- Running `kustomize edit set namesuffix $NAMESUFFIX` with the namesuffix specified in your values.yaml
- Running `kustomize edit set namespace $NS` with the namespace specified in your values.yaml
A `values.yaml` file for kustomization would look like the below:
```yaml
images:
# kustomize edit set image mysql=eu.gcr.io/my-project/mysql@canary
- name: mysql
newName: eu.gcr.io/my-project/mysql
newTag: canary
# kustomize edit set image myapp=my-registry/my-app@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
- name: myapp
digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
newName: my-registry/my-app
# kustomize edit set nameprefix foo-
namePrefix: foo-
# kustomize edit set namesuffix -bar
nameSuffix: -bar
# kustomize edit set namespace myapp
namespace: myapp
```
At this point, Helmfile can generate a complete kustomization from the base kustomization you specified in `releases[].chart` of your helmfile.yaml and `values.yaml`,
which can be included in the temporary chart.
After all, Helmfile just installs the temporary chart like standard charts, which allows you to manage everything with Helmfile regardless of each app is declared using a Helm chart or a kustomization.
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of kustomization support and more.
### Adhoc Kustomization of Helm charts
With Helmfile's integration with Helmfile, not only deploying Kustomization as a Helm chart, you can kustomize charts before installation.
Currently, Helmfile allows you to set the following fields for kustomizing the chart:
- [`releases[].strategicMergePatches`](#strategicmergepatches)
- `releases[].jsonPatches`
- [`releases[].transformers`](#transformers)
#### `strategicMergePatches`
You can add/update any Kubernetes resource field rendered from a Helm chart by specifying `releases[].strategicMergePatches`:
```
repositories:
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
releases:
- name: raw1
chart: incubator/raw
values:
- resources:
- apiVersion: v1
kind: ConfigMap
metadata:
name: raw1
namespace: default
data:
foo: FOO
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: raw1
namespace: default
data:
bar: BAR
```
Running `helmfile template` on the above example results in a ConfigMap called `raw` whose `data` is:
```yaml
foo: FOO
bar: BAR
```
Please note that the second `data` field `bar` is coming from the strategic-merge patch defined in the above helmfile.yaml.
There's also `releases[].jsonPatches` that works similarly to `strategicMergePatches` but has additional capability to remove fields.
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of patching support and more.
#### `transformers`
You can set `transformers` to apply [Kustomize's transformers](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead).
Each item can be a path to a YAML or go template file, or an embedded transformer declaration as a YAML hash.
It's often used to add common labels and annotations to your resources.
In the below example. we add common annotations and labels every resource rendered from the `aws-load-balancer-controller` chart:
```yaml
releases:
- name: "aws-load-balancer-controller"
namespace: "kube-system"
forceNamespace: "kube-system"
chart: "center/aws/aws-load-balancer-controller"
transformers:
- apiVersion: builtin
kind: AnnotationsTransformer
metadata:
name: notImportantHere
annotations:
area: 51
greeting: take me to your leader
fieldSpecs:
- path: metadata/annotations
create: true
- apiVersion: builtin
kind: LabelTransformer
metadata:
name: notImportantHere
labels:
foo: bar
fieldSpecs:
- path: metadata/labels
create: true
```
As explained earlier, `transformers` can be not only a list of embedded transformers, but also YAML or go template files, or a mix of those three kinds.
```yaml
transformers:
# Embedded transformer
- apiVersion: builtin
kind: AnnotationsTransformer
metadata:
name: notImportantHere
annotations:
area: 51
greeting: take me to your leader
fieldSpecs:
- path: metadata/annotations
create: true
# YAML file
- path/to/transformer.yaml
# Go template
# The same set of template parameters as release values files templates is available.
- path/to/transformer.yaml.gotmpl
```
Please see https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information on how to declare transformers.
### Adding dependencies without forking the chart
With Helmfile, you can add chart dependencies to a Helm chart without forking it.
An example `helmfile.yaml` that adds a `stable/envoy` dependency to the release `foo` looks like the below:
```
repositories:
- name: stable
url: https://charts.helm.sh/stable
releases:
- name: foo
chart: ./path/to/foo
dependencies:
- chart: stable/envoy
version: 1.5
```
When Helmfile encounters `releases[].dependencies`, it creates a another temporary chart from `./path/to/foo` and adds the following `dependencies` to the `Chart.yaml`, so that you don't need to fork the chart.
```
dependencies:
- name: envoy
repo: https://charts.helm.sh/stable
condition: envoy.enabled
```
A Helm chart can have two or more dependencies for the same chart with different `alias`es. To give your dependency an `alias`, defien it like you would do in a standard `Chart.yaml`:
```
repositories:
- name: stable
url: https://charts.helm.sh/stable
releases:
- name: foo
chart: ./path/to/foo
dependencies:
- chart: stable/envoy
version: 1.5
alias: bar
- chart: stable/envoy
version: 1.5
alias: baz
```
which will tweaks the temporary chart's `Chart.yaml` to have:
```
dependencies:
- alias: bar
name: envoy
repo: https://charts.helm.sh/stable
condition: bar.enabled
- alias: baz
name: envoy
repo: https://charts.helm.sh/stable
condition: baz.enabled
```
Please see #649 for more context around this feature.
After the support for adhoc dependency to local chart (#1765),
you can even write local file paths relative to `helmfile.yaml` in `chart`:
```
releases:
- name: foo
chart: ./path/to/foo
dependencies:
- chart: ./path/to/bar
```
Internally, Helmfile creates another temporary chart from the local chart `./path/to/foo`, and modifies the chart's `Chart.yaml` dependencies to look like:
```
dependencies:
- alias: bar
name: bar
repo: file:///abs/path/to/bar
condition: bar.enabled
```
Please read https://github.com/roboll/helmfile/issues/1762#issuecomment-816341251 for more details.
+58
View File
@@ -0,0 +1,58 @@
# Secrets
helmfile can handle secrets using [helm-secrets](https://github.com/jkroepke/helm-secrets) plugin or using remote secrets storage
(everything that package [vals](https://github.com/variantdev/vals) can handle vault, AWS SSM etc)
This section will describe the second use case.
# Remote secrets
This paragraph will describe how to use remote secrets storage (vault, SSM etc) in helmfile
## Fetching single key
To fetch single key from remote secret storage you can use `fetchSecretValue` template function example below
```yaml
# helmfile.yaml
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com
environments:
default:
values:
- service:
password: ref+vault://svc/#pass
login: ref+vault://svc/#login
releases:
- name: service
namespace: default
labels:
cluster: services
secrets: vault
chart: stable/svc
version: 0.1.0
values:
- service:
login: {{ .Values.service.login | fetchSecretValue }} # this will resolve ref+vault://svc/#pass and fetch secret from vault
password: {{ .Values.service.password | fetchSecretValue | quote }}
# - values/service.yaml.gotmpl # alternatively
```
## Fetching multiple keys
Alternatively you can use `expandSecretRefs` to fetch a map of secrets
```yaml
# values/service.yaml.gotmpl
service:
{{ .Values.service | expandSecretRefs | toYaml | nindent 2 }}
```
This will produce
```yaml
# values/service.yaml
service:
login: svc-login # fetched from vault
password: pass
```
+281 -28
View File
@@ -2,6 +2,23 @@
This guide covers the Helmfile’s considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
## Helmfile .Values vs Helm .Values
Templating engine of Helmfile uses the same pipeline name `.Values` as Helm, so in some use-cases `.Values` of Helmfile and
Helm can be seen in the same file. To distinguish these two kinds of `.Values`, Helmfile provides an alias `.StateValues`
for its `.Values`.
```
app:
project: {{.Environmont.Name}}-{{.StateValues.project}} # Same as {{.Environmont.Name}}-{{.Values.project}}
{{`
extraEnvVars:
- name: APP_PROJECT
value: {{.Values.app.project}}
`}}
```
## Missing keys and Default values
helmfile tries its best to inform users for noticing potential mistakes.
@@ -11,13 +28,13 @@ One example of how helmfile achieves it is that, `helmfile` fails when you tried
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
```
{{ .Environment.Values.eventApi.replicas | default 1 }}
{{ .Values.eventApi.replicas | default 1 }}
```
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
In case it isn't a mistake and you do want to allow missing keys, use the `get` template function:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" }}
{{ .Values | get "eventApi.replicas" nil }}
```
This result in printing `<no value` in your template, that may or may not result in a failure.
@@ -25,14 +42,14 @@ This result in printing `<no value` in your template, that may or may not result
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" | default 1 }}
{{ .Values | get "eventApi.replicas" 1 }}
```
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
## Release Template / Conventional Directory Structure
Introducing helmfile into a large-scale project that involes dozens of releases often results in a lot of repetitions in `helmfile.yaml` files.
Introducing helmfile into a large-scale project that involves dozens of releases often results in a lot of repetitions in `helmfile.yaml` files.
The example below shows repetitions in `namespace`, `chart`, `values`, and `secrets`:
@@ -57,7 +74,7 @@ releases:
values:
- "./config/kubernetes-dashboard/values.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
values:
secrets:
- "./config/kubernetes-dashboard/secrets.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
```
@@ -89,35 +106,70 @@ releases:
<<: *default
```
Release Templating supports the following parts of release definition:
- basic fields: `name`, `namespace`, `chart`, `version`
- boolean fields: `installed`, `wait`, `waitForJobs`, `tillerless`, `verify` by the means of additional text
fields designed for templating only: `installedTemplate`, `waitTemplate`, `tillerlessTemplate`, `verifyTemplate`
```yaml
# ...
installedTemplate: '{{`{{ eq .Release.Namespace "kube-system" }}`}}'
waitTemplate: '{{`{{ eq .Release.Labels.tag "safe" | not }}`}}'
# ...
```
- `set` block values:
```yaml
# ...
setTemplate:
- name: '{{`{{ .Release.Name }}`}}'
values: '{{`{{ .Release.Namespace }}`}}'
# ...
```
- `values` and `secrets` file paths:
```yaml
# ...
valuesTemplate:
- config/{{`{{ .Release.Name }}`}}/values.yaml
secrets:
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
# ...
```
- inline `values` map:
```yaml
# ...
valuesTemplate:
- image:
tag: `{{ .Release.Labels.tag }}`
# ...
```
See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work.
## Layering
## Layering Release Values
You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default.
Please note, that it is not possible to layer `values` sections. If `values` is defined in the release and in the release template, only the `values` defined in the release will be considered. The same applies to `secrets` and `set`.
## Layering State Files
> See **Layering State Template Files** if you're layering templates.
You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and which release to be bundled by default.
Use Layering to extract the common parts into a dedicated *library helmfile*s, so that each helmfile becomes DRY.
Let's assume that your `helmfile.yaml` looks like:
```
{ readFile "commons.yaml" }}
---
{{ readFile "environments.yaml" }}
---
```yaml
bases:
- environments.yaml
releases:
- name: metricbeat
chart: stable/metricbeat
- name: myapp
chart: mychart
```
Whereas `commons.yaml` contained a monitoring agent:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
```
And `environments.yaml` contained well-known environments:
Whereas `environments.yaml` contained well-known environments:
```yaml
environments:
@@ -125,23 +177,24 @@ environments:
production:
```
At run time, template expressions in your `helmfile.yaml` are executed:
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
---
# environments.yaml
environments:
development:
production:
---
# helmfile.yaml
releases:
- name: myapp
chart: mychart
- name: metricbeat
chart: stable/metricbeat
```
Resulting YAML documents are merged in the order of occurrence,
Finally the resulting YAML documents are merged in the order of occurrence,
so that your `helmfile.yaml` becomes:
```yaml
@@ -150,7 +203,7 @@ environments:
production:
releases:
- name: metricbaet
- name: metricbeat
chart: stable/metricbeat
- name: myapp
chart: mychart
@@ -159,3 +212,203 @@ releases:
Great!
Now, repeat the above steps for each your `helmfile.yaml`, so that all your helmfiles becomes DRY.
Please also see [the discussion in the issue 388](https://github.com/roboll/helmfile/issues/388#issuecomment-491710348) for more advanced layering examples.
## Merging Arrays in Layers
Helmfile doesn't merge arrays across layers. That is, the below example doesn't work as you might have expected:
```yaml
releases:
- name: metricbeat
chart: stable/metricbeat
---
releases:
- name: myapp
chart: mychart
```
Helmfile overrides the `releases` array with the latest layer so the resulting state file will be:
```yaml
releases:
# metricbeat release disappeared! but that's how helmfile works
- name: myapp
chart: mychart
```
A work-around is to treat the state file as a go template and use `readFile` template function to import the common part of your state file as a plain text:
`common.yaml`:
```yaml
templates:
metricbeat: &metricbeat
name: metricbeat
chart: stable/metricbeat
```
`helmfile.yaml`:
```yaml
{{ readFile "common.yaml" }}
releases:
- <<: *metricbeat
- name: myapp
chart: mychart
```
## Layering State Template Files
Do you need to make your state file even more DRY?
Turned out layering state files wasn't enough for you?
Helmfile supports an advanced feature that allows you to compose state "template" files to generate the final state to be processed.
In the following example `helmfile.yaml.gotmpl`, each `---` separated part of the file is a go template.
`helmfile.yaml.gotmpl`:
```yaml
# Part 1: Reused Enviroment Values
bases:
- myenv.yaml
---
# Part 2: Reused Defaults
bases:
- mydefaults.yaml
---
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-{{ .Values.myname }}
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
```
Suppose the `myenv.yaml` and `test.env.yaml` loaded in the first part looks like:
`myenv.yaml`:
```yaml
environments:
test:
values:
- test.env.yaml
```
`test.env.yaml`:
```yaml
kubeContext: test
wait: false
cvOnly: false
myname: "dog"
```
Where the gotmpl file loaded in the second part looks like:
`mydefaults.yaml.gotmpl`:
```yaml
helmDefaults:
tillerNamespace: kube-system
kubeContext: {{ .Values.kubeContext }}
verify: false
{{ if .Values.wait }}
wait: true
{{ else }}
wait: false
{{ end }}
timeout: 600
recreatePods: false
force: true
```
Each go template is rendered in the context where `.Values` is inherited from the previous part.
So in `mydefaults.yaml.gotmpl`, both `.Values.kubeContext` and `.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to:
```yaml
helmDefaults:
tillerNamespace: kube-system
kubeContext: test
verify: false
wait: false
timeout: 600
recreatePods: false
force: true
```
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
```yaml
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-{{ .Values.myname }}
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
````
hence rendered to:
```yaml
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-dog
values:
replicaCount: 1
image:
repository: "nginx"
tag: "latest"
```
## Re-using environment state in sub-helmfiles
Do you want to decouple the environment state loading from the sub-helmfiles and load it only once?
This example shows how to do this:
```yaml
environments:
stage:
values:
- env/stage.yaml
prod:
values:
- env/prod.yaml
---
helmfiles:
- path: releases/myrelease/helmfile.yaml
values:
- {{ toYaml .Values | nindent 4 }}
# pass the current state values to the sub-helmfile
# add other values to use overlay logic here
```
and `releases/myrelease/helmfile.yaml` is as DRY as
```yaml
releases:
- name: mychart-{{ .Values.myrelease.myname }}
installed: {{ .Values | get "myrelease.enabled" false }}
chart: mychart
version: {{ .Values.myrelease.version }}
labels:
chart: mychart
values:
- values.yaml.gotmpl
# templated values would also inherit the values passed from upstream
```
View File
-8
View File
@@ -1,8 +0,0 @@
package environment
type Environment struct {
Name string
Values map[string]interface{}
}
var EmptyEnvironment Environment
-102
View File
@@ -1,102 +0,0 @@
package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"os"
)
type Hook struct {
Name string `yaml:"name"`
Events []string `yaml:"events"`
Command string `yaml:"command"`
Args []string `yaml:"args"`
}
type event struct {
Name string
}
type Bus struct {
Runner helmexec.Runner
Hooks []Hook
BasePath string
StateFilePath string
Namespace string
Env environment.Environment
ReadFile func(string) ([]byte, error)
Logger *zap.SugaredLogger
}
func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error) {
if bus.Runner == nil {
bus.Runner = helmexec.ShellRunner{
Dir: bus.BasePath,
}
}
executed := false
for _, hook := range bus.Hooks {
contained := false
for _, e := range hook.Events {
contained = contained || e == evt
}
if !contained {
continue
}
var err error
name := hook.Name
if name == "" {
name = hook.Command
}
fmt.Fprintf(os.Stderr, "%s: basePath=%s\n", bus.StateFilePath, bus.BasePath)
data := map[string]interface{}{
"Environment": bus.Env,
"Namespace": bus.Namespace,
"Event": event{
Name: evt,
},
}
for k, v := range context {
data[k] = v
}
render := tmpl.NewTextRenderer(bus.ReadFile, bus.BasePath, data)
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
command, err := render.RenderTemplateText(hook.Command)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
args := make([]string, len(hook.Args))
for i, raw := range hook.Args {
args[i], err = render.RenderTemplateText(raw)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
}
bytes, err := bus.Runner.Execute(command, args)
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
if err != nil {
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
}
executed = true
}
return executed, nil
}
-113
View File
@@ -1,113 +0,0 @@
package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"os"
"testing"
)
var logger = helmexec.NewLogger(os.Stdout, "warn")
type runner struct {
}
func (r *runner) Execute(cmd string, args []string) ([]byte, error) {
if cmd == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
}
for _, a := range args {
if a == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid arg: %s", a)
}
}
return []byte(""), nil
}
func TestTrigger(t *testing.T) {
cases := []struct {
name string
hook *Hook
triggeredEvt string
expectedResult bool
expectedErr string
}{
{
"okhook1",
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
"foo",
true,
"",
},
{
"missinghook1",
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
"bar",
false,
"",
},
{
"nohook1",
nil,
"bar",
false,
"",
},
{
"nghook1",
&Hook{"nghook1", []string{"foo"}, "ng", []string{}},
"foo",
false,
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
},
{
"nghook2",
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}},
"foo",
false,
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
},
}
readFile := func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: %s", filename)
}
for _, c := range cases {
hooks := []Hook{}
if c.hook != nil {
hooks = append(hooks, *c.hook)
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "path/to/helmfile.yaml",
BasePath: "path/to",
Namespace: "myns",
Env: environment.Environment{Name: "prod"},
Logger: logger,
ReadFile: readFile,
}
bus.Runner = &runner{}
data := map[string]interface{}{
"Release": "myrel",
"HelmfileCommand": "mycmd",
}
ok, err := bus.Trigger(c.triggeredEvt, data)
if ok != c.expectedResult {
t.Errorf("unexpected result for case \"%s\": expected=%v, actual=%v", c.name, c.expectedResult, ok)
}
if c.expectedErr != "" {
if err == nil {
t.Error("error expected, but not occurred")
} else if err.Error() != c.expectedErr {
t.Errorf("unexpected error for case \"%s\": expected=%s, actual=%v", c.name, c.expectedErr, err)
}
} else {
if err != nil {
t.Errorf("unexpected error for case \"%s\": %v", c.name, err)
}
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ You would then start a database migration job by executing:
```console
# Start a database migration for the prod environment
$ RAILS_ENV=prod helmfile sync --selector job=dbmigrator
$ RAILS_ENV=prod helmfile --selector job=dbmigrator sync
# Tail log until you are satisfied
$ kubectl logs -l job=dbmigrator
+42
View File
@@ -0,0 +1,42 @@
module github.com/roboll/helmfile
go 1.14
require (
github.com/Azure/azure-sdk-for-go v35.0.0+incompatible // indirect
github.com/Masterminds/semver/v3 v3.1.0
github.com/Masterminds/sprig/v3 v3.1.0
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/davecgh/go-spew v1.1.1
github.com/frankban/quicktest v1.11.2 // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/go-test/deep v1.0.7
github.com/golang/mock v1.5.0
github.com/google/go-cmp v0.5.5
github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-getter v1.5.0
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
github.com/hashicorp/go-version v1.2.1
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
github.com/imdario/mergo v0.3.11
github.com/jessevdk/go-flags v1.4.0
github.com/kylelemons/godebug v1.1.0
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-isatty v0.0.12
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
github.com/r3labs/diff v1.1.0
github.com/spf13/cobra v1.1.1
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/urfave/cli v1.22.5
github.com/variantdev/chartify v0.8.5
github.com/variantdev/dag v1.0.0
github.com/variantdev/vals v0.14.0
go.uber.org/multierr v1.6.0
go.uber.org/zap v1.16.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.3.0
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.0.3
k8s.io/apimachinery v0.19.3
)
+1103
View File
File diff suppressed because it is too large Load Diff
-221
View File
@@ -1,221 +0,0 @@
package helmexec
import (
"io"
"io/ioutil"
"os"
"strings"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
command = "helm"
)
type execer struct {
helmBinary string
runner Runner
logger *zap.SugaredLogger
kubeContext string
extra []string
decryptionMutex sync.Mutex
}
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
var cfg zapcore.EncoderConfig
cfg.MessageKey = "message"
out := zapcore.AddSync(writer)
var level zapcore.Level
err := level.Set(logLevel)
if err != nil {
panic(err)
}
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
out,
level,
)
return zap.New(core).Sugar()
}
// New for running helm commands
func New(logger *zap.SugaredLogger, kubeContext string) *execer {
return &execer{
helmBinary: command,
logger: logger,
kubeContext: kubeContext,
runner: &ShellRunner{},
}
}
func (helm *execer) SetExtraArgs(args ...string) {
helm.extra = args
}
func (helm *execer) SetHelmBinary(bin string) {
helm.helmBinary = bin
}
func (helm *execer) AddRepo(name, repository, certfile, keyfile, username, password string) error {
var args []string
args = append(args, "repo", "add", name, repository)
if certfile != "" && keyfile != "" {
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
}
if username != "" && password != "" {
args = append(args, "--username", username, "--password", password)
}
helm.logger.Infof("Adding repo %v %v", name, repository)
out, err := helm.exec(args...)
helm.write(out)
return err
}
func (helm *execer) UpdateRepo() error {
helm.logger.Info("Updating repo")
out, err := helm.exec("repo", "update")
helm.write(out)
return err
}
func (helm *execer) UpdateDeps(chart string) error {
helm.logger.Infof("Updating dependency %v", chart)
out, err := helm.exec("dependency", "update", chart)
helm.write(out)
return err
}
func (helm *execer) BuildDeps(chart string) error {
helm.logger.Infof("Building dependency %v", chart)
out, err := helm.exec("dependency", "build", chart)
helm.write(out)
return err
}
func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
helm.logger.Infof("Upgrading %v", chart)
out, err := helm.exec(append([]string{"upgrade", "--install", "--reset-values", name, chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) ReleaseStatus(name string, flags ...string) error {
helm.logger.Infof("Getting status %v", name)
out, err := helm.exec(append([]string{"status", name}, flags...)...)
helm.write(out)
return err
}
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}, flags...)...)
helm.write(out)
return string(out), err
}
func (helm *execer) DecryptSecret(name string) (string, error) {
// Prevents https://github.com/roboll/helmfile/issues/258
helm.decryptionMutex.Lock()
defer helm.decryptionMutex.Unlock()
helm.logger.Infof("Decrypting secret %v", name)
out, err := helm.exec(append([]string{"secrets", "dec", name})...)
helm.write(out)
if err != nil {
return "", err
}
tmpFile, err := ioutil.TempFile("", "secret")
if err != nil {
return "", err
}
defer tmpFile.Close()
// os.Rename seems to results in "cross-device link` errors in some cases
// Instead of moving, copy it to the destination temp file as a work-around
// See https://github.com/roboll/helmfile/issues/251#issuecomment-417166296f
decFilename := name + ".dec"
decFile, err := os.Open(decFilename)
if err != nil {
return "", err
}
defer decFile.Close()
_, err = io.Copy(tmpFile, decFile)
if err != nil {
return "", err
}
if err := decFile.Close(); err != nil {
return "", err
}
if err := os.Remove(decFilename); err != nil {
return "", err
}
return tmpFile.Name(), err
}
func (helm *execer) TemplateRelease(chart string, flags ...string) error {
out, err := helm.exec(append([]string{"template", chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
helm.logger.Infof("Comparing %v %v", name, chart)
out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) Lint(chart string, flags ...string) error {
helm.logger.Infof("Linting %v", chart)
out, err := helm.exec(append([]string{"lint", chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) Fetch(chart string, flags ...string) error {
helm.logger.Infof("Fetching %v", chart)
out, err := helm.exec(append([]string{"fetch", chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) DeleteRelease(name string, flags ...string) error {
helm.logger.Infof("Deleting %v", name)
out, err := helm.exec(append([]string{"delete", name}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) TestRelease(name string, flags ...string) error {
helm.logger.Infof("Testing %v", name)
out, err := helm.exec(append([]string{"test", name}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) exec(args ...string) ([]byte, error) {
cmdargs := args
if len(helm.extra) > 0 {
cmdargs = append(cmdargs, helm.extra...)
}
if helm.kubeContext != "" {
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
}
helm.logger.Debugf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
return helm.runner.Execute(helm.helmBinary, cmdargs)
}
func (helm *execer) write(out []byte) {
if len(out) > 0 {
helm.logger.Infof("%s", out)
}
}
-378
View File
@@ -1,378 +0,0 @@
package helmexec
import (
"bytes"
"os"
"reflect"
"testing"
"go.uber.org/zap"
)
// Mocking the command-line runner
type mockRunner struct {
output []byte
err error
}
func (mock *mockRunner) Execute(cmd string, args []string) ([]byte, error) {
return []byte{}, nil
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
execer := New(logger, kubeContext)
execer.runner = &mockRunner{}
return execer
}
// Test methods
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(logger, "dev")
if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext")
}
if buffer.String() != "something" {
t.Error("helmexec.New() - changed buffer")
}
if len(helm.extra) != 0 {
t.Error("helmexec.New() - extra args not empty")
}
}
func Test_SetExtraArgs(t *testing.T) {
helm := New(NewLogger(os.Stdout, "info"), "dev")
helm.SetExtraArgs()
if len(helm.extra) != 0 {
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
}
helm.SetExtraArgs("foo")
if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
t.Error("helmexec.SetExtraArgs() - one extra argument missing")
}
helm.SetExtraArgs("alpha", "beta")
if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
}
}
func Test_SetHelmBinary(t *testing.T) {
helm := New(NewLogger(os.Stdout, "info"), "dev")
if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not helm")
}
helm.SetHelmBinary("foo")
if helm.helmBinary != "foo" {
t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
}
}
func Test_AddRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem", "", "")
expected := `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.UpdateRepo()
expected := `Updating repo
exec: helm repo update --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_SyncRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.SyncRelease("release", "chart", "--timeout 10", "--wait")
expected := `Upgrading chart
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SyncRelease("release", "chart")
expected = `Upgrading chart
exec: helm upgrade --install --reset-values release chart --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.UpdateDeps("./chart/foo")
expected := `Updating dependency ./chart/foo
exec: helm dependency update ./chart/foo --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.UpdateDeps("./chart/foo")
expected = `Updating dependency ./chart/foo
exec: helm dependency update ./chart/foo --verify --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_BuildDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.BuildDeps("./chart/foo")
expected := `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.BuildDeps("./chart/foo")
expected = `Building dependency ./chart/foo
exec: helm dependency build ./chart/foo --verify --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DecryptSecret(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DecryptSecret("secretName")
expected := `Decrypting secret secretName
exec: helm secrets dec secretName --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DiffRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DiffRelease("release", "chart", "--timeout 10", "--wait")
expected := `Comparing release chart
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.DiffRelease("release", "chart")
expected = `Comparing release chart
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DeleteRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease("release")
expected := `Deleting release
exec: helm delete release --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DeleteRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease("release", "--purge")
expected := `Deleting release
exec: helm delete release --purge --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_TestRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease("release")
expected := `Testing release
exec: helm test release --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_TestRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease("release", "--cleanup", "--timeout", "60")
expected := `Testing release
exec: helm test release --cleanup --timeout 60 --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_ReleaseStatus(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.ReleaseStatus("myRelease")
expected := `Getting status myRelease
exec: helm status myRelease --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_exec(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "")
helm.exec("version")
expected := "exec: helm version\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
helm = MockExecer(logger, "dev")
ret, _ := helm.exec("diff")
if len(ret) != 0 {
t.Error("helmexec.exec() - expected empty return value")
}
buffer.Reset()
helm = MockExecer(logger, "dev")
helm.exec("diff", "release", "chart", "--timeout 10", "--wait")
expected = "exec: helm diff release chart --timeout 10 --wait --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.exec("version")
expected = "exec: helm version --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("foo")
helm.exec("version")
expected = "exec: helm version foo --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm = MockExecer(logger, "")
helm.SetHelmBinary("overwritten")
helm.exec("version")
expected = "exec: overwritten version\n"
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Lint(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.Lint("path/to/chart", "--values", "file.yml")
expected := `Linting path/to/chart
exec: helm lint path/to/chart --values file.yml --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Fetch(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
expected := `Fetching chart
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
var logLevelTests = map[string]string{
"debug": `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
`,
"info": `Adding repo myRepo https://repo.example.com/
`,
"warn": ``,
}
func Test_LogLevels(t *testing.T) {
var buffer bytes.Buffer
for logLevel, expected := range logLevelTests {
buffer.Reset()
logger := NewLogger(&buffer, logLevel)
helm := MockExecer(logger, "")
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
}
-22
View File
@@ -1,22 +0,0 @@
package helmexec
// Interface for executing helm commands
type Interface interface {
SetExtraArgs(args ...string)
SetHelmBinary(bin string)
AddRepo(name, repository, certfile, keyfile, username, password string) error
UpdateRepo() error
BuildDeps(chart string) error
UpdateDeps(chart string) error
SyncRelease(name, chart string, flags ...string) error
DiffRelease(name, chart string, flags ...string) error
TemplateRelease(chart string, flags ...string) error
Fetch(chart string, flags ...string) error
Lint(chart string, flags ...string) error
ReleaseStatus(name string, flags ...string) error
DeleteRelease(name string, flags ...string) error
TestRelease(name string, flags ...string) error
List(filter string, flags ...string) (string, error)
DecryptSecret(name string) (string, error)
}
-27
View File
@@ -1,27 +0,0 @@
package helmexec
import (
"os/exec"
)
const (
tmpPrefix = "helmfile-"
tmpSuffix = "-exec"
)
// Runner interface for shell commands
type Runner interface {
Execute(cmd string, args []string) ([]byte, error)
}
// ShellRunner implemention for shell commands
type ShellRunner struct {
Dir string
}
// Execute a shell command
func (shell ShellRunner) Execute(cmd string, args []string) ([]byte, error) {
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
return preparedCmd.CombinedOutput()
}
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
echo "=> Building container"
docker build \
--tag "$IMAGE_NAME" \
--file Dockerfile .
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
go()
{
docker run --rm -v "$(pwd):/workspace" -w /workspace golang:latest go $@;
}
go get -u github.com/tcnksm/ghr github.com/mitchellh/gox
go mod vendor
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
echo "=> Fetch unshallow origin"
git fetch --unshallow origin || true
echo "=> Tag image:"
docker tag "${IMAGE_NAME}" "${DOCKER_REPO}:$(git describe --tags --abbrev=0 HEAD^)"
docker tag "${IMAGE_NAME}" "${DOCKER_REPO}:$(git describe --tags --abbrev=0 HEAD)"
echo "=> Push images"
docker push "${DOCKER_REPO}"
+664 -305
View File
File diff suppressed because it is too large Load Diff
+1809 -172
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+790
View File
@@ -0,0 +1,790 @@
package app
import (
"bufio"
"bytes"
"io"
"path/filepath"
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/exectest"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"github.com/variantdev/vals"
)
func TestSync(t *testing.T) {
type fields struct {
skipNeeds bool
includeNeeds bool
}
type testcase struct {
fields fields
ns string
concurrency int
skipDiffOnInstall bool
error string
files map[string]string
selectors []string
lists map[exectest.ListKey]string
upgraded []exectest.Release
deleted []exectest.Release
log string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
Lists: tc.lists,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := &bytes.Buffer{}
func() {
t.Helper()
logReader, logWriter := io.Pipe()
logFlushed := &sync.WaitGroup{}
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
logFlushed.Add(1)
go func() {
scanner := bufio.NewScanner(logReader)
for scanner.Scan() {
bs.Write(scanner.Bytes())
bs.WriteString("\n")
}
logFlushed.Done()
}()
defer func() {
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
if err := logWriter.Close(); err != nil {
panic(err)
}
logFlushed.Wait()
}()
logger := helmexec.NewLogger(logWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
if tc.ns != "" {
app.Namespace = tc.ns
}
if tc.selectors != nil {
app.Selectors = tc.selectors
}
syncErr := app.Sync(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
includeNeeds: tc.fields.includeNeeds,
})
var gotErr string
if syncErr != nil {
gotErr = syncErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
for relIdx := range wantUpgrades {
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
if len(wantDeletes) > len(helm.Deleted) {
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
}
for relIdx := range wantDeletes {
if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
}
for flagIdx := range wantDeletes[relIdx].Flags {
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
}
}
}
}()
if tc.log != "" {
actual := bs.String()
diff, exists := testhelper.Diff(tc.log, actual, 3)
if exists {
t.Errorf("unexpected log:\nDIFF\n%s\nEOD", diff)
}
}
}
t.Run("skip-needs=true", func(t *testing.T) {
check(t, testcase{
fields: fields{
skipNeeds: true,
},
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
upgraded: []exectest.Release{
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
Affected releases are:
external-secrets (incubator/raw) UPDATED
my-release (incubator/raw) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 default/default/external-secrets
2 default/default/my-release
processing releases in group 1/2: default/default/external-secrets
getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: default/default/my-release
getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
external-secrets incubator/raw
my-release incubator/raw
`,
})
})
t.Run("skip-needs=false include-needs=true", func(t *testing.T) {
check(t, testcase{
fields: fields{
skipNeeds: false,
includeNeeds: true,
},
error: ``,
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
upgraded: []exectest.Release{},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
Affected releases are:
external-secrets (incubator/raw) UPDATED
kubernetes-external-secrets (incubator/raw) UPDATED
my-release (incubator/raw) UPDATED
processing 3 groups of releases in this order:
GROUP RELEASES
1 default/kube-system/kubernetes-external-secrets
2 default/default/external-secrets
3 default/default/my-release
processing releases in group 1/3: default/kube-system/kubernetes-external-secrets
getting deployed release version failed:unexpected list key: {^kubernetes-external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/3: default/default/external-secrets
getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 3/3: default/default/my-release
getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
kubernetes-external-secrets incubator/raw
external-secrets incubator/raw
my-release incubator/raw
`,
})
})
t.Run("skip-needs=false include-needs=true with installed but disabled release", func(t *testing.T) {
check(t, testcase{
fields: fields{
skipNeeds: false,
includeNeeds: true,
},
error: ``,
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
installed: false
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
upgraded: []exectest.Release{},
lists: map[exectest.ListKey]string{
// delete frontend-v1 and backend-v1
exectest.ListKey{Filter: "^kubernetes-external-secrets$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
^kubernetes-external-secrets$ 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7: installed: false
8:
9: - name: external-secrets
10: chart: incubator/raw
11: namespace: default
12: labels:
13: app: test
14: needs:
15: - kube-system/kubernetes-external-secrets
16:
17: - name: my-release
18: chart: incubator/raw
19: namespace: default
20: labels:
21: app: test
22: needs:
23: - default/external-secrets
24:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7: installed: false
8:
9: - name: external-secrets
10: chart: incubator/raw
11: namespace: default
12: labels:
13: app: test
14: needs:
15: - kube-system/kubernetes-external-secrets
16:
17: - name: my-release
18: chart: incubator/raw
19: namespace: default
20: labels:
21: app: test
22: needs:
23: - default/external-secrets
24:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
Affected releases are:
external-secrets (incubator/raw) UPDATED
kubernetes-external-secrets (incubator/raw) DELETED
my-release (incubator/raw) UPDATED
processing 1 groups of releases in this order:
GROUP RELEASES
1 default/kube-system/kubernetes-external-secrets
processing releases in group 1/1: default/kube-system/kubernetes-external-secrets
processing 2 groups of releases in this order:
GROUP RELEASES
1 default/default/external-secrets
2 default/default/my-release
processing releases in group 1/2: default/default/external-secrets
getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: default/default/my-release
getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
external-secrets incubator/raw
my-release incubator/raw
DELETED RELEASES:
NAME
kubernetes-external-secrets
`,
})
})
t.Run("skip-needs=false include-needs=true with not installed and disabled release", func(t *testing.T) {
check(t, testcase{
fields: fields{
skipNeeds: false,
includeNeeds: true,
},
error: ``,
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
installed: false
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
upgraded: []exectest.Release{},
lists: map[exectest.ListKey]string{
// delete frontend-v1 and backend-v1
exectest.ListKey{Filter: "^kubernetes-external-secrets$", Flags: helmV2ListFlags}: ``,
},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7: installed: false
8:
9: - name: external-secrets
10: chart: incubator/raw
11: namespace: default
12: labels:
13: app: test
14: needs:
15: - kube-system/kubernetes-external-secrets
16:
17: - name: my-release
18: chart: incubator/raw
19: namespace: default
20: labels:
21: app: test
22: needs:
23: - default/external-secrets
24:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7: installed: false
8:
9: - name: external-secrets
10: chart: incubator/raw
11: namespace: default
12: labels:
13: app: test
14: needs:
15: - kube-system/kubernetes-external-secrets
16:
17: - name: my-release
18: chart: incubator/raw
19: namespace: default
20: labels:
21: app: test
22: needs:
23: - default/external-secrets
24:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
Affected releases are:
external-secrets (incubator/raw) UPDATED
my-release (incubator/raw) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 default/default/external-secrets
2 default/default/my-release
processing releases in group 1/2: default/default/external-secrets
getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: default/default/my-release
getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
external-secrets incubator/raw
my-release incubator/raw
`,
})
})
t.Run("bad --selector", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test_non_existent"},
upgraded: []exectest.Release{},
error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile",
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
0 release(s) matching app=test_non_existent found in helmfile.yaml
`,
})
})
}
+4531 -144
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
package main
package app
import (
"bufio"
@@ -11,7 +11,7 @@ import (
// 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 {
func AskForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
+209
View File
@@ -0,0 +1,209 @@
package app
import "go.uber.org/zap"
type ConfigProvider interface {
Args() string
HelmBinary() string
FileOrDir() string
KubeContext() string
Namespace() string
Chart() string
Selectors() []string
StateValuesSet() map[string]interface{}
StateValuesFiles() []string
Env() string
loggingConfig
}
type DeprecatedChartsConfigProvider interface {
Values() []string
concurrencyConfig
loggingConfig
}
type DepsConfigProvider interface {
Args() string
SkipRepos() bool
}
type ReposConfigProvider interface {
Args() string
}
type ApplyConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipCRDs() bool
SkipDeps() bool
Wait() bool
WaitForJobs() bool
IncludeTests() bool
SuppressSecrets() bool
ShowSecrets() bool
SuppressDiff() bool
DetailedExitcode() bool
NoColor() bool
Context() int
DiffOutput() string
RetainValuesFiles() bool
SkipCleanup() bool
SkipDiffOnInstall() bool
SkipNeeds() bool
IncludeNeeds() bool
concurrencyConfig
interactive
loggingConfig
}
type SyncConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipCRDs() bool
SkipDeps() bool
Wait() bool
WaitForJobs() bool
SkipNeeds() bool
IncludeNeeds() bool
concurrencyConfig
loggingConfig
}
type DiffConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipCRDs() bool
SkipDeps() bool
IncludeTests() bool
SuppressSecrets() bool
ShowSecrets() bool
SuppressDiff() bool
SkipNeeds() bool
IncludeNeeds() bool
DetailedExitcode() bool
NoColor() bool
Context() int
DiffOutput() string
concurrencyConfig
}
type DeleteConfigProvider interface {
Args() string
Purge() bool
SkipDeps() bool
interactive
loggingConfig
concurrencyConfig
}
type DestroyConfigProvider interface {
Args() string
SkipDeps() bool
interactive
loggingConfig
concurrencyConfig
}
type TestConfigProvider interface {
Args() string
SkipDeps() bool
Timeout() int
Cleanup() bool
Logs() bool
concurrencyConfig
}
type LintConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
concurrencyConfig
}
type FetchConfigProvider interface {
SkipDeps() bool
OutputDir() string
concurrencyConfig
}
type TemplateConfigProvider interface {
Args() string
Values() []string
Set() []string
OutputDirTemplate() string
Validate() bool
SkipDeps() bool
SkipCleanup() bool
OutputDir() string
IncludeCRDs() bool
IncludeNeeds() bool
concurrencyConfig
}
type WriteValuesConfigProvider interface {
Values() []string
Set() []string
OutputFileTemplate() string
SkipDeps() bool
}
type StatusesConfigProvider interface {
Args() string
concurrencyConfig
}
type StateConfigProvider interface {
EmbedValues() bool
}
type concurrencyConfig interface {
Concurrency() int
}
type loggingConfig interface {
Logger() *zap.SugaredLogger
}
type interactive interface {
Interactive() bool
}
type ListConfigProvider interface {
Output() string
}
+19 -4
View File
@@ -1,7 +1,22 @@
package app
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
import (
"os"
"strings"
)
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
DefaultHelmfileDirectory = "helmfile.d"
ExperimentalEnvVar = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
)
func experimentalModeEnabled() bool {
return os.Getenv(ExperimentalEnvVar) == "true"
}
func isExplicitSelectorInheritanceEnabled() bool {
return experimentalModeEnabled() || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit)
}
+27
View File
@@ -0,0 +1,27 @@
package app
import (
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
"os"
"testing"
)
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
//env var ExperimentalEnvVar is set
assert.Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to true
defer env.Patch(t, ExperimentalEnvVar, "true")()
assert.Check(t, isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to anything
defer env.Patch(t, ExperimentalEnvVar, "foo")()
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
assert.Check(t, isExplicitSelectorInheritanceEnabled())
}
+19 -16
View File
@@ -1,33 +1,36 @@
package app
import "github.com/roboll/helmfile/state"
import (
"github.com/roboll/helmfile/pkg/state"
)
type Context struct {
updatedRepos map[string]struct{}
updatedRepos map[string]bool
updatedReposV2 map[string]bool
}
func NewContext() Context {
return Context{
updatedRepos: map[string]struct{}{},
updatedRepos: map[string]bool{},
updatedReposV2: map[string]bool{},
}
}
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
var errs []error
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) error {
var (
updated []string
err error
)
allUpdated := true
for _, r := range st.Repositories {
_, exists := ctx.updatedRepos[r.Name]
allUpdated = allUpdated && exists
if helm.IsHelm3() {
updated, err = st.SyncRepos(helm, ctx.updatedRepos)
} else {
updated, err = st.SyncRepos(helm, ctx.updatedReposV2)
}
if !allUpdated {
errs = st.SyncRepos(helm)
for _, r := range st.Repositories {
ctx.updatedRepos[r.Name] = struct{}{}
}
for _, r := range updated {
ctx.updatedRepos[r] = true
}
return errs
return err
}
+245
View File
@@ -0,0 +1,245 @@
package app
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/state"
"github.com/variantdev/vals"
"go.uber.org/zap"
)
const (
DefaultHelmBinary = state.DefaultHelmBinary
)
type desiredStateLoader struct {
overrideKubeContext string
overrideHelmBinary string
env string
namespace string
chart string
readFile func(string) ([]byte, error)
deleteFile func(string) error
fileExists func(string) (bool, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
directoryExistsAt func(string) bool
getHelm func(*state.HelmState) helmexec.Interface
remote *remote.Remote
logger *zap.SugaredLogger
valsRuntime vals.Evaluator
}
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
var overrodeEnv *environment.Environment
args := opts.Environment.OverrideValues
if len(args) > 0 {
if opts.CalleePath == "" {
return nil, fmt.Errorf("bug: opts.CalleePath was nil: f=%s, opts=%v", f, opts)
}
storage := state.NewStorage(opts.CalleePath, ld.logger, ld.glob)
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile, ld.logger, ld.remote)
handler := state.MissingFileHandlerError
vals, err := envld.LoadEnvironmentValues(&handler, args)
if err != nil {
return nil, err
}
overrodeEnv = &environment.Environment{
Name: ld.env,
Values: vals,
}
}
st, err := ld.loadFileWithOverrides(nil, overrodeEnv, filepath.Dir(f), filepath.Base(f), true)
if err != nil {
return nil, err
}
if opts.Reverse {
st.Reverse()
}
if ld.overrideKubeContext != "" {
if st.OverrideKubeContext != "" {
return nil, errors.New("err: Cannot use option --kube-context and set attribute kubeContext.")
}
st.OverrideKubeContext = ld.overrideKubeContext
// HelmDefaults.KubeContext is also overriden in here
// to set default release value properly.
st.HelmDefaults.KubeContext = ld.overrideKubeContext
}
if ld.namespace != "" {
if st.OverrideNamespace != "" {
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
}
st.OverrideNamespace = ld.namespace
}
if ld.chart != "" {
if st.OverrideChart != "" {
return nil, errors.New("err: Cannot use option --chart and set attribute chart.")
}
st.OverrideChart = ld.chart
}
return st, nil
}
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
return ld.loadFileWithOverrides(inheritedEnv, nil, baseDir, file, evaluateBases)
}
func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
var f string
if filepath.IsAbs(file) {
f = file
} else {
f = filepath.Join(baseDir, file)
}
fileBytes, err := ld.readFile(f)
if err != nil {
return nil, err
}
ext := filepath.Ext(f)
var self *state.HelmState
if !experimentalModeEnabled() || ext == ".gotmpl" {
self, err = ld.renderAndLoad(
inheritedEnv,
overrodeEnv,
baseDir,
f,
fileBytes,
evaluateBases,
)
} else {
self, err = ld.load(
fileBytes,
baseDir,
file,
evaluateBases,
inheritedEnv,
overrodeEnv,
)
}
if err != nil {
return nil, err
}
for i, h := range self.Helmfiles {
if h.Path == f {
return nil, fmt.Errorf("%s contains a recursion into the same sub-helmfile at helmfiles[%d]", f, i)
}
if h.Path == "." {
return nil, fmt.Errorf("%s contains a recursion into the the directory containing this helmfile at helmfiles[%d]", f, i)
}
}
return self, nil
}
func (a *desiredStateLoader) underlying() *state.StateCreator {
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.directoryExistsAt, a.valsRuntime, a.getHelm, a.overrideHelmBinary, a.remote)
c.DeleteFile = a.deleteFile
c.LoadFile = a.loadFile
return c
}
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool, env, overrodeEnv *environment.Environment) (*state.HelmState, error) {
merged, err := env.Merge(overrodeEnv)
if err != nil {
return nil, err
}
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, merged)
if err != nil {
return nil, err
}
helmfiles, err := st.ExpandedHelmfiles()
if err != nil {
return nil, err
}
st.Helmfiles = helmfiles
return st, nil
}
func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
// Allows part-splitting to work with CLRF-ed content
normalizedContent := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
parts := bytes.Split(normalizedContent, []byte("\n---\n"))
var finalState *state.HelmState
for i, part := range parts {
var yamlBuf *bytes.Buffer
var err error
id := fmt.Sprintf("%s.part.%d", filename, i)
if env == nil && overrodeEnv == nil {
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
} else {
yamlBuf, err = ld.renderTemplatesToYamlWithEnv(baseDir, id, part, env, overrodeEnv)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
}
currentState, err := ld.load(
yamlBuf.Bytes(),
baseDir,
filename,
evaluateBases,
env,
overrodeEnv,
)
if err != nil {
return nil, err
}
for i, r := range currentState.Releases {
if r.Chart == "" {
return nil, fmt.Errorf("error during %s parsing: encountered empty chart while reading release %q at index %d", id, r.Name, i)
}
}
if finalState == nil {
finalState = currentState
} else {
if err := mergo.Merge(&finalState.ReleaseSetSpec, &currentState.ReleaseSetSpec, mergo.WithOverride); err != nil {
return nil, err
}
finalState.RenderedValues = currentState.RenderedValues
}
env = &finalState.Env
ld.logger.Debugf("merged environment: %v", env)
}
return finalState, nil
}
+765
View File
@@ -0,0 +1,765 @@
package app
import (
"bufio"
"bytes"
"io"
"path/filepath"
"sync"
"testing"
"github.com/roboll/helmfile/pkg/exectest"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"github.com/variantdev/vals"
)
func TestDestroy_2(t *testing.T) {
type testcase struct {
helm3 bool
ns string
concurrency int
error string
files map[string]string
selectors []string
lists map[exectest.ListKey]string
diffs map[exectest.DiffKey]error
upgraded []exectest.Release
deleted []exectest.Release
log string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted
var helm = &exectest.Helm{
Helm3: tc.helm3,
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
Lists: tc.lists,
Diffs: tc.diffs,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := &bytes.Buffer{}
func() {
t.Helper()
logReader, logWriter := io.Pipe()
logFlushed := &sync.WaitGroup{}
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
logFlushed.Add(1)
go func() {
scanner := bufio.NewScanner(logReader)
for scanner.Scan() {
bs.Write(scanner.Bytes())
bs.WriteString("\n")
}
logFlushed.Done()
}()
defer func() {
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
if err := logWriter.Close(); err != nil {
panic(err)
}
logFlushed.Wait()
}()
logger := helmexec.NewLogger(logWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", ""): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
if tc.ns != "" {
app.Namespace = tc.ns
}
if tc.selectors != nil {
app.Selectors = tc.selectors
}
destroyErr := app.Destroy(destroyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
logger: logger,
})
if tc.error == "" && destroyErr != nil {
t.Fatalf("unexpected error: %v", destroyErr)
} else if tc.error != "" && destroyErr == nil {
t.Fatal("expected error did not occur")
} else if tc.error != "" && destroyErr != nil && tc.error != destroyErr.Error() {
t.Fatalf("invalid error: expected %q, got %q", tc.error, destroyErr.Error())
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
for relIdx := range wantUpgrades {
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
if len(wantDeletes) > len(helm.Deleted) {
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
}
for relIdx := range wantDeletes {
if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
}
for flagIdx := range wantDeletes[relIdx].Flags {
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
}
}
}
}()
if tc.log != "" {
actual := bs.String()
diff, exists := testhelper.Diff(tc.log, actual, 3)
if exists {
t.Errorf("unexpected log:\nDIFF\n%s\nEOD", diff)
}
}
}
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: database
chart: charts/mysql
needs:
- logging
- name: frontend-v1
chart: charts/frontend
installed: false
needs:
- servicemesh
- logging
- backend-v1
- name: frontend-v2
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: frontend-v3
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: backend-v1
chart: charts/backend
installed: false
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: backend-v2
chart: charts/backend
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: anotherbackend
chart: charts/anotherbackend
needs:
- servicemesh
- logging
- database
- name: servicemesh
chart: charts/istio
needs:
- logging
- name: logging
chart: charts/fluent-bit
- name: front-proxy
chart: stable/envoy
`,
}
filesForTwoReleases := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: backend-v1
chart: charts/backend
installed: false
- name: frontend-v1
chart: charts/frontend
needs:
- backend-v1
`,
}
t.Run("smoke", func(t *testing.T) {
//
// complex test cases for smoke testing
//
check(t, testcase{
files: files,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^frontend-v2$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^frontend-v3$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v3 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v2$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
backend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^logging$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
logging 4 Fri Nov 1 08:40:07 2019 DEPLOYED fluent-bit-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^front-proxy$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
front-proxy 4 Fri Nov 1 08:40:07 2019 DEPLOYED envoy-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^servicemesh$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
servicemesh 4 Fri Nov 1 08:40:07 2019 DEPLOYED istio-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^database$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
database 4 Fri Nov 1 08:40:07 2019 DEPLOYED mysql-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^anotherbackend$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0 3.1.0 default
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v3", Flags: []string{}},
{Name: "frontend-v2", Flags: []string{}},
{Name: "frontend-v1", Flags: []string{}},
{Name: "backend-v2", Flags: []string{}},
{Name: "backend-v1", Flags: []string{}},
{Name: "anotherbackend", Flags: []string{}},
{Name: "servicemesh", Flags: []string{}},
{Name: "database", Flags: []string{}},
{Name: "front-proxy", Flags: []string{}},
{Name: "logging", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
merged environment: &{default map[] map[]}
10 release(s) found in helmfile.yaml
processing 5 groups of releases in this order:
GROUP RELEASES
1 frontend-v3, frontend-v2, frontend-v1
2 backend-v2, backend-v1
3 anotherbackend
4 servicemesh, database
5 front-proxy, logging
processing releases in group 1/5: frontend-v3, frontend-v2, frontend-v1
release "frontend-v3" processed
release "frontend-v2" processed
release "frontend-v1" processed
processing releases in group 2/5: backend-v2, backend-v1
release "backend-v2" processed
release "backend-v1" processed
processing releases in group 3/5: anotherbackend
release "anotherbackend" processed
processing releases in group 4/5: servicemesh, database
release "servicemesh" processed
release "database" processed
processing releases in group 5/5: front-proxy, logging
release "front-proxy" processed
release "logging" processed
DELETED RELEASES:
NAME
frontend-v3
frontend-v2
frontend-v1
backend-v2
backend-v1
anotherbackend
servicemesh
database
front-proxy
logging
`,
})
})
t.Run("destroy only one release with selector", func(t *testing.T) {
check(t, testcase{
files: files,
selectors: []string{"name=logging"},
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^frontend-v2$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^frontend-v3$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v3 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v2$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
backend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^logging$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
logging 4 Fri Nov 1 08:40:07 2019 DEPLOYED fluent-bit-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^front-proxy$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
front-proxy 4 Fri Nov 1 08:40:07 2019 DEPLOYED envoy-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^servicemesh$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
servicemesh 4 Fri Nov 1 08:40:07 2019 DEPLOYED istio-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^database$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
database 4 Fri Nov 1 08:40:07 2019 DEPLOYED mysql-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^anotherbackend$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0 3.1.0 default
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "logging", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
merged environment: &{default map[] map[]}
1 release(s) matching name=logging found in helmfile.yaml
processing 1 groups of releases in this order:
GROUP RELEASES
1 logging
processing releases in group 1/1: logging
release "logging" processed
DELETED RELEASES:
NAME
logging
`,
})
})
t.Run("destroy installed but disabled release", func(t *testing.T) {
check(t, testcase{
files: filesForTwoReleases,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v1", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
processing 2 groups of releases in this order:
GROUP RELEASES
1 frontend-v1
2 backend-v1
processing releases in group 1/2: frontend-v1
release "frontend-v1" processed
processing releases in group 2/2: backend-v1
release "backend-v1" processed
DELETED RELEASES:
NAME
frontend-v1
backend-v1
`,
})
})
t.Run("helm3", func(t *testing.T) {
check(t, testcase{
helm3: true,
files: filesForTwoReleases,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV3ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV3ListFlagsWithoutKubeContext}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v1", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
processing 2 groups of releases in this order:
GROUP RELEASES
1 frontend-v1
2 backend-v1
processing releases in group 1/2: frontend-v1
release "frontend-v1" processed
processing releases in group 2/2: backend-v1
release "backend-v1" processed
DELETED RELEASES:
NAME
frontend-v1
backend-v1
`,
})
})
}
+801
View File
@@ -0,0 +1,801 @@
package app
import (
"bufio"
"bytes"
"io"
"path/filepath"
"sync"
"testing"
"github.com/roboll/helmfile/pkg/exectest"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"github.com/variantdev/vals"
"go.uber.org/zap"
)
const (
helmV2ListFlags = "--kube-contextdefault--deleting--deployed--failed--pending"
helmV2ListFlagsWithoutKubeContext = "--deleting--deployed--failed--pending"
helmV3ListFlags = "--kube-contextdefault--uninstalling--deployed--failed--pending"
helmV3ListFlagsWithoutKubeContext = "--uninstalling--deployed--failed--pending"
)
type destroyConfig struct {
args string
concurrency int
interactive bool
skipDeps bool
logger *zap.SugaredLogger
}
func (d destroyConfig) Args() string {
return d.args
}
func (d destroyConfig) Interactive() bool {
return d.interactive
}
func (d destroyConfig) Logger() *zap.SugaredLogger {
return d.logger
}
func (d destroyConfig) Concurrency() int {
return d.concurrency
}
func (d destroyConfig) SkipDeps() bool {
return d.skipDeps
}
func TestDestroy(t *testing.T) {
type testcase struct {
helm3 bool
ns string
concurrency int
error string
files map[string]string
selectors []string
lists map[exectest.ListKey]string
diffs map[exectest.DiffKey]error
upgraded []exectest.Release
deleted []exectest.Release
log string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted
var helm = &exectest.Helm{
Helm3: tc.helm3,
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
Lists: tc.lists,
Diffs: tc.diffs,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := &bytes.Buffer{}
func() {
t.Helper()
logReader, logWriter := io.Pipe()
logFlushed := &sync.WaitGroup{}
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
logFlushed.Add(1)
go func() {
scanner := bufio.NewScanner(logReader)
for scanner.Scan() {
bs.Write(scanner.Bytes())
bs.WriteString("\n")
}
logFlushed.Done()
}()
defer func() {
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
if err := logWriter.Close(); err != nil {
panic(err)
}
logFlushed.Wait()
}()
logger := helmexec.NewLogger(logWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
if tc.ns != "" {
app.Namespace = tc.ns
}
if tc.selectors != nil {
app.Selectors = tc.selectors
}
destroyErr := app.Destroy(destroyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
logger: logger,
})
if tc.error == "" && destroyErr != nil {
t.Fatalf("unexpected error: %v", destroyErr)
} else if tc.error != "" && destroyErr == nil {
t.Fatal("expected error did not occur")
} else if tc.error != "" && destroyErr != nil && tc.error != destroyErr.Error() {
t.Fatalf("invalid error: expected %q, got %q", tc.error, destroyErr.Error())
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
for relIdx := range wantUpgrades {
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
if len(wantDeletes) > len(helm.Deleted) {
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
}
for relIdx := range wantDeletes {
if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
}
for flagIdx := range wantDeletes[relIdx].Flags {
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
}
}
}
}()
if tc.log != "" {
actual := bs.String()
diff, exists := testhelper.Diff(tc.log, actual, 3)
if exists {
t.Errorf("unexpected log:\nDIFF\n%s\nEOD", diff)
}
}
}
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: database
chart: charts/mysql
needs:
- logging
- name: frontend-v1
chart: charts/frontend
installed: false
needs:
- servicemesh
- logging
- backend-v1
- name: frontend-v2
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: frontend-v3
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: backend-v1
chart: charts/backend
installed: false
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: backend-v2
chart: charts/backend
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: anotherbackend
chart: charts/anotherbackend
needs:
- servicemesh
- logging
- database
- name: servicemesh
chart: charts/istio
needs:
- logging
- name: logging
chart: charts/fluent-bit
- name: front-proxy
chart: stable/envoy
`,
}
filesForTwoReleases := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: backend-v1
chart: charts/backend
installed: false
- name: frontend-v1
chart: charts/frontend
needs:
- backend-v1
`,
}
t.Run("smoke", func(t *testing.T) {
//
// complex test cases for smoke testing
//
check(t, testcase{
files: files,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^frontend-v2$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^frontend-v3$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v3 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v2$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
backend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^logging$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
logging 4 Fri Nov 1 08:40:07 2019 DEPLOYED fluent-bit-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^front-proxy$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
front-proxy 4 Fri Nov 1 08:40:07 2019 DEPLOYED envoy-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^servicemesh$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
servicemesh 4 Fri Nov 1 08:40:07 2019 DEPLOYED istio-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^database$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
database 4 Fri Nov 1 08:40:07 2019 DEPLOYED mysql-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^anotherbackend$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0 3.1.0 default
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v3", Flags: []string{}},
{Name: "frontend-v2", Flags: []string{}},
{Name: "frontend-v1", Flags: []string{}},
{Name: "backend-v2", Flags: []string{}},
{Name: "backend-v1", Flags: []string{}},
{Name: "anotherbackend", Flags: []string{}},
{Name: "servicemesh", Flags: []string{}},
{Name: "database", Flags: []string{}},
{Name: "front-proxy", Flags: []string{}},
{Name: "logging", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
merged environment: &{default map[] map[]}
10 release(s) found in helmfile.yaml
processing 5 groups of releases in this order:
GROUP RELEASES
1 default//frontend-v3, default//frontend-v2, default//frontend-v1
2 default//backend-v2, default//backend-v1
3 default//anotherbackend
4 default//servicemesh, default//database
5 default//front-proxy, default//logging
processing releases in group 1/5: default//frontend-v3, default//frontend-v2, default//frontend-v1
release "frontend-v3" processed
release "frontend-v2" processed
release "frontend-v1" processed
processing releases in group 2/5: default//backend-v2, default//backend-v1
release "backend-v2" processed
release "backend-v1" processed
processing releases in group 3/5: default//anotherbackend
release "anotherbackend" processed
processing releases in group 4/5: default//servicemesh, default//database
release "servicemesh" processed
release "database" processed
processing releases in group 5/5: default//front-proxy, default//logging
release "front-proxy" processed
release "logging" processed
DELETED RELEASES:
NAME
frontend-v3
frontend-v2
frontend-v1
backend-v2
backend-v1
anotherbackend
servicemesh
database
front-proxy
logging
`,
})
})
t.Run("destroy only one release with selector", func(t *testing.T) {
check(t, testcase{
files: files,
selectors: []string{"name=logging"},
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^frontend-v2$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^frontend-v3$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v3 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v2$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
backend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^logging$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
logging 4 Fri Nov 1 08:40:07 2019 DEPLOYED fluent-bit-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^front-proxy$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
front-proxy 4 Fri Nov 1 08:40:07 2019 DEPLOYED envoy-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^servicemesh$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
servicemesh 4 Fri Nov 1 08:40:07 2019 DEPLOYED istio-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^database$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
database 4 Fri Nov 1 08:40:07 2019 DEPLOYED mysql-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^anotherbackend$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0 3.1.0 default
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "logging", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
merged environment: &{default map[] map[]}
1 release(s) matching name=logging found in helmfile.yaml
processing 1 groups of releases in this order:
GROUP RELEASES
1 default//logging
processing releases in group 1/1: default//logging
release "logging" processed
DELETED RELEASES:
NAME
logging
`,
})
})
t.Run("destroy installed but disabled release", func(t *testing.T) {
check(t, testcase{
files: filesForTwoReleases,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v1", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
processing 2 groups of releases in this order:
GROUP RELEASES
1 default//frontend-v1
2 default//backend-v1
processing releases in group 1/2: default//frontend-v1
release "frontend-v1" processed
processing releases in group 2/2: default//backend-v1
release "backend-v1" processed
DELETED RELEASES:
NAME
frontend-v1
backend-v1
`,
})
})
t.Run("helm3", func(t *testing.T) {
check(t, testcase{
helm3: true,
files: filesForTwoReleases,
diffs: map[exectest.DiffKey]error{},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV3ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV3ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{},
deleted: []exectest.Release{
{Name: "frontend-v1", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: backend-v1
3: chart: charts/backend
4: installed: false
5: - name: frontend-v1
6: chart: charts/frontend
7: needs:
8: - backend-v1
9:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
processing 2 groups of releases in this order:
GROUP RELEASES
1 default//frontend-v1
2 default//backend-v1
processing releases in group 1/2: default//frontend-v1
release "frontend-v1" processed
processing releases in group 2/2: default//backend-v1
release "backend-v1" processed
DELETED RELEASES:
NAME
frontend-v1
backend-v1
`,
})
})
}
File diff suppressed because it is too large Load Diff
+1502
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
package app
import (
"encoding/json"
"fmt"
"github.com/gosuri/uitable"
)
func FormatAsTable(releases []*HelmRelease) error {
table := uitable.New()
table.AddRow("NAME", "NAMESPACE", "ENABLED", "LABELS", "CHART", "VERSION")
for _, r := range releases {
table.AddRow(r.Name, r.Namespace, fmt.Sprintf("%t", r.Enabled), r.Labels, r.Chart, r.Version)
}
fmt.Println(table.String())
return nil
}
func FormatAsJson(releases []*HelmRelease) error {
output, err := json.Marshal(releases)
if err != nil {
return fmt.Errorf("error generating json: %v", err)
}
fmt.Println(string(output))
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package app
import (
"github.com/roboll/helmfile/pkg/state"
"gopkg.in/yaml.v2"
)
type LoadOpts struct {
Selectors []string
Environment state.SubhelmfileEnvironmentSpec
RetainValuesFiles bool
// CalleePath is the absolute path to the file being loaded
CalleePath string
Reverse bool
Filter bool
}
func (o LoadOpts) DeepCopy() LoadOpts {
bytes, err := yaml.Marshal(o)
if err != nil {
panic(err)
}
new := LoadOpts{}
if err := yaml.Unmarshal(bytes, &new); err != nil {
panic(err)
}
return new
}
+114
View File
@@ -0,0 +1,114 @@
package app
import "github.com/roboll/helmfile/pkg/helmexec"
type noCallHelmExec struct {
}
type versionOnlyHelmExec struct {
*noCallHelmExec
isHelm3 bool
}
func (helm *versionOnlyHelmExec) IsHelm3() bool {
return helm.isHelm3
}
func (helm *noCallHelmExec) doPanic() {
panic("unexpected call to helm")
}
func (helm *noCallHelmExec) TemplateRelease(name, chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) ChartPull(chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) ChartExport(chart string, path string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) UpdateDeps(chart string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) BuildDeps(name, chart string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) SetExtraArgs(args ...string) {
helm.doPanic()
return
}
func (helm *noCallHelmExec) SetHelmBinary(bin string) {
helm.doPanic()
return
}
func (helm *noCallHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) UpdateRepo() error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) RegistryLogin(name string, username string, password string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
helm.doPanic()
return "", nil
}
func (helm *noCallHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
helm.doPanic()
return "", nil
}
func (helm *noCallHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) Fetch(chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) Lint(name, chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) IsHelm3() bool {
helm.doPanic()
return false
}
func (helm *noCallHelmExec) GetVersion() helmexec.Version {
helm.doPanic()
return helmexec.Version{}
}
func (helm *noCallHelmExec) IsVersionAtLeast(versionStr string) bool {
helm.doPanic()
return false
}
+198
View File
@@ -0,0 +1,198 @@
package app
import (
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/roboll/helmfile/pkg/argparser"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
)
type Run struct {
state *state.HelmState
helm helmexec.Interface
ctx Context
ReleaseToChart map[state.PrepareChartKey]string
Ask func(string) bool
}
func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run {
if helm == nil {
panic("Assertion failed: helmexec.Interface must not be nil")
}
return &Run{state: st, helm: helm, ctx: ctx}
}
func (r *Run) askForConfirmation(msg string) bool {
if r.Ask != nil {
return r.Ask(msg)
}
return AskForConfirmation(msg)
}
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
if r.ReleaseToChart != nil {
panic("Run.PrepareCharts can be called only once")
}
if !opts.SkipRepos {
ctx := r.ctx
if err := ctx.SyncReposOnce(r.state, r.helm); err != nil {
return err
}
}
// Create tmp directory and bail immediately if it fails
var dir string
if len(opts.OutputDir) == 0 {
tempDir, err := ioutil.TempDir("", "helmfile*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
dir = tempDir
} else {
dir = opts.OutputDir
fmt.Printf("Charts will be downloaded to: %s\n", dir)
}
if _, err := r.state.TriggerGlobalPrepareEvent(helmfileCommand); err != nil {
return err
}
releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, opts)
if len(errs) > 0 {
return fmt.Errorf("%v", errs)
}
for i := range r.state.Releases {
rel := &r.state.Releases[i]
key := state.PrepareChartKey{
Name: rel.Name,
Namespace: rel.Namespace,
KubeContext: rel.KubeContext,
}
if chart := releaseToChart[key]; chart != "" {
rel.Chart = chart
}
}
r.ReleaseToChart = releaseToChart
f()
_, err := r.state.TriggerGlobalCleanupEvent(helmfileCommand)
return err
}
func (r *Run) Deps(c DepsConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.UpdateDeps(r.helm)
}
func (r *Run) Repos(c ReposConfigProvider) error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.ctx.SyncReposOnce(r.state, r.helm)
}
func (r *Run) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) []error {
st := r.state
helm := r.helm
affectedReleases := state.AffectedReleases{}
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfigProvider, diffOpts *state.DiffOpts) (*string, map[string]state.ReleaseSpec, map[string]state.ReleaseSpec, []error) {
st := r.state
helm := r.helm
var changedReleases []state.ReleaseSpec
var deletingReleases []state.ReleaseSpec
var planningErrs []error
// TODO Better way to detect diff on only filtered releases
{
changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.ShowSecrets(), c.SuppressDiff(), triggerCleanupEvent, diffOpts)
var err error
deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases)
if err != nil {
planningErrs = append(planningErrs, err)
}
}
fatalErrs := []error{}
for _, e := range planningErrs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
fatalErrs = append(fatalErrs, e)
}
default:
fatalErrs = append(fatalErrs, e)
}
}
if len(fatalErrs) > 0 {
return nil, nil, nil, fatalErrs
}
releasesToBeDeleted := map[string]state.ReleaseSpec{}
for _, r := range deletingReleases {
id := state.ReleaseToID(&r)
releasesToBeDeleted[id] = r
}
releasesToBeUpdated := map[string]state.ReleaseSpec{}
for _, r := range changedReleases {
id := state.ReleaseToID(&r)
// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
if _, ok := releasesToBeDeleted[id]; !ok {
releasesToBeUpdated[id] = r
}
}
// sync only when there are changes
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
var msg *string
if c.DetailedExitcode() {
// TODO better way to get the logger
m := "No affected releases"
msg = &m
}
return msg, nil, nil, nil
}
names := []string{}
for _, r := range releasesToBeUpdated {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
// Make the output deterministic for testing purpose
sort.Strings(names)
infoMsg := fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil
}
+65
View File
@@ -0,0 +1,65 @@
package app
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func assertEqualsToSnapshot(t *testing.T, name string, data string) {
type thisPkgLocator struct{}
t.Helper()
snapshotFileName := snapshotFileName(t, name)
if os.Getenv("HELMFILE_UPDATE_SNAPSHOT") != "" {
update(t, snapshotFileName, []byte(data))
return
}
wantData, err := ioutil.ReadFile(snapshotFileName)
if err != nil {
t.Fatalf(
"Snapshot file %q does not exist. Rerun this test with `HELMFILE_UPDATE_SNAPSHOT=1 go test -v -run %s %s` to create the snapshot",
snapshotFileName,
t.Name(),
reflect.TypeOf(thisPkgLocator{}).PkgPath(),
)
}
want := string(wantData)
if d := cmp.Diff(want, data); d != "" {
t.Errorf("unexpected %s: want (-), got (+): %s", name, d)
t.Errorf(
"If you think this is due to the snapshot file being outdated, rerun this test with `HELMFILE_UPDATE_SNAPSHOT=1 go test -v -run %s %s` to update the snapshot",
t.Name(),
reflect.TypeOf(thisPkgLocator{}).PkgPath(),
)
}
}
func update(t *testing.T, snapshotFileName string, data []byte) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(snapshotFileName), 0755); err != nil {
t.Fatalf("%v", err)
}
if err := ioutil.WriteFile(snapshotFileName, data, 0644); err != nil {
t.Fatalf("%v", err)
}
}
func snapshotFileName(t *testing.T, name string) string {
dir := filepath.Join(strings.Split(strings.ToLower(t.Name()), "/")...)
return filepath.Join("testdata", dir, name)
}
@@ -0,0 +1,65 @@
processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: foo
3: chart: incubator/raw
4: namespace: default
5: labels:
6: app: test
7: component: raw
8: index: '1'
9:
10: - name: foo
11: chart: incubator/raw
12: namespace: default
13: labels:
14: app: test
15: component: raw
16: index: '2'
17:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: foo
3: chart: incubator/raw
4: namespace: default
5: labels:
6: app: test
7: component: raw
8: index: '1'
9:
10: - name: foo
11: chart: incubator/raw
12: namespace: default
13: labels:
14: app: test
15: component: raw
16: index: '2'
17:
merged environment: &{default map[] map[]}
1 release(s) matching index=1 found in helmfile.yaml
Affected releases are:
foo (incubator/raw) UPDATED
processing 1 groups of releases in this order:
GROUP RELEASES
1 default/default/foo
processing releases in group 1/1: default/default/foo
getting deployed release version failed:unexpected list key: {^foo$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
foo incubator/raw
+101 -34
View File
@@ -3,12 +3,12 @@ package app
import (
"bytes"
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"path/filepath"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/tmpl"
)
func prependLineNumbers(text string) string {
@@ -20,61 +20,128 @@ func prependLineNumbers(text string) string {
return buf.String()
}
type twoPassRenderer struct {
reader func(string) ([]byte, error)
env string
namespace string
filename string
logger *zap.SugaredLogger
abs func(string) (string, error)
}
func (r *twoPassRenderer) renderEnvironment(content []byte) environment.Environment {
firstPassEnv := environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
firstPassRenderer := tmpl.NewFirstPassRenderer(filepath.Dir(r.filename), tmplData)
func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
tmplData := state.EnvironmentTemplateData{
Environment: *firstPassEnv,
Namespace: r.namespace,
Values: map[string]interface{}{},
}
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
// parse as much as we can, tolerate errors, this is a preparse
yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil && r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
r.logger.Debugf("template syntax error: %v", err)
if yamlBuf == nil { // we have a template syntax error, let the second parse report
r.logger.Debugf("template syntax error: %v", err)
return firstPassEnv
return firstPassEnv, nil
}
}
c := state.NewCreator(r.logger, r.reader, r.abs)
yamlData := yamlBuf.String()
if r.logger != nil {
r.logger.Debugf("first-pass rendering output of \"%s\":\n%s", filename, prependLineNumbers(yamlData))
}
// Work-around for https://github.com/golang/go/issues/24963
sanitized := strings.ReplaceAll(yamlData, "<no value>", "")
if len(yamlData) != len(sanitized) {
msg := "replaced <no value>s to workaround https://github.com/golang/go/issues/24963 to address https://github.com/roboll/helmfile/issues/553:\n%s"
r.logger.Debugf(msg, cmp.Diff(yamlData, sanitized))
}
c := r.underlying()
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.CreateFromYaml(yamlBuf.Bytes(), r.filename, r.env)
prestate, err := c.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
r.logger.Debugf("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
}
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
if prestate != nil {
firstPassEnv = prestate.Env
firstPassEnv = &prestate.Env
}
return firstPassEnv
return firstPassEnv, prestate
}
func (r *twoPassRenderer) renderTemplate(content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
firstPassEnv := r.renderEnvironment(content)
type RenderOpts struct {
}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
secondPassRenderer := tmpl.NewFileRenderer(r.reader, filepath.Dir(r.filename), tmplData)
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte) (*bytes.Buffer, error) {
env := &environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
return r.renderTemplatesToYamlWithEnv(baseDir, filename, content, env, nil)
}
func (r *desiredStateLoader) renderTemplatesToYamlWithEnv(baseDir, filename string, content []byte, inherited, overrode *environment.Environment) (*bytes.Buffer, error) {
return r.twoPassRenderTemplateToYaml(inherited, overrode, baseDir, filename, content)
}
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
if r.logger != nil {
r.logger.Debugf("first-pass rendering starting for \"%s\": inherited=%v, overrode=%v", filename, inherited, overrode)
}
initEnv, err := inherited.Merge(overrode)
if err != nil {
return nil, err
}
if r.logger != nil {
r.logger.Debugf("first-pass uses: %v", initEnv)
}
renderedEnv, prestate := r.renderPrestate(initEnv, baseDir, filename, content)
if r.logger != nil {
r.logger.Debugf("first-pass produced: %v", renderedEnv)
}
finalEnv, err := inherited.Merge(renderedEnv)
if err != nil {
return nil, err
}
finalEnv, err = finalEnv.Merge(overrode)
if err != nil {
return nil, err
}
if r.logger != nil {
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, *finalEnv)
}
vals, err := finalEnv.GetMergedValues()
if err != nil {
return nil, err
}
if prestate != nil {
prestate.Env = *finalEnv
r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
}
tmplData := state.EnvironmentTemplateData{
Environment: *finalEnv,
Namespace: r.namespace,
Values: vals,
}
secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil {
if r.logger != nil {
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", r.filename, prependLineNumbers(string(content)))
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", r.filename, prependLineNumbers(yamlBuf.String()))
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}
+50 -68
View File
@@ -1,26 +1,32 @@
package app
import (
"fmt"
"github.com/roboll/helmfile/pkg/remote"
"os"
"path/filepath"
"strings"
"testing"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/testhelper"
"gopkg.in/yaml.v2"
)
func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer {
return &twoPassRenderer{
reader: readFile,
env: env,
namespace: "namespace",
filename: "",
logger: helmexec.NewLogger(os.Stdout, "debug"),
abs: filepath.Abs,
}
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
testfs := testhelper.NewTestFs(files)
logger := helmexec.NewLogger(os.Stdout, "debug")
r := remote.NewRemote(logger, testfs.Cwd, testfs.ReadFile, testfs.DirectoryExistsAt, testfs.FileExistsAt)
return &desiredStateLoader{
env: env,
namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"),
readFile: testfs.ReadFile,
fileExists: testfs.FileExists,
abs: testfs.Abs,
glob: testfs.Glob,
remote: r,
}, testfs
}
func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
@@ -37,30 +43,21 @@ releases:
chart: mychart1
`)
fileReaderCalls := 0
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
expectedFilename := filepath.Clean("default/values.yaml")
if !strings.HasSuffix(filename, expectedFilename) {
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
}
fileReaderCalls++
if fileReaderCalls == 2 {
return []byte("SecondPass"), nil
}
return []byte(""), nil
files := map[string]string{
"/path/to/default/values.yaml": ``,
"/path/to/other/default/values.yaml": `SecondPass`,
}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
r, testfs := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if fileReaderCalls > 2 {
if testfs.FileReaderCalls() > 2 {
t.Error("reader should be called only twice")
}
@@ -71,10 +68,10 @@ releases:
func TestReadFromYaml_RenderTemplate(t *testing.T) {
defaultValuesYaml := []byte(`
defaultValuesYaml := `
releaseName: "hello"
conditionalReleaseTag: "yes"
`)
`
yamlContent := []byte(`
environments:
@@ -93,18 +90,13 @@ releases:
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
expectedFilename := filepath.Clean("default/values.yaml")
if !strings.HasSuffix(filename, expectedFilename) {
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
}
return defaultValuesYaml, nil
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r := makeRenderer(fileReader, "staging")
r, _ := makeLoader(files, "staging")
// test the double rendering
yamlBuf, err := r.renderTemplate(yamlContent)
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -130,7 +122,7 @@ releases:
}
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
defaultValuesYaml := []byte("")
defaultValuesYaml := ``
yamlContent := []byte(`
environments:
@@ -146,14 +138,13 @@ releases:
{{ end }}
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r := makeRenderer(fileReader, "staging")
r, _ := makeLoader(files, "staging")
// test the double rendering
_, err := r.renderTemplate(yamlContent)
_, err := r.renderTemplatesToYaml("", "", yamlContent)
if !strings.Contains(err.Error(), "stringTemplate:8") {
t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err)
@@ -165,9 +156,9 @@ releases:
// This does not apply to .gotmpl files, which is a nice side-effect.
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
defaultValuesYamlGotmpl := []byte(`
defaultValuesYamlGotmpl := `
releaseName: {{ readFile "nonIgnoredFile" }}
`)
`
yamlContent := []byte(`
environments:
@@ -183,15 +174,13 @@ releases:
{{ end }}
`)
fileReader := func(filename string) ([]byte, error) {
if strings.HasSuffix(filename, "nonIgnoredFile") {
return []byte("release-a"), nil
}
return defaultValuesYamlGotmpl, nil
files := map[string]string{
"/path/to/nonIgnoredFile": `release-a`,
"/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl,
}
r := makeRenderer(fileReader, "staging")
rendered, _ := r.renderTemplate(yamlContent)
r, _ := makeLoader(files, "staging")
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
var state state.HelmState
yaml.Unmarshal(rendered.Bytes(), &state)
@@ -206,19 +195,15 @@ releases:
}
func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
defaultValuesYaml := []byte(``)
yamlContent := []byte(`releases:
- name: {{ .Namespace }}-myrelease
chart: mychart
`)
// make a reader that returns a simulated context
fileReader := func(filename string) ([]byte, error) {
return defaultValuesYaml, nil
}
files := map[string]string{}
r := makeRenderer(fileReader, "staging")
yamlBuf, err := r.renderTemplate(yamlContent)
r, _ := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -231,7 +216,7 @@ func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
}
}
func TestReadFromYaml_HelfileShouldBeResilentToTemplateErrors(t *testing.T) {
func TestReadFromYaml_HelmfileShouldBeResilentToTemplateErrors(t *testing.T) {
yamlContent := []byte(`environments:
staging:
production:
@@ -244,12 +229,9 @@ releases:
{{ end }}
chart: mychart
`)
fileReader := func(filename string) ([]byte, error) {
return yamlContent, nil
}
r := makeRenderer(fileReader, "staging")
_, err := r.renderTemplate(yamlContent)
r, _ := makeLoader(map[string]string{}, "staging")
_, err := r.renderTemplatesToYaml("", "", yamlContent)
if err == nil {
t.Fatalf("wanted error, none returned")
}
+3
View File
@@ -0,0 +1,3 @@
package version
var Version string
+2 -7
View File
@@ -1,10 +1,9 @@
package args
package argparser
import (
"fmt"
"github.com/roboll/helmfile/pkg/state"
"strings"
"github.com/roboll/helmfile/state"
)
type keyVal struct {
@@ -83,10 +82,6 @@ func GetArgs(args string, state *state.HelmState) []string {
}
}
if state.HelmDefaults.KubeContext != "" {
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
}
var argArr []string
for _, flag := range argsMap.flags {
@@ -1,20 +1,23 @@
package args
package argparser
import (
"github.com/roboll/helmfile/pkg/state"
"strings"
"testing"
"github.com/roboll/helmfile/state"
)
func TestGetArgs(t *testing.T) {
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns"
defaultArgs := []string{"--recreate-pods", "--force"}
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
testState := &state.HelmState{HelmDefaults: Helmdefaults}
testState := &state.HelmState{
ReleaseSetSpec: state.ReleaseSetSpec{
HelmDefaults: Helmdefaults,
},
}
receivedArgs := GetArgs(args, testState)
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force --kube-context=test"
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force"
if compareArgs(expectedOutput, receivedArgs) == false {
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
@@ -25,10 +28,14 @@ func Test2(t *testing.T) {
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns"
defaultArgs := []string{"--recreate-pods", "--force"}
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
testState := &state.HelmState{HelmDefaults: Helmdefaults}
testState := &state.HelmState{
ReleaseSetSpec: state.ReleaseSetSpec{
HelmDefaults: Helmdefaults,
},
}
receivedArgs := GetArgs(args, testState)
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force --kube-context=test"
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force"
if compareArgs(expectedOutput, receivedArgs) == false {
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
+85
View File
@@ -0,0 +1,85 @@
package environment
import (
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/maputil"
"gopkg.in/yaml.v2"
)
type Environment struct {
Name string
Values map[string]interface{}
Defaults map[string]interface{}
}
var EmptyEnvironment Environment
func (e Environment) DeepCopy() Environment {
valuesBytes, err := yaml.Marshal(e.Values)
if err != nil {
panic(err)
}
var values map[string]interface{}
if err := yaml.Unmarshal(valuesBytes, &values); err != nil {
panic(err)
}
values, err = maputil.CastKeysToStrings(values)
if err != nil {
panic(err)
}
defaultsBytes, err := yaml.Marshal(e.Defaults)
if err != nil {
panic(err)
}
var defaults map[string]interface{}
if err := yaml.Unmarshal(defaultsBytes, &defaults); err != nil {
panic(err)
}
defaults, err = maputil.CastKeysToStrings(defaults)
if err != nil {
panic(err)
}
return Environment{
Name: e.Name,
Values: values,
Defaults: defaults,
}
}
func (e *Environment) Merge(other *Environment) (*Environment, error) {
if e == nil {
if other != nil {
copy := other.DeepCopy()
return &copy, nil
}
return nil, nil
}
copy := e.DeepCopy()
if other != nil {
if err := mergo.Merge(&copy, other, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, err
}
}
return &copy, nil
}
func (e *Environment) GetMergedValues() (map[string]interface{}, error) {
vals := map[string]interface{}{}
if err := mergo.Merge(&vals, e.Defaults, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, err
}
if err := mergo.Merge(&vals, e.Values, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, err
}
vals, err := maputil.CastKeysToStrings(vals)
if err != nil {
return nil, err
}
return vals, nil
}
+98
View File
@@ -0,0 +1,98 @@
package environment
import (
"github.com/google/go-cmp/cmp"
"testing"
)
// See https://github.com/roboll/helmfile/issues/1150
func TestMerge_OverwriteNilValue_Issue1150(t *testing.T) {
dst := &Environment{
Name: "dst",
Values: map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": nil,
},
},
Defaults: nil,
}
src := &Environment{
Name: "src",
Values: map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
},
},
Defaults: nil,
}
merged, err := dst.Merge(src)
if err != nil {
t.Fatal(err)
}
actual := merged.Values
expected := map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
// See https://github.com/roboll/helmfile/issues/1154
func TestMerge_OverwriteWithNilValue_Issue1154(t *testing.T) {
dst := &Environment{
Name: "dst",
Values: map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.0",
},
},
},
Defaults: nil,
}
src := &Environment{
Name: "src",
Values: map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
"prometheus": nil,
},
},
Defaults: nil,
}
merged, err := dst.Merge(src)
if err != nil {
t.Fatal(err)
}
actual := merged.Values
expected := map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
"prometheus": nil,
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
+134
View File
@@ -0,0 +1,134 @@
package event
import (
"fmt"
"strings"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/tmpl"
"go.uber.org/zap"
)
type Hook struct {
Name string `yaml:"name"`
Events []string `yaml:"events"`
Command string `yaml:"command"`
Kubectl map[string]string `yaml:"kubectlApply,omitempty"`
Args []string `yaml:"args"`
ShowLogs bool `yaml:"showlogs"`
}
type event struct {
Name string
Error error
}
type Bus struct {
Runner helmexec.Runner
Hooks []Hook
BasePath string
StateFilePath string
Namespace string
Chart string
Env environment.Environment
ReadFile func(string) ([]byte, error)
Logger *zap.SugaredLogger
}
func (bus *Bus) Trigger(evt string, evtErr error, context map[string]interface{}) (bool, error) {
if bus.Runner == nil {
bus.Runner = helmexec.ShellRunner{
Dir: bus.BasePath,
Logger: bus.Logger,
}
}
executed := false
for _, hook := range bus.Hooks {
contained := false
for _, e := range hook.Events {
contained = contained || e == evt
}
if !contained {
continue
}
var err error
name := hook.Name
if name == "" {
if hook.Kubectl != nil {
name = "kubectlApply"
} else {
name = hook.Command
}
}
if hook.Kubectl != nil {
if hook.Command != "" {
bus.Logger.Warnf("warn: ignoring command '%s' given within a kubectlApply hook", hook.Command)
}
hook.Command = "kubectl"
if val, found := hook.Kubectl["filename"]; found {
if _, found := hook.Kubectl["kustomize"]; found {
return false, fmt.Errorf("hook[%s]: kustomize & filename cannot be used together", name)
}
hook.Args = append([]string{"apply", "-f"}, val)
} else if val, found := hook.Kubectl["kustomize"]; found {
hook.Args = append([]string{"apply", "-k"}, val)
} else {
return false, fmt.Errorf("hook[%s]: either kustomize or filename must be given", name)
}
}
bus.Logger.Debugf("hook[%s]: stateFilePath=%s, basePath=%s\n", name, bus.StateFilePath, bus.BasePath)
data := map[string]interface{}{
"Environment": bus.Env,
"Namespace": bus.Namespace,
"Event": event{
Name: evt,
Error: evtErr,
},
}
for k, v := range context {
data[k] = v
}
render := tmpl.NewTextRenderer(bus.ReadFile, bus.BasePath, data)
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
command, err := render.RenderTemplateText(hook.Command)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
args := make([]string, len(hook.Args))
for i, raw := range hook.Args {
args[i], err = render.RenderTemplateText(raw)
if err != nil {
return false, fmt.Errorf("hook[%s]: %v", name, err)
}
}
bytes, err := bus.Runner.Execute(command, args, map[string]string{})
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
if hook.ShowLogs {
prefix := fmt.Sprintf("\nhook[%s] logs | ", evt)
bus.Logger.Infow(prefix + strings.ReplaceAll(string(bytes), "\n", prefix))
}
if err != nil {
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
}
executed = true
}
return executed, nil
}
+189
View File
@@ -0,0 +1,189 @@
package event
import (
"fmt"
"io"
"os"
"testing"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
var logger = helmexec.NewLogger(os.Stdout, "warn")
type runner struct {
}
func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
return []byte(""), nil
}
func (r *runner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
if cmd == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
}
for _, a := range args {
if a == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid arg: %s", a)
}
}
return []byte(""), nil
}
func TestTrigger(t *testing.T) {
cases := []struct {
name string
hook *Hook
triggeredEvt string
expectedResult bool
expectedErr string
}{
{
"okhook1",
&Hook{"okhook1", []string{"foo"}, "ok", nil, []string{}, true},
"foo",
true,
"",
},
{
"okhooké",
&Hook{"okhook2", []string{"foo"}, "ok", nil, []string{}, false},
"foo",
true,
"",
},
{
"missinghook1",
&Hook{"okhook1", []string{"foo"}, "ok", nil, []string{}, false},
"bar",
false,
"",
},
{
"nohook1",
nil,
"bar",
false,
"",
},
{
"nghook1",
&Hook{"nghook1", []string{"foo"}, "ng", nil, []string{}, false},
"foo",
false,
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
},
{
"nghook2",
&Hook{"nghook2", []string{"foo"}, "ok", nil, []string{"ng"}, false},
"foo",
false,
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
},
{
"okkubeapply1",
&Hook{"okkubeapply1", []string{"foo"}, "", map[string]string{"kustomize": "kustodir"}, []string{}, false},
"foo",
true,
"",
},
{
"okkubeapply2",
&Hook{"okkubeapply2", []string{"foo"}, "", map[string]string{"filename": "resource.yaml"}, []string{}, false},
"foo",
true,
"",
},
{
"kokubeapply",
&Hook{"kokubeapply", []string{"foo"}, "", map[string]string{"kustomize": "kustodir", "filename": "resource.yaml"}, []string{}, true},
"foo",
false,
"hook[kokubeapply]: kustomize & filename cannot be used together",
},
{
"kokubeapply2",
&Hook{"kokubeapply2", []string{"foo"}, "", map[string]string{}, []string{}, true},
"foo",
false,
"hook[kokubeapply2]: either kustomize or filename must be given",
},
{
"kokubeapply3",
&Hook{"", []string{"foo"}, "", map[string]string{}, []string{}, true},
"foo",
false,
"hook[kubectlApply]: either kustomize or filename must be given",
},
{
"warnkubeapply1",
&Hook{"warnkubeapply1", []string{"foo"}, "ok", map[string]string{"filename": "resource.yaml"}, []string{}, true},
"foo",
true,
"",
},
{
"warnkubeapply2",
&Hook{"warnkubeapply2", []string{"foo"}, "", map[string]string{"filename": "resource.yaml"}, []string{"ng"}, true},
"foo",
true,
"",
},
{
"warnkubeapply3",
&Hook{"warnkubeapply3", []string{"foo"}, "ok", map[string]string{"filename": "resource.yaml"}, []string{"ng"}, true},
"foo",
true,
"",
},
}
readFile := func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: %s", filename)
}
for _, c := range cases {
hooks := []Hook{}
if c.hook != nil {
hooks = append(hooks, *c.hook)
}
observer, observedLogs := observer.New(zap.InfoLevel)
zeLogger := zap.New(observer).Sugar()
bus := &Bus{
Hooks: hooks,
StateFilePath: "path/to/helmfile.yaml",
BasePath: "path/to",
Namespace: "myns",
Env: environment.Environment{Name: "prod"},
Logger: zeLogger,
ReadFile: readFile,
}
bus.Runner = &runner{}
data := map[string]interface{}{
"Release": "myrel",
"HelmfileCommand": "mycmd",
}
ok, err := bus.Trigger(c.triggeredEvt, nil, data)
if ok != c.expectedResult {
t.Errorf("unexpected result for case \"%s\": expected=%v, actual=%v", c.name, c.expectedResult, ok)
}
if c.expectedErr != "" {
if err == nil {
t.Errorf("error expected for case \"%s\", but not occurred", c.name)
} else if err.Error() != c.expectedErr {
t.Errorf("unexpected error for case \"%s\": expected=%s, actual=%v", c.name, c.expectedErr, err)
}
} else {
if err != nil {
t.Errorf("unexpected error for case \"%s\": %v", c.name, err)
}
}
if observedLogs.Len() != 0 && !hooks[0].ShowLogs {
t.Errorf("unexpected error for case \"%s\": Logs should not be created : %v", c.name, observedLogs.All())
}
}
}
+200
View File
@@ -0,0 +1,200 @@
package exectest
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/roboll/helmfile/pkg/helmexec"
)
type ListKey struct {
Filter string
Flags string
}
type DiffKey struct {
Name string
Chart string
Flags string
}
type Helm struct {
Charts []string
Repo []string
Releases []Release
Deleted []Release
Lists map[ListKey]string
Diffs map[DiffKey]error
Diffed []Release
FailOnUnexpectedDiff bool
FailOnUnexpectedList bool
Version *semver.Version
UpdateDepsCallbacks map[string]func(string) error
DiffMutex *sync.Mutex
ChartsMutex *sync.Mutex
ReleasesMutex *sync.Mutex
Helm3 bool
}
type Release struct {
Name string
Flags []string
}
type Affected struct {
Upgraded []*Release
Deleted []*Release
Failed []*Release
}
func (helm *Helm) UpdateDeps(chart string) error {
if strings.Contains(chart, "error") {
return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart)
}
helm.Charts = append(helm.Charts, chart)
if helm.UpdateDepsCallbacks != nil {
callback, exists := helm.UpdateDepsCallbacks[chart]
if exists {
if err := callback(chart); err != nil {
return err
}
}
}
return nil
}
func (helm *Helm) BuildDeps(name, chart string) error {
if strings.Contains(chart, "error") {
return errors.New("error")
}
helm.Charts = append(helm.Charts, chart)
return nil
}
func (helm *Helm) SetExtraArgs(args ...string) {
return
}
func (helm *Helm) SetHelmBinary(bin string) {
return
}
func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password, managed}
return nil
}
func (helm *Helm) UpdateRepo() error {
return nil
}
func (helm *Helm) RegistryLogin(name string, username string, password string) error {
return nil
}
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
if strings.Contains(name, "error") {
return errors.New("error")
}
helm.sync(helm.ReleasesMutex, func() {
helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
})
helm.sync(helm.ChartsMutex, func() {
helm.Charts = append(helm.Charts, chart)
})
return nil
}
func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
if helm.DiffMutex != nil {
helm.DiffMutex.Lock()
}
helm.Diffed = append(helm.Diffed, Release{Name: name, Flags: flags})
if helm.DiffMutex != nil {
helm.DiffMutex.Unlock()
}
key := DiffKey{Name: name, Chart: chart, Flags: strings.Join(flags, "")}
err, ok := helm.Diffs[key]
if !ok && helm.FailOnUnexpectedDiff {
return fmt.Errorf("unexpected diff with key: %v", key)
}
return err
}
func (helm *Helm) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
if strings.Contains(release, "error") {
return errors.New("error")
}
helm.Releases = append(helm.Releases, Release{Name: release, Flags: flags})
return nil
}
func (helm *Helm) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
if strings.Contains(name, "error") {
return errors.New("error")
}
helm.Deleted = append(helm.Deleted, Release{Name: name, Flags: flags})
return nil
}
func (helm *Helm) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
key := ListKey{Filter: filter, Flags: strings.Join(flags, "")}
res, ok := helm.Lists[key]
if !ok && helm.FailOnUnexpectedList {
return "", fmt.Errorf("unexpected list key: %v", key)
}
return res, nil
}
func (helm *Helm) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
return "", nil
}
func (helm *Helm) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
if strings.Contains(name, "error") {
return errors.New("error")
}
helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
return nil
}
func (helm *Helm) Fetch(chart string, flags ...string) error {
return nil
}
func (helm *Helm) Lint(name, chart string, flags ...string) error {
return nil
}
func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error {
return nil
}
func (helm *Helm) ChartPull(chart string, flags ...string) error {
return nil
}
func (helm *Helm) ChartExport(chart string, path string, flags ...string) error {
return nil
}
func (helm *Helm) IsHelm3() bool {
return helm.Helm3
}
func (helm *Helm) GetVersion() helmexec.Version {
return helmexec.Version{
Major: int(helm.Version.Major()),
Minor: int(helm.Version.Minor()),
Patch: int(helm.Version.Patch()),
}
}
func (helm *Helm) IsVersionAtLeast(versionStr string) bool {
if helm.Version == nil {
return false
}
ver := semver.MustParse(versionStr)
return helm.Version.Equal(ver) || helm.Version.GreaterThan(ver)
}
func (helm *Helm) sync(m *sync.Mutex, f func()) {
if m != nil {
m.Lock()
defer m.Unlock()
}
f()
}
+47
View File
@@ -0,0 +1,47 @@
package helmexec
import (
"io"
"os"
"path/filepath"
)
type HelmContext struct {
Tillerless bool
TillerNamespace string
HistoryMax int
WorkerIndex int
Writer io.Writer
}
func (context *HelmContext) GetTillerlessArgs(helm *execer) []string {
if context.Tillerless && !helm.IsHelm3() {
if context.TillerNamespace != "" {
return []string{"tiller", "run", context.TillerNamespace, "--", helm.helmBinary}
} else {
return []string{"tiller", "run", "--", helm.helmBinary}
}
} else {
return []string{}
}
}
func (context *HelmContext) getTillerlessEnv() map[string]string {
if context.Tillerless {
result := map[string]string{
"HELM_TILLER_SILENT": "true",
// Changing the TILLER port doesn't really work: https://github.com/helm/helm/issues/3159
// So this is not used for the moment.
// "HELM_TILLER_PORT": strconv.Itoa(44134 + context.WorkerIndex),
}
if config := os.Getenv("KUBECONFIG"); config != "" {
absConfig, err := filepath.Abs(config)
if err == nil {
result["KUBECONFIG"] = absConfig
}
}
return result
} else {
return map[string]string{}
}
}
+530
View File
@@ -0,0 +1,530 @@
package helmexec
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type decryptedSecret struct {
mutex sync.RWMutex
bytes []byte
err error
}
type execer struct {
helmBinary string
version semver.Version
runner Runner
logger *zap.SugaredLogger
kubeContext string
extra []string
decryptedSecretMutex sync.Mutex
decryptedSecrets map[string]*decryptedSecret
writeTempFile func([]byte) (string, error)
}
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
var cfg zapcore.EncoderConfig
cfg.MessageKey = "message"
out := zapcore.AddSync(writer)
var level zapcore.Level
err := level.Set(logLevel)
if err != nil {
panic(err)
}
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
out,
level,
)
return zap.New(core).Sugar()
}
func parseHelmVersion(versionStr string) (semver.Version, error) {
if len(versionStr) == 0 {
return semver.Version{}, nil
}
versionStr = strings.TrimLeft(versionStr, "Client: ")
versionStr = strings.TrimRight(versionStr, "\n")
ver, err := semver.NewVersion(versionStr)
if err != nil {
return semver.Version{}, fmt.Errorf("error parsing helm version '%s'", versionStr)
}
// Support explicit helm3 opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" && ver.Major() < 3 {
return *semver.MustParse("v3.0.0"), nil
}
return *ver, nil
}
func getHelmVersion(helmBinary string, runner Runner) (semver.Version, error) {
// Autodetect from `helm version`
outBytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil {
return semver.Version{}, fmt.Errorf("error determining helm version: %w", err)
}
return parseHelmVersion(string(outBytes))
}
// New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
// TODO: proper error handling
version, err := getHelmVersion(helmBinary, runner)
if err != nil {
panic(err)
}
return &execer{
helmBinary: helmBinary,
version: version,
logger: logger,
kubeContext: kubeContext,
runner: runner,
decryptedSecrets: make(map[string]*decryptedSecret),
}
}
func (helm *execer) SetExtraArgs(args ...string) {
helm.extra = args
}
func (helm *execer) SetHelmBinary(bin string) {
helm.helmBinary = bin
}
func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
var args []string
var out []byte
var err error
if name == "" && repository != "" {
helm.logger.Infof("empty field name\n")
return fmt.Errorf("empty field name")
}
switch managed {
case "acr":
helm.logger.Infof("Adding repo %v (acr)", name)
out, err = helm.azcli(name)
case "":
args = append(args, "repo", "add", name, repository)
// See https://github.com/helm/helm/pull/8777
if cons, err := semver.NewConstraint(">= 3.3.2"); err == nil {
if cons.Check(&helm.version) {
args = append(args, "--force-update")
}
} else {
panic(err)
}
if certfile != "" && keyfile != "" {
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
}
if cafile != "" {
args = append(args, "--ca-file", cafile)
}
if username != "" && password != "" {
args = append(args, "--username", username, "--password", password)
}
helm.logger.Infof("Adding repo %v %v", name, repository)
out, err = helm.exec(args, map[string]string{})
default:
helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name)
out = nil
err = nil
}
helm.info(out)
return err
}
func (helm *execer) UpdateRepo() error {
helm.logger.Info("Updating repo")
out, err := helm.exec([]string{"repo", "update"}, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) RegistryLogin(repository string, username string, password string) error {
helm.logger.Info("Logging in to registry")
args := []string{
"registry",
"login",
repository,
"--username",
username,
"--password-stdin",
}
buffer := bytes.Buffer{}
buffer.Write([]byte(fmt.Sprintf("%s\n", password)))
out, err := helm.execStdIn(args, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, &buffer)
helm.info(out)
return err
}
func (helm *execer) BuildDeps(name, chart string) error {
helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart)
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) UpdateDeps(chart string) error {
helm.logger.Infof("Updating dependency %v", chart)
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
helm.info(out)
return err
}
func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags ...string) error {
helm.logger.Infof("Upgrading release=%v, chart=%v", name, chart)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
if helm.IsHelm3() {
flags = append(flags, "--history-max", strconv.Itoa(context.HistoryMax))
} else {
env["HELM_TILLER_HISTORY_MAX"] = strconv.Itoa(context.HistoryMax)
}
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
helm.write(nil, out)
return err
}
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Getting status %v", name)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
helm.write(nil, out)
return err
}
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
helm.logger.Infof("Listing releases matching %v", filter)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
var args []string
if helm.IsHelm3() {
args = []string{"list", "--filter", filter}
} else {
args = []string{"list", filter}
}
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
// In v2 we have been expecting `helm list FILTER` prints nothing.
// In v3 helm still prints the header like `NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION`,
// which confuses helmfile's existing logic that treats any non-empty output from `helm list` is considered as the indication
// of the release to exist.
//
// This fixes it by removing the header from the v3 output, so that the output is formatted the same as that of v2.
if helm.IsHelm3() {
lines := strings.Split(string(out), "\n")
lines = lines[1:]
out = []byte(strings.Join(lines, "\n"))
}
helm.write(nil, out)
return string(out), err
}
func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) {
absPath, err := filepath.Abs(name)
if err != nil {
return "", err
}
helm.logger.Debugf("Preparing to decrypt secret %v", absPath)
helm.decryptedSecretMutex.Lock()
secret, ok := helm.decryptedSecrets[absPath]
// Cache miss
if !ok {
secret = &decryptedSecret{}
helm.decryptedSecrets[absPath] = secret
secret.mutex.Lock()
defer secret.mutex.Unlock()
helm.decryptedSecretMutex.Unlock()
helm.logger.Infof("Decrypting secret %v", absPath)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
helm.info(out)
if err != nil {
secret.err = err
return "", err
}
// HELM_SECRETS_DEC_SUFFIX is used by the helm-secrets plugin to define the output file
decSuffix := os.Getenv("HELM_SECRETS_DEC_SUFFIX")
if len(decSuffix) == 0 {
decSuffix = ".yaml.dec"
}
// helm secrets replaces the extension with its suffix ONLY when the extension is ".yaml"
var decFilename string
if strings.HasSuffix(absPath, ".yaml") {
decFilename = strings.Replace(absPath, ".yaml", decSuffix, 1)
} else {
decFilename = absPath + decSuffix
}
secretBytes, err := ioutil.ReadFile(decFilename)
if err != nil {
secret.err = err
return "", err
}
secret.bytes = secretBytes
if err := os.Remove(decFilename); err != nil {
return "", err
}
} else {
// Cache hit
helm.logger.Debugf("Found secret in cache %v", absPath)
secret.mutex.RLock()
helm.decryptedSecretMutex.Unlock()
defer secret.mutex.RUnlock()
if secret.err != nil {
return "", secret.err
}
}
tempFile := helm.writeTempFile
if tempFile == nil {
tempFile = func(content []byte) (string, error) {
dir := filepath.Dir(name)
extension := filepath.Ext(name)
tmpFile, err := ioutil.TempFile(dir, "secret*"+extension)
if err != nil {
return "", err
}
_, err = tmpFile.Write(content)
if err != nil {
return "", err
}
return tmpFile.Name(), nil
}
}
tmpFileName, err := tempFile(secret.bytes)
if err != nil {
return "", err
}
helm.logger.Debugf("Decrypted %s into %s", absPath, tmpFileName)
return tmpFileName, err
}
func (helm *execer) TemplateRelease(name string, chart string, flags ...string) error {
helm.logger.Infof("Templating release=%v, chart=%v", name, chart)
var args []string
if helm.IsHelm3() {
args = []string{"template", name, chart}
} else {
args = []string{"template", chart, "--name", name}
}
out, err := helm.exec(append(args, flags...), map[string]string{})
var outputToFile bool
for _, f := range flags {
if strings.HasPrefix("--output-dir", f) {
outputToFile = true
break
}
}
if outputToFile {
// With --output-dir is passed to helm-template,
// we can safely direct all the logs from it to our logger.
//
// It's safe because anything written to stdout by helm-template with output-dir is logs,
// like excessive `wrote path/to/output/dir/chart/template/file.yaml` messages,
// but manifets.
//
// See https://github.com/roboll/helmfile/pull/1691#issuecomment-805636021 for more information.
helm.info(out)
} else {
// Always write to stdout for use with e.g. `helmfile template | kubectl apply -f -`
helm.write(nil, out)
}
return err
}
func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
if context.Writer != nil {
fmt.Fprintf(context.Writer, "Comparing release=%v, chart=%v\n", name, chart)
} else {
helm.logger.Infof("Comparing release=%v, chart=%v", name, chart)
}
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env)
// Do our best to write STDOUT only when diff existed
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
detailedExitcodeEnabled := false
for _, f := range flags {
if strings.Contains(f, "detailed-exitcode") {
detailedExitcodeEnabled = true
break
}
}
if detailedExitcodeEnabled {
switch e := err.(type) {
case ExitError:
if e.ExitStatus() == 2 {
if !(suppressDiff) {
helm.write(context.Writer, out)
}
return err
}
}
} else if !(suppressDiff) {
helm.write(context.Writer, out)
}
return err
}
func (helm *execer) Lint(name, chart string, flags ...string) error {
helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
helm.write(nil, out)
return err
}
func (helm *execer) Fetch(chart string, flags ...string) error {
helm.logger.Infof("Fetching %v", chart)
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
helm.info(out)
return err
}
func (helm *execer) ChartPull(chart string, flags ...string) error {
helm.logger.Infof("Pulling %v", chart)
out, err := helm.exec(append([]string{"chart", "pull", chart}, flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"})
helm.info(out)
return err
}
func (helm *execer) ChartExport(chart string, path string, flags ...string) error {
helm.logger.Infof("Exporting %v", chart)
out, err := helm.exec(append([]string{"chart", "export", chart, "--destination", path}, flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"})
helm.info(out)
return err
}
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Deleting %v", name)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
helm.write(nil, out)
return err
}
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Testing %v", name)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
args := []string{"test", name}
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
helm.write(nil, out)
return err
}
func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
cmdargs := args
if len(helm.extra) > 0 {
cmdargs = append(cmdargs, helm.extra...)
}
if helm.kubeContext != "" {
cmdargs = append([]string{"--kube-context", helm.kubeContext}, cmdargs...)
}
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
helm.logger.Debug(cmd)
outBytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
return outBytes, err
}
func (helm *execer) execStdIn(args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
cmdargs := args
if len(helm.extra) > 0 {
cmdargs = append(cmdargs, helm.extra...)
}
if helm.kubeContext != "" {
cmdargs = append([]string{"--kube-context", helm.kubeContext}, cmdargs...)
}
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
helm.logger.Debug(cmd)
outBytes, err := helm.runner.ExecuteStdIn(helm.helmBinary, cmdargs, env, stdin)
return outBytes, err
}
func (helm *execer) azcli(name string) ([]byte, error) {
cmdargs := append(strings.Split("acr helm repo add --name", " "), name)
cmd := fmt.Sprintf("exec: az %s", strings.Join(cmdargs, " "))
helm.logger.Debug(cmd)
outBytes, err := helm.runner.Execute("az", cmdargs, map[string]string{})
helm.logger.Debugf("%s: %s", cmd, outBytes)
return outBytes, err
}
func (helm *execer) info(out []byte) {
if len(out) > 0 {
helm.logger.Infof("%s", out)
}
}
func (helm *execer) write(w io.Writer, out []byte) {
if len(out) > 0 {
if w == nil {
w = os.Stdout
}
fmt.Fprintf(w, "%s\n", out)
}
}
func (helm *execer) IsHelm3() bool {
return helm.version.Major() == 3
}
func (helm *execer) GetVersion() Version {
return Version{
Major: int(helm.version.Major()),
Minor: int(helm.version.Minor()),
Patch: int(helm.version.Patch()),
}
}
func (helm *execer) IsVersionAtLeast(versionStr string) bool {
ver := semver.MustParse(versionStr)
return helm.version.Equal(ver) || helm.version.GreaterThan(ver)
}
+615
View File
@@ -0,0 +1,615 @@
package helmexec
import (
"bytes"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/Masterminds/semver/v3"
"go.uber.org/zap"
)
// Mocking the command-line runner
type mockRunner struct {
output []byte
err error
}
func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
return mock.output, mock.err
}
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
return mock.output, mock.err
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
execer := New("helm", logger, kubeContext, &mockRunner{})
return execer
}
// Test methods
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
helm := MockExecer(NewLogger(buffer, "debug"), "dev")
if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext")
}
if buffer.String() != "something" {
t.Error("helmexec.New() - changed buffer")
}
if len(helm.extra) != 0 {
t.Error("helmexec.New() - extra args not empty")
}
}
func Test_SetExtraArgs(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
helm.SetExtraArgs()
if len(helm.extra) != 0 {
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
}
helm.SetExtraArgs("foo")
if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
t.Error("helmexec.SetExtraArgs() - one extra argument missing")
}
helm.SetExtraArgs("alpha", "beta")
if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
}
}
func Test_SetHelmBinary(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not helm")
}
helm.SetHelmBinary("foo")
if helm.helmBinary != "foo" {
t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
}
}
func Test_AddRepo_Helm_3_3_2(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := &execer{
helmBinary: "helm",
version: *semver.MustParse("3.3.2"),
logger: logger,
kubeContext: "dev",
runner: &mockRunner{},
}
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "")
expected := `Adding repo myRepo https://repo.example.com/
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_AddRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "")
expected := `Adding repo myRepo https://repo.example.com/
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --ca-file ca.crt
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kube-context dev repo add myRepo https://repo.example.com/
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("acrRepo", "", "", "", "", "", "", "acr")
expected = `Adding repo acrRepo (acr)
exec: az acr helm repo add --name acrRepo
exec: az acr helm repo add --name acrRepo:
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("otherRepo", "", "", "", "", "", "", "unknown")
expected = `ERROR: unknown type 'unknown' for repository otherRepo
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --username example_user --password example_password
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.AddRepo("", "https://repo.example.com/", "", "", "", "", "", "")
expected = `empty field name
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.UpdateRepo()
expected := `Updating repo
exec: helm --kube-context dev repo update
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_SyncRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs")
expected := `Upgrading release=release, chart=chart
exec: helm --kube-context dev upgrade --install --reset-values release chart --timeout 10 --wait --wait-for-jobs
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SyncRelease(HelmContext{}, "release", "chart")
expected = `Upgrading release=release, chart=chart
exec: helm --kube-context dev upgrade --install --reset-values release chart
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_SyncReleaseTillerless(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
"--timeout 10", "--wait", "--wait-for-jobs")
expected := `Upgrading release=release, chart=chart
exec: helm --kube-context dev tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --wait-for-jobs
`
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.UpdateDeps("./chart/foo")
expected := `Updating dependency ./chart/foo
exec: helm --kube-context dev dependency update ./chart/foo
`
if buffer.String() != expected {
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.UpdateDeps("./chart/foo")
expected = `Updating dependency ./chart/foo
exec: helm --kube-context dev dependency update ./chart/foo --verify
`
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_BuildDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.BuildDeps("foo", "./chart/foo")
expected := `Building dependency release=foo, chart=./chart/foo
exec: helm --kube-context dev dependency build ./chart/foo
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.BuildDeps("foo", "./chart/foo")
expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kube-context dev dependency build ./chart/foo --verify
`
if buffer.String() != expected {
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DecryptSecret(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
tmpFilePath := "path/to/temp/file"
helm.writeTempFile = func(content []byte) (string, error) {
return tmpFilePath, nil
}
helm.DecryptSecret(HelmContext{}, "secretName")
cwd, err := filepath.Abs(".")
if err != nil {
t.Errorf("Error: %v", err)
}
// Run again for caching
helm.DecryptSecret(HelmContext{}, "secretName")
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
Decrypting secret %s/secretName
exec: helm --kube-context dev secrets dec %s/secretName
Preparing to decrypt secret %s/secretName
Found secret in cache %s/secretName
`, cwd, cwd, cwd, cwd, cwd)
if d := cmp.Diff(expected, buffer.String()); d != "" {
t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
}
}
func Test_DecryptSecretWithGotmpl(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
tmpFilePath := "path/to/temp/file"
helm.writeTempFile = func(content []byte) (string, error) {
return tmpFilePath, nil
}
secretName := "secretName.yaml.gotmpl"
_, decryptErr := helm.DecryptSecret(HelmContext{}, secretName)
cwd, err := filepath.Abs(".")
if err != nil {
t.Errorf("Error: %v", err)
}
expected := fmt.Sprintf(`%s/%s.yaml.dec`, cwd, secretName)
if d := cmp.Diff(expected, decryptErr.(*os.PathError).Path); d != "" {
t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
}
}
func Test_DiffRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DiffRelease(HelmContext{}, "release", "chart", false, "--timeout 10", "--wait", "--wait-for-jobs")
expected := `Comparing release=release, chart=chart
exec: helm --kube-context dev diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --wait-for-jobs
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.DiffRelease(HelmContext{}, "release", "chart", false)
expected = `Comparing release=release, chart=chart
exec: helm --kube-context dev diff upgrade --reset-values --allow-unreleased release chart
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DiffReleaseTillerless(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", false, "--timeout 10", "--wait", "--wait-for-jobs")
expected := `Comparing release=release, chart=chart
exec: helm --kube-context dev tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --wait-for-jobs
`
if buffer.String() != expected {
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DeleteRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease(HelmContext{}, "release")
expected := `Deleting release
exec: helm --kube-context dev delete release
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DeleteRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DeleteRelease(HelmContext{}, "release", "--purge")
expected := `Deleting release
exec: helm --kube-context dev delete release --purge
`
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_TestRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease(HelmContext{}, "release")
expected := `Testing release
exec: helm --kube-context dev test release
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_TestRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
expected := `Testing release
exec: helm --kube-context dev test release --cleanup --timeout 60
`
if buffer.String() != expected {
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_ReleaseStatus(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.ReleaseStatus(HelmContext{}, "myRelease")
expected := `Getting status myRelease
exec: helm --kube-context dev status myRelease
`
if buffer.String() != expected {
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_exec(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "")
env := map[string]string{}
helm.exec([]string{"version"}, env)
expected := `exec: helm version
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
helm = MockExecer(logger, "dev")
ret, _ := helm.exec([]string{"diff"}, env)
if len(ret) != 0 {
t.Error("helmexec.exec() - expected empty return value")
}
buffer.Reset()
helm = MockExecer(logger, "dev")
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env)
expected = `exec: helm --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.exec([]string{"version"}, env)
expected = `exec: helm --kube-context dev version
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("foo")
helm.exec([]string{"version"}, env)
expected = `exec: helm --kube-context dev version foo
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm = MockExecer(logger, "")
helm.SetHelmBinary("overwritten")
helm.exec([]string{"version"}, env)
expected = `exec: overwritten version
`
if buffer.String() != expected {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Lint(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.Lint("release", "path/to/chart", "--values", "file.yml")
expected := `Linting release=release, chart=path/to/chart
exec: helm --kube-context dev lint path/to/chart --values file.yml
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Fetch(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
expected := `Fetching chart
exec: helm --kube-context dev fetch chart --version 1.2.3 --untar --untardir /tmp/dir
`
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
var logLevelTests = map[string]string{
"debug": `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
`,
"info": `Adding repo myRepo https://repo.example.com/
`,
"warn": ``,
}
func Test_LogLevels(t *testing.T) {
var buffer bytes.Buffer
for logLevel, expected := range logLevelTests {
buffer.Reset()
logger := NewLogger(&buffer, logLevel)
helm := MockExecer(logger, "")
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "")
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
}
func Test_getTillerlessEnv(t *testing.T) {
context := HelmContext{Tillerless: true, TillerNamespace: "foo", WorkerIndex: 1}
os.Unsetenv("KUBECONFIG")
actual := context.getTillerlessEnv()
if val, found := actual["HELM_TILLER_SILENT"]; !found || val != "true" {
t.Errorf("getTillerlessEnv() HELM_TILLER_SILENT\nactual = %s\nexpect = true", val)
}
// This feature is disabled until it is fixed in helm
/*if val, found := actual["HELM_TILLER_PORT"]; !found || val != "44135" {
t.Errorf("getTillerlessEnv() HELM_TILLER_PORT\nactual = %s\nexpect = 44135", val)
}*/
if val, found := actual["KUBECONFIG"]; found {
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = nil", val)
}
os.Setenv("KUBECONFIG", "toto")
actual = context.getTillerlessEnv()
cwd, _ := os.Getwd()
expected := path.Join(cwd, "toto")
if val, found := actual["KUBECONFIG"]; !found || val != expected {
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = %s", val, expected)
}
os.Unsetenv("KUBECONFIG")
}
func Test_mergeEnv(t *testing.T) {
actual := env2map(mergeEnv([]string{"A=1", "B=c=d", "E=2"}, map[string]string{"B": "3", "F": "4"}))
expected := map[string]string{"A": "1", "B": "3", "E": "2", "F": "4"}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
}
}
func Test_Template(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
expected := `Templating release=release, chart=path/to/chart
exec: helm --kube-context dev template path/to/chart --name release --values file.yml
`
if buffer.String() != expected {
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_IsHelm3(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version")
}
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner)
if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
}
os.Setenv("HELMFILE_HELM3", "1")
helm2Runner = mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Helm3 not detected when HELMFILE_HELM3 is set")
}
os.Setenv("HELMFILE_HELM3", "")
}
func Test_GetVersion(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
ver := helm.GetVersion()
if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver))
}
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner)
ver = helm.GetVersion()
if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver))
}
}
func Test_IsVersionAtLeast(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if !helm.IsVersionAtLeast("2.1.0") {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
}
if helm.IsVersionAtLeast("2.19.0") {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
}
if helm.IsVersionAtLeast("3.2.0") {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
}
}
+71
View File
@@ -0,0 +1,71 @@
package helmexec
import (
"fmt"
"strings"
)
func newExitError(path string, args []string, exitStatus int, err error, stderr, combined string) ExitError {
var out string
out += fmt.Sprintf("PATH:\n%s", Indent(path, " "))
out += "\n\nARGS:"
for i, a := range args {
out += fmt.Sprintf("\n%s", Indent(fmt.Sprintf("%d: %s (%d bytes)", i, a, len(a)), " "))
}
out += fmt.Sprintf("\n\nERROR:\n%s", Indent(err.Error(), " "))
out += fmt.Sprintf("\n\nEXIT STATUS\n%s", Indent(fmt.Sprintf("%d", exitStatus), " "))
if len(stderr) > 0 {
out += fmt.Sprintf("\n\nSTDERR:\n%s", Indent(stderr, " "))
}
if len(combined) > 0 {
out += fmt.Sprintf("\n\nCOMBINED OUTPUT:\n%s", Indent(combined, " "))
}
return ExitError{
Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out),
Code: exitStatus,
}
}
// indents a block of text with an indent string
func Indent(text, indent string) string {
var b strings.Builder
b.Grow(len(text) * 2)
lines := strings.Split(text, "\n")
last := len(lines) - 1
for i, j := range lines {
if i > 0 && i < last && j != "" {
b.WriteString("\n")
}
if j != "" {
b.WriteString(indent + j)
}
}
return b.String()
}
// ExitError is created whenever your shell command exits with a non-zero exit status
type ExitError struct {
Message string
Code int
}
func (e ExitError) Error() string {
return e.Message
}
func (e ExitError) ExitStatus() int {
return e.Code
}
+40
View File
@@ -0,0 +1,40 @@
package helmexec
// Version represents the version of helm
type Version struct {
Major int
Minor int
Patch int
}
// Interface for executing helm commands
type Interface interface {
SetExtraArgs(args ...string)
SetHelmBinary(bin string)
AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error
UpdateRepo() error
RegistryLogin(name string, username string, password string) error
BuildDeps(name, chart string) error
UpdateDeps(chart string) error
SyncRelease(context HelmContext, name, chart string, flags ...string) error
DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error
TemplateRelease(name, chart string, flags ...string) error
Fetch(chart string, flags ...string) error
ChartPull(chart string, flags ...string) error
ChartExport(chart string, path string, flags ...string) error
Lint(name, chart string, flags ...string) error
ReleaseStatus(context HelmContext, name string, flags ...string) error
DeleteRelease(context HelmContext, name string, flags ...string) error
TestRelease(context HelmContext, name string, flags ...string) error
List(context HelmContext, filter string, flags ...string) (string, error)
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
IsHelm3() bool
GetVersion() Version
IsVersionAtLeast(versionStr string) bool
}
type DependencyUpdater interface {
UpdateDeps(chart string) error
IsHelm3() bool
}
+13
View File
@@ -0,0 +1,13 @@
package helmexec
import "math/rand"
var executionIDComponents = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func newExecutionID() string {
b := make([]rune, 5)
for i := range b {
b[i] = executionIDComponents[rand.Intn(len(executionIDComponents))]
}
return string(b)
}
+27
View File
@@ -0,0 +1,27 @@
package helmexec
import (
"go.uber.org/zap"
"strings"
)
type logWriterGenerator struct {
log *zap.SugaredLogger
}
func (g logWriterGenerator) Writer(prefix string) *logWriter {
return &logWriter{
log: g.log,
prefix: prefix,
}
}
type logWriter struct {
log *zap.SugaredLogger
prefix string
}
func (w *logWriter) Write(p []byte) (int, error) {
w.log.Debugf("%s%s", w.prefix, strings.TrimSpace(string(p)))
return len(p), nil
}
+134
View File
@@ -0,0 +1,134 @@
package helmexec
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"go.uber.org/zap"
)
const (
tmpPrefix = "helmfile-"
tmpSuffix = "-exec"
)
// Runner interface for shell commands
type Runner interface {
Execute(cmd string, args []string, env map[string]string) ([]byte, error)
ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error)
}
// ShellRunner implemention for shell commands
type ShellRunner struct {
Dir string
Logger *zap.SugaredLogger
}
// Execute a shell command
func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
preparedCmd.Env = mergeEnv(os.Environ(), env)
return Output(preparedCmd, &logWriterGenerator{
log: shell.Logger,
})
}
// Execute a shell command
func (shell ShellRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
preparedCmd.Env = mergeEnv(os.Environ(), env)
preparedCmd.Stdin = stdin
return Output(preparedCmd, &logWriterGenerator{
log: shell.Logger,
})
}
func Output(c *exec.Cmd, logWriterGenerators ...*logWriterGenerator) ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
var stdout bytes.Buffer
var stderr bytes.Buffer
var combined bytes.Buffer
var logWriters []io.Writer
id := newExecutionID()
for _, g := range logWriterGenerators {
logPrefix := fmt.Sprintf("%s:%s> ", filepath.Base(c.Path), id)
logWriters = append(logWriters, g.Writer(logPrefix))
}
c.Stdout = io.MultiWriter(append([]io.Writer{&stdout, &combined}, logWriters...)...)
c.Stderr = io.MultiWriter(append([]io.Writer{&stderr, &combined}, logWriters...)...)
err := c.Run()
if err != nil {
// TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like:
//
// err: release "envoy2" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
// <redundant new line!>
// err: release "envoy" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
switch ee := err.(type) {
case *exec.ExitError:
// Propagate any non-zero exit status from the external command, rather than throwing it away,
// so that helmfile could return its own exit code accordingly
waitStatus := ee.Sys().(syscall.WaitStatus)
exitStatus := waitStatus.ExitStatus()
err = newExitError(c.Path, c.Args, exitStatus, ee, stderr.String(), combined.String())
default:
panic(fmt.Sprintf("unexpected error: %v", err))
}
}
return stdout.Bytes(), err
}
func mergeEnv(orig []string, new map[string]string) []string {
wanted := env2map(orig)
for k, v := range new {
wanted[k] = v
}
return map2env(wanted)
}
func map2env(wanted map[string]string) []string {
result := []string{}
for k, v := range wanted {
result = append(result, k+"="+v)
}
return result
}
func env2map(env []string) map[string]string {
wanted := map[string]string{}
for _, cur := range env {
pair := strings.SplitN(cur, "=", 2)
var v string
// An environment can completely miss `=` and the right side.
// If we didn't deal with that, this may fail due to an index-out-of-range error
if len(pair) > 1 {
v = pair[1]
}
wanted[pair[0]] = v
}
return wanted
}
+191
View File
@@ -0,0 +1,191 @@
package maputil
import (
"fmt"
"strings"
)
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
new := map[string]interface{}{}
switch src := s.(type) {
case map[interface{}]interface{}:
for k, v := range src {
var str_k string
switch typed_k := k.(type) {
case string:
str_k = typed_k
default:
return nil, fmt.Errorf("unexpected type of key in map: expected string, got %T: value=%v, map=%v", typed_k, typed_k, src)
}
casted_v, err := recursivelyStringifyMapKey(v)
if err != nil {
return nil, err
}
new[str_k] = casted_v
}
case map[string]interface{}:
for k, v := range src {
casted_v, err := recursivelyStringifyMapKey(v)
if err != nil {
return nil, err
}
new[k] = casted_v
}
}
return new, nil
}
func recursivelyStringifyMapKey(v interface{}) (interface{}, error) {
var casted_v interface{}
switch typed_v := v.(type) {
case map[interface{}]interface{}, map[string]interface{}:
tmp, err := CastKeysToStrings(typed_v)
if err != nil {
return nil, err
}
casted_v = tmp
case []interface{}:
a := []interface{}{}
for i := range typed_v {
res, err := recursivelyStringifyMapKey(typed_v[i])
if err != nil {
return nil, err
}
a = append(a, res)
}
casted_v = a
default:
casted_v = typed_v
}
return casted_v, nil
}
type arg interface {
getMap(map[string]interface{}) map[string]interface{}
set(map[string]interface{}, string)
}
type keyArg struct {
key string
}
func (a keyArg) getMap(m map[string]interface{}) map[string]interface{} {
_, ok := m[a.key]
if !ok {
m[a.key] = map[string]interface{}{}
}
switch t := m[a.key].(type) {
case map[string]interface{}:
return t
default:
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
}
}
func (a keyArg) set(m map[string]interface{}, value string) {
m[a.key] = value
}
type indexedKeyArg struct {
key string
index int
}
func (a indexedKeyArg) getArray(m map[string]interface{}) []interface{} {
_, ok := m[a.key]
if !ok {
m[a.key] = make([]interface{}, a.index+1)
}
switch t := m[a.key].(type) {
case []interface{}:
if len(t) <= a.index {
t2 := make([]interface{}, a.index+1)
copy(t, t2)
t = t2
}
return t
default:
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
}
}
func (a indexedKeyArg) getMap(m map[string]interface{}) map[string]interface{} {
t := a.getArray(m)
if t[a.index] == nil {
t[a.index] = map[string]interface{}{}
}
switch t := t[a.index].(type) {
case map[string]interface{}:
return t
default:
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
}
}
func (a indexedKeyArg) set(m map[string]interface{}, value string) {
t := a.getArray(m)
t[a.index] = value
m[a.key] = t
}
func getCursor(key string) arg {
key = strings.ReplaceAll(key, "[", " ")
key = strings.ReplaceAll(key, "]", " ")
k := key
idx := 0
n, err := fmt.Sscanf(key, "%s %d", &k, &idx)
if n == 2 && err == nil {
return indexedKeyArg{
key: k,
index: idx,
}
}
return keyArg{
key: key,
}
}
func ParseKey(key string) []string {
r := []string{}
part := ""
escaped := false
for _, rune := range key {
split := false
switch {
case escaped == false && rune == '\\':
escaped = true
continue
case rune == '.':
split = escaped == false
}
escaped = false
if split {
r = append(r, part)
part = ""
} else {
part = part + string(rune)
}
}
if len(part) > 0 {
r = append(r, part)
}
return r
}
func Set(m map[string]interface{}, key []string, value string) {
if len(key) == 0 {
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
}
for len(key) > 1 {
m, key = getCursor(key[0]).getMap(m), key[1:]
}
getCursor(key[0]).set(m, value)
}
+130
View File
@@ -0,0 +1,130 @@
package maputil
import "testing"
func TestMapUtil_StrKeys(t *testing.T) {
m := map[string]interface{}{
"a": []interface{}{
map[string]interface{}{
"b": []interface{}{
map[string]interface{}{
"c": "C",
},
},
},
},
}
r, err := CastKeysToStrings(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
a := r["a"].([]interface{})
a0 := a[0].(map[string]interface{})
b := a0["b"].([]interface{})
b0 := b[0].(map[string]interface{})
c := b0["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
func TestMapUtil_IFKeys(t *testing.T) {
m := map[interface{}]interface{}{
"a": []interface{}{
map[interface{}]interface{}{
"b": []interface{}{
map[interface{}]interface{}{
"c": "C",
},
},
},
},
}
r, err := CastKeysToStrings(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
a := r["a"].([]interface{})
a0 := a[0].(map[string]interface{})
b := a0["b"].([]interface{})
b0 := b[0].(map[string]interface{})
c := b0["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
func TestMapUtil_KeyArg(t *testing.T) {
m := map[string]interface{}{}
key := []string{"a", "b", "c"}
Set(m, key, "C")
c := (((m["a"].(map[string]interface{}))["b"]).(map[string]interface{}))["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
func TestMapUtil_IndexedKeyArg(t *testing.T) {
m := map[string]interface{}{}
key := []string{"a", "b[0]", "c"}
Set(m, key, "C")
c := (((m["a"].(map[string]interface{}))["b"].([]interface{}))[0].(map[string]interface{}))["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
type parseKeyTc struct {
key string
result map[int]string
}
func TestMapUtil_ParseKey(t *testing.T) {
tcs := []parseKeyTc{
{
key: `a.b.c`,
result: map[int]string{
0: "a",
1: "b",
2: "c",
},
},
{
key: `a\.b.c`,
result: map[int]string{
0: "a.b",
1: "c",
},
},
{
key: `a\.b\.c`,
result: map[int]string{
0: "a.b.c",
},
},
}
for _, tc := range tcs {
parts := ParseKey(tc.key)
for index, value := range tc.result {
if parts[index] != value {
t.Errorf("unexpected key part[%d]: expected=%s, got=%s", index, value, parts[index])
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
package plugins
import (
"github.com/variantdev/vals"
"sync"
)
const (
// cache size for improving performance of ref+.* secrets rendering
valsCacheSize = 512
)
var instance *vals.Runtime
var once sync.Once
func ValsInstance() (*vals.Runtime, error) {
var err error
once.Do(func() {
instance, err = vals.New(vals.Options{CacheSize: valsCacheSize})
})
return instance, err
}
+17
View File
@@ -0,0 +1,17 @@
package plugins
import "testing"
func TestValsInstance(t *testing.T) {
i, err := ValsInstance()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
i2, _ := ValsInstance()
if i != i2 {
t.Error("Instances should be equal")
}
}
+279
View File
@@ -0,0 +1,279 @@
package remote
import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/helper/url"
"go.uber.org/multierr"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"os"
"path/filepath"
"strings"
)
const DefaultCacheDir = ".helmfile/cache"
type Remote struct {
Logger *zap.SugaredLogger
// Home is the home directory for helmfile. Usually this points to $HOME of the user running helmfile.
// Helmfile saves fetched remote files into .helmfile/cache under home
Home string
// Getter is the underlying implementation of getter used for fetching remote files
Getter Getter
// ReadFile is the implementation of the file reader that reads a local file from the specified path.
// Inject any implementation of your choice, like an im-memory impl for testing, ioutil.ReadFile for the real-world use.
ReadFile func(string) ([]byte, error)
DirExists func(string) bool
FileExists func(string) bool
}
func (r *Remote) Unmarshal(src string, dst interface{}) error {
bytes, err := r.GetBytes(src)
if err != nil {
return err
}
strs := strings.Split(src, "/")
file := strs[len(strs)-1]
ext := filepath.Ext(file)
{
r.Logger.Debugf("unmarshalling %s", string(bytes))
var err error
switch ext {
case "json":
err = json.Unmarshal(bytes, dst)
default:
err = yaml.Unmarshal(bytes, dst)
}
r.Logger.Debugf("unmarshalled to %v", dst)
if err != nil {
return err
}
}
return nil
}
func (r *Remote) GetBytes(goGetterSrc string) ([]byte, error) {
f, err := r.Fetch(goGetterSrc)
if err != nil {
return nil, err
}
bytes, err := r.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("read file: %v", err)
}
return bytes, nil
}
// Locate takes an URL to a remote file or a path to a local file.
// If the argument was an URL, it fetches the remote directory contained within the URL,
// and returns the path to the file in the fetched directory
func (r *Remote) Locate(urlOrPath string) (string, error) {
if r.FileExists(urlOrPath) || r.DirExists(urlOrPath) {
return urlOrPath, nil
}
fetched, err := r.Fetch(urlOrPath)
if err != nil {
switch err.(type) {
case InvalidURLError:
return urlOrPath, nil
}
return "", err
}
return fetched, nil
}
type InvalidURLError struct {
err string
}
func (e InvalidURLError) Error() string {
return e.err
}
type Source struct {
Getter, Scheme, User, Host, Dir, File, RawQuery string
}
func IsRemote(goGetterSrc string) bool {
if _, err := Parse(goGetterSrc); err != nil {
return false
}
return true
}
func Parse(goGetterSrc string) (*Source, error) {
items := strings.Split(goGetterSrc, "::")
var getter string
switch len(items) {
case 2:
getter = items[0]
goGetterSrc = items[1]
}
u, err := url.Parse(goGetterSrc)
if err != nil {
return nil, InvalidURLError{err: fmt.Sprintf("parse url: %v", err)}
}
if u.Scheme == "" {
return nil, InvalidURLError{err: fmt.Sprintf("parse url: missing scheme - probably this is a local file path? %s", goGetterSrc)}
}
pathComponents := strings.Split(u.Path, "@")
if len(pathComponents) != 2 {
return nil, fmt.Errorf("invalid src format: it must be `[<getter>::]<scheme>://<host>/<path/to/dir>@<path/to/file>?key1=val1&key2=val2: got %s", goGetterSrc)
}
return &Source{
Getter: getter,
User: u.User.String(),
Scheme: u.Scheme,
Host: u.Host,
Dir: pathComponents[0],
File: pathComponents[1],
RawQuery: u.RawQuery,
}, nil
}
func (r *Remote) Fetch(goGetterSrc string, cacheDirOpt ...string) (string, error) {
u, err := Parse(goGetterSrc)
if err != nil {
return "", err
}
srcDir := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
file := u.File
r.Logger.Debugf("getter: %s", u.Getter)
r.Logger.Debugf("scheme: %s", u.Scheme)
r.Logger.Debugf("user: %s", u.User)
r.Logger.Debugf("host: %s", u.Host)
r.Logger.Debugf("dir: %s", u.Dir)
r.Logger.Debugf("file: %s", u.File)
// This should be shared across variant commands, so that they can share cache for the shared imports
cacheBaseDir := DefaultCacheDir
if len(cacheDirOpt) == 1 {
cacheBaseDir = cacheDirOpt[0]
} else if len(cacheDirOpt) > 0 {
return "", fmt.Errorf("[bug] cacheDirOpt's length: want 0 or 1, got %d", len(cacheDirOpt))
}
query := u.RawQuery
var cacheKey string
replacer := strings.NewReplacer(":", "", "//", "_", "/", "_", ".", "_")
dirKey := replacer.Replace(srcDir)
if len(query) > 0 {
paramsKey := strings.Replace(query, "&", "_", -1)
cacheKey = fmt.Sprintf("%s.%s", dirKey, paramsKey)
} else {
cacheKey = dirKey
}
cached := false
// e.g. .helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
getterDst := filepath.Join(cacheBaseDir, cacheKey)
// e.g. $PWD/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
cacheDirPath := filepath.Join(r.Home, getterDst)
r.Logger.Debugf("home: %s", r.Home)
r.Logger.Debugf("getter dest: %s", getterDst)
r.Logger.Debugf("cached dir: %s", cacheDirPath)
{
if r.FileExists(cacheDirPath) {
return "", fmt.Errorf("%s is not directory. please remove it so that variant could use it for dependency caching", getterDst)
}
if r.DirExists(cacheDirPath) {
cached = true
}
}
if !cached {
var getterSrc string
if u.User != "" {
getterSrc = fmt.Sprintf("%s://%s@%s%s", u.Scheme, u.User, u.Host, u.Dir)
} else {
getterSrc = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
}
if len(query) > 0 {
getterSrc = strings.Join([]string{getterSrc, query}, "?")
}
if u.Getter != "" {
getterSrc = u.Getter + "::" + getterSrc
}
r.Logger.Debugf("downloading %s to %s", getterSrc, getterDst)
if err := r.Getter.Get(r.Home, getterSrc, cacheDirPath); err != nil {
rmerr := os.RemoveAll(cacheDirPath)
if rmerr != nil {
return "", multierr.Append(err, rmerr)
}
return "", err
}
}
return filepath.Join(cacheDirPath, file), nil
}
type Getter interface {
Get(wd, src, dst string) error
}
type GoGetter struct {
Logger *zap.SugaredLogger
}
func (g *GoGetter) Get(wd, src, dst string) error {
ctx := context.Background()
get := &getter.Client{
Ctx: ctx,
Src: src,
Dst: dst,
Pwd: wd,
Mode: getter.ClientModeDir,
Options: []getter.ClientOption{},
}
g.Logger.Debugf("client: %+v", *get)
if err := get.Get(); err != nil {
return fmt.Errorf("get: %v", err)
}
return nil
}
func NewRemote(logger *zap.SugaredLogger, homeDir string, readFile func(string) ([]byte, error), dirExists func(string) bool, fileExists func(string) bool) *Remote {
remote := &Remote{
Logger: logger,
Home: homeDir,
Getter: &GoGetter{Logger: logger},
ReadFile: readFile,
DirExists: dirExists,
FileExists: fileExists,
}
return remote
}
+236
View File
@@ -0,0 +1,236 @@
package remote
import (
"fmt"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
)
func TestRemote_HttpsGitHub(t *testing.T) {
cleanfs := map[string]string{
"/path/to/home": "",
}
cachefs := map[string]string{
"/path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
}
type testcase struct {
files map[string]string
expectCacheHit bool
}
testcases := []testcase{
{files: cleanfs, expectCacheHit: false},
{files: cachefs, expectCacheHit: true},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
testfs := testhelper.NewTestFs(testcase.files)
hit := true
get := func(wd, src, dst string) error {
if wd != "/path/to/home" {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0" {
return fmt.Errorf("unexpected src: %s", src)
}
hit = false
return nil
}
getter := &testGetter{
get: get,
}
remote := &Remote{
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Home: "/path/to/home",
Getter: getter,
ReadFile: testfs.ReadFile,
FileExists: testfs.FileExistsAt,
DirExists: testfs.DirectoryExistsAt,
}
// FYI, go-getter in the `dir` mode accepts URL like the below. So helmfile expects URLs similar to it:
// go-getter -mode dir git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0 gettertest1/b
// We use `@` to separate dir and the file path. This is a good idea borrowed from helm-git:
// https://github.com/aslafy-z/helm-git
url := "git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
file, err := remote.Locate(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if file != "/path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
t.Errorf("unexpected file located: %s", file)
}
if testcase.expectCacheHit && !hit {
t.Errorf("unexpected result: unexpected cache miss")
}
if !testcase.expectCacheHit && hit {
t.Errorf("unexpected result: unexpected cache hit")
}
})
}
}
func TestRemote_SShGitHub(t *testing.T) {
cleanfs := map[string]string{
"/path/to/home": "",
}
cachefs := map[string]string{
"/path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
}
type testcase struct {
files map[string]string
expectCacheHit bool
}
testcases := []testcase{
{files: cleanfs, expectCacheHit: false},
{files: cachefs, expectCacheHit: true},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
testfs := testhelper.NewTestFs(testcase.files)
hit := true
get := func(wd, src, dst string) error {
if wd != "/path/to/home" {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::ssh://git@github.com/cloudposse/helmfiles.git?ref=0.40.0" {
return fmt.Errorf("unexpected src: %s", src)
}
hit = false
return nil
}
getter := &testGetter{
get: get,
}
remote := &Remote{
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Home: "/path/to/home",
Getter: getter,
ReadFile: testfs.ReadFile,
FileExists: testfs.FileExistsAt,
DirExists: testfs.DirectoryExistsAt,
}
url := "git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
file, err := remote.Locate(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if file != "/path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
t.Errorf("unexpected file located: %s", file)
}
if testcase.expectCacheHit && !hit {
t.Errorf("unexpected result: unexpected cache miss")
}
if !testcase.expectCacheHit && hit {
t.Errorf("unexpected result: unexpected cache hit")
}
})
}
}
func TestParse(t *testing.T) {
type testcase struct {
input string
getter, scheme, dir, file, query string
err string
}
testcases := []testcase{
{
input: "raw/incubator",
err: "parse url: missing scheme - probably this is a local file path? raw/incubator",
},
{
input: "git::https://github.com/stakater/Forecastle.git@deployments/kubernetes/chart/forecastle?ref=v1.0.54",
getter: "git",
scheme: "https",
dir: "/stakater/Forecastle.git",
file: "deployments/kubernetes/chart/forecastle",
query: "ref=v1.0.54",
},
}
for i := range testcases {
tc := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
src, err := Parse(tc.input)
var errMsg string
if err != nil {
errMsg = err.Error()
}
if diff := cmp.Diff(tc.err, errMsg); diff != "" {
t.Fatalf("Unexpected error:\n%s", diff)
}
var getter, scheme, dir, file, query string
if src != nil {
getter = src.Getter
scheme = src.Scheme
dir = src.Dir
file = src.File
query = src.RawQuery
}
if diff := cmp.Diff(tc.getter, getter); diff != "" {
t.Fatalf("Unexpected getter:\n%s", diff)
}
if diff := cmp.Diff(tc.scheme, scheme); diff != "" {
t.Fatalf("Unexpected scheme:\n%s", diff)
}
if diff := cmp.Diff(tc.file, file); diff != "" {
t.Fatalf("Unexpected file:\n%s", diff)
}
if diff := cmp.Diff(tc.dir, dir); diff != "" {
t.Fatalf("Unexpected dir:\n%s", diff)
}
if diff := cmp.Diff(tc.query, query); diff != "" {
t.Fatalf("Unexpected query:\n%s", diff)
}
})
}
}
type testGetter struct {
get func(wd, src, dst string) error
}
func (t *testGetter) Get(wd, src, dst string) error {
return t.get(wd, src, dst)
}
+458
View File
@@ -0,0 +1,458 @@
package state
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/Masterminds/semver/v3"
goversion "github.com/hashicorp/go-version"
"github.com/r3labs/diff"
"github.com/roboll/helmfile/pkg/app/version"
"github.com/roboll/helmfile/pkg/helmexec"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
type ChartMeta struct {
Name string `yaml:"name"`
}
type unresolvedChartDependency struct {
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
// It can't be collided with other charts referenced in the same helmfile spec.
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
ChartName string `yaml:"name"`
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
Repository string `yaml:"repository"`
// VersionConstraint is the version constraint of the dependent chart. "*" means the latest version.
VersionConstraint string `yaml:"version"`
}
type ResolvedChartDependency struct {
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
// It can't be collided with other charts referenced in the same helmfile spec.
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
ChartName string `yaml:"name"`
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
Repository string `yaml:"repository"`
// Version is the version number of the dependent chart.
// In the context of helmfile this can be omitted. When omitted, it is considered `*` which results helm/helmfile fetching the latest version.
Version string `yaml:"version"`
}
type UnresolvedDependencies struct {
deps map[string][]unresolvedChartDependency
}
type ChartRequirements struct {
UnresolvedDependencies []unresolvedChartDependency `yaml:"dependencies"`
}
type ChartLockedRequirements struct {
Version string `yaml:"version"`
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
Digest string `yaml:"digest"`
Generated string `yaml:"generated"`
}
func (d *UnresolvedDependencies) Add(chart, url, versionConstraint string) error {
dep := unresolvedChartDependency{
ChartName: chart,
Repository: url,
VersionConstraint: versionConstraint,
}
return d.add(dep)
}
func (d *UnresolvedDependencies) add(dep unresolvedChartDependency) error {
deps := d.deps[dep.ChartName]
if deps == nil {
deps = []unresolvedChartDependency{dep}
} else {
deps = append(deps, dep)
}
d.deps[dep.ChartName] = deps
return nil
}
func (d *UnresolvedDependencies) ToChartRequirements() *ChartRequirements {
deps := []unresolvedChartDependency{}
for _, ds := range d.deps {
for _, d := range ds {
if d.VersionConstraint == "" {
d.VersionConstraint = "*"
}
deps = append(deps, d)
}
}
return &ChartRequirements{UnresolvedDependencies: deps}
}
type ResolvedDependencies struct {
deps map[string][]ResolvedChartDependency
}
func (d *ResolvedDependencies) add(dep ResolvedChartDependency) error {
deps := d.deps[dep.ChartName]
if deps == nil {
deps = []ResolvedChartDependency{dep}
} else {
deps = append(deps, dep)
}
d.deps[dep.ChartName] = deps
return nil
}
func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, error) {
if versionConstraint == "" {
versionConstraint = "*"
}
deps, exists := d.deps[chart]
if exists {
for _, dep := range deps {
constraint, err := semver.NewConstraint(versionConstraint)
if err != nil {
return "", err
}
version, err := semver.NewVersion(dep.Version)
if err != nil {
return "", err
}
if constraint.Check(version) {
return dep.Version, nil
}
}
}
return "", fmt.Errorf("no resolved dependency found for \"%s\", running \"helmfile deps\" may resolve the issue", chart)
}
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
filename, unresolved, err := getUnresolvedDependenciess(st)
if err != nil {
return nil, err
}
if len(unresolved.deps) == 0 {
return st, nil
}
depMan := NewChartDependencyManager(filename, st.logger)
if st.readFile != nil {
depMan.readFile = st.readFile
}
return resolveDependencies(st, depMan, unresolved)
}
func resolveDependencies(st *HelmState, depMan *chartDependencyManager, unresolved *UnresolvedDependencies) (*HelmState, error) {
resolved, lockfileExists, err := depMan.Resolve(unresolved)
if err != nil {
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
}
if !lockfileExists {
return st, nil
}
repoToURL := map[string]string{}
for _, r := range st.Repositories {
repoToURL[r.Name] = r.URL
}
updated := *st
for i, r := range updated.Releases {
repo, chart, ok := resolveRemoteChart(r.Chart)
if !ok {
continue
}
_, ok = repoToURL[repo]
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
// which may imply that this is a local chart within a directory, like `charts/myapp`
if !ok {
continue
}
ver, err := resolved.Get(chart, r.Version)
if err != nil {
return nil, err
}
updated.Releases[i].Version = ver
}
return &updated, nil
}
func (st *HelmState) updateDependenciesInTempDir(shell helmexec.DependencyUpdater, tempDir func(string, string) (string, error)) (*HelmState, error) {
filename, unresolved, err := getUnresolvedDependenciess(st)
if err != nil {
return nil, err
}
if len(unresolved.deps) == 0 {
st.logger.Warnf("There are no repositories defined in your helmfile.yaml.\nThis means helmfile cannot update your dependencies or create a lock file.\nSee https://github.com/roboll/helmfile/issues/878 for more information.")
return st, nil
}
d, err := tempDir("", "")
if err != nil {
return nil, fmt.Errorf("unable to create dir: %v", err)
}
defer os.RemoveAll(d)
return updateDependencies(st, shell, unresolved, filename, d)
}
func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies, error) {
repoToURL := map[string]string{}
for _, r := range st.Repositories {
repoToURL[r.Name] = r.URL
}
unresolved := &UnresolvedDependencies{deps: map[string][]unresolvedChartDependency{}}
//if err := unresolved.Add("stable/envoy", "https://kubernetes-charts.storage.googleapis.com", ""); err != nil {
// panic(err)
//}
for _, r := range st.Releases {
repo, chart, ok := resolveRemoteChart(r.Chart)
if !ok {
continue
}
url, ok := repoToURL[repo]
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
// which may imply that this is a local chart within a directory, like `charts/myapp`
if !ok {
continue
}
if err := unresolved.Add(chart, url, r.Version); err != nil {
return "", nil, err
}
}
filename := filepath.Base(st.FilePath)
filename = strings.TrimSuffix(filename, ".gotmpl")
filename = strings.TrimSuffix(filename, ".yaml")
filename = strings.TrimSuffix(filename, ".yml")
return filename, unresolved, nil
}
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
depMan := NewChartDependencyManager(filename, st.logger)
_, err := depMan.Update(shell, wd, unresolved)
if err != nil {
return nil, fmt.Errorf("unable to update %d deps: %v", len(unresolved.deps), err)
}
return resolveDependencies(st, depMan, unresolved)
}
type chartDependencyManager struct {
Name string
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
writeFile func(string, []byte, os.FileMode) error
}
func NewChartDependencyManager(name string, logger *zap.SugaredLogger) *chartDependencyManager {
return &chartDependencyManager{
Name: name,
readFile: ioutil.ReadFile,
writeFile: ioutil.WriteFile,
logger: logger,
}
}
func (m *chartDependencyManager) lockFileName() string {
return fmt.Sprintf("%s.lock", m.Name)
}
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
if shell.IsHelm3() {
return m.updateHelm3(shell, wd, unresolved)
}
return m.updateHelm2(shell, wd, unresolved)
}
func (m *chartDependencyManager) updateHelm3(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
// Generate `Chart.yaml` of the temporary local chart
chartMetaContent := fmt.Sprintf("name: %s\nversion: 1.0.0\napiVersion: v2\n", m.Name)
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
if err != nil {
return nil, err
}
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(chartMetaContent+string(reqsContent))); err != nil {
return nil, err
}
return m.doUpdate("Chart.lock", unresolved, shell, wd)
}
func (m *chartDependencyManager) updateHelm2(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
// Generate `Chart.yaml` of the temporary local chart
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\nversion: 1.0.0\n", m.Name))); err != nil {
return nil, err
}
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
if err != nil {
return nil, err
}
if err := m.writeBytes(filepath.Join(wd, "requirements.yaml"), reqsContent); err != nil {
return nil, err
}
return m.doUpdate("requirements.lock", unresolved, shell, wd)
}
func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *UnresolvedDependencies, shell helmexec.DependencyUpdater, wd string) (*ResolvedDependencies, error) {
// Generate `requirements.lock` of the temporary local chart by coping `<basename>.lock`
lockFile := m.lockFileName()
originalLockFileContent, err := m.readBytes(lockFile)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if shell.IsHelm3() && originalLockFileContent != nil {
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
return nil, err
}
}
// Update the lock file by running `helm dependency update`
if err := shell.UpdateDeps(wd); err != nil {
return nil, err
}
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, chartLockFile))
if err != nil {
return nil, err
}
// Sort requirements alphabetically by name.
lockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
return nil, err
}
sort.Slice(lockedReqs.ResolvedDependencies, func(i, j int) bool {
return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName
})
// Don't update lock file if no dependency updated.
if !shell.IsHelm3() && originalLockFileContent != nil {
originalLockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil {
return nil, err
}
changes, err := diff.Diff(originalLockedReqs.ResolvedDependencies, lockedReqs.ResolvedDependencies)
if err != nil {
return nil, err
}
if len(changes) == 0 {
lockedReqs.Generated = originalLockedReqs.Generated
}
}
lockedReqs.Version = version.Version
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
if err != nil {
return nil, err
}
// Commit the lock file if and only if everything looks ok
if err := m.writeBytes(lockFile, updatedLockFileContent); err != nil {
return nil, err
}
resolved, _, err := m.Resolve(unresolved)
return resolved, err
}
func (m *chartDependencyManager) Resolve(unresolved *UnresolvedDependencies) (*ResolvedDependencies, bool, error) {
updatedLockFileContent, err := m.readBytes(m.lockFileName())
if err != nil {
if os.IsNotExist(err) {
return nil, false, nil
}
return nil, false, err
}
// Load resolved dependencies into memory
lockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
return nil, false, err
}
// Make sure go run main.go works and compatible with old lock files.
if version.Version != "" && lockedReqs.Version != "" {
lockedVersion, err := goversion.NewVersion(lockedReqs.Version)
if err != nil {
return nil, false, err
}
currentVersion, err := goversion.NewVersion(version.Version)
if err != nil {
return nil, false, err
}
if currentVersion.LessThan(lockedVersion) {
return nil, false, fmt.Errorf("the lockfile was created by Helmfile %s, which is newer than current %s; Please upgrade to Helmfile %s or greater", lockedVersion.Original(), currentVersion.Original(), lockedVersion.Original())
}
}
resolved := &ResolvedDependencies{deps: map[string][]ResolvedChartDependency{}}
for _, d := range lockedReqs.ResolvedDependencies {
if err := resolved.add(d); err != nil {
return nil, false, err
}
}
return resolved, true, nil
}
func (m *chartDependencyManager) readBytes(filename string) ([]byte, error) {
bytes, err := m.readFile(filename)
if err != nil {
return nil, err
}
m.logger.Debugf("readBytes: read from %s:\n%s", filename, bytes)
return bytes, nil
}
func (m *chartDependencyManager) writeBytes(filename string, data []byte) error {
err := m.writeFile(filename, data, 0644)
if err != nil {
return err
}
m.logger.Debugf("writeBytes: wrote to %s:\n%s", filename, data)
return nil
}
+378
View File
@@ -0,0 +1,378 @@
package state
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/variantdev/vals"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
const (
DefaultHelmBinary = "helm"
)
type StateLoadError struct {
msg string
Cause error
}
func (e *StateLoadError) Error() string {
return fmt.Sprintf("%s: %v", e.msg, e.Cause)
}
type UndefinedEnvError struct {
msg string
}
func (e *UndefinedEnvError) Error() string {
return e.msg
}
type StateCreator struct {
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
fileExists func(string) (bool, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
DeleteFile func(string) error
directoryExistsAt func(string) bool
valsRuntime vals.Evaluator
Strict bool
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
getHelm func(*HelmState) helmexec.Interface
overrideHelmBinary string
remote *remote.Remote
}
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error), directoryExistsAt func(string) bool, valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, remote *remote.Remote) *StateCreator {
return &StateCreator{
logger: logger,
readFile: readFile,
fileExists: fileExists,
abs: abs,
glob: glob,
directoryExistsAt: directoryExistsAt,
Strict: true,
valsRuntime: valsRuntime,
getHelm: getHelm,
overrideHelmBinary: overrideHelmBinary,
remote: remote,
}
}
// Parse parses YAML into HelmState
func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, error) {
var state HelmState
state.FilePath = file
state.basePath = baseDir
decoder := yaml.NewDecoder(bytes.NewReader(content))
if !c.Strict {
decoder.SetStrict(false)
} else {
decoder.SetStrict(true)
}
i := 0
for {
i++
var intermediate HelmState
err := decoder.Decode(&intermediate)
if err == io.EOF {
break
} else if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d", file, i), err}
}
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice); err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
}
}
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{}
}
if state.DeprecatedContext != "" && state.HelmDefaults.KubeContext == "" {
state.HelmDefaults.KubeContext = state.DeprecatedContext
}
if c.overrideHelmBinary != "" && c.overrideHelmBinary != DefaultHelmBinary {
state.DefaultHelmBinary = c.overrideHelmBinary
} else if state.DefaultHelmBinary == "" {
// Let `helmfile --helm-binary ""` not break this helmfile run
state.DefaultHelmBinary = DefaultHelmBinary
}
state.logger = c.logger
state.readFile = c.readFile
state.removeFile = os.Remove
state.fileExists = c.fileExists
state.glob = c.glob
state.directoryExistsAt = c.directoryExistsAt
state.valsRuntime = c.valsRuntime
return &state, nil
}
// LoadEnvValues loads environment values files relative to the `baseDir`
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment, failOnMissingEnv bool) (*HelmState, error) {
state := *target
e, err := c.loadEnvValues(&state, env, failOnMissingEnv, ctxEnv, c.readFile, c.glob)
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
newDefaults, err := state.loadValuesEntries(nil, state.DefaultValues, c.remote)
if err != nil {
return nil, err
}
if err := mergo.Merge(&e.Defaults, newDefaults, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, err
}
state.Env = *e
return &state, nil
}
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
// evaluateBases=true means that this is NOT a base helmfile
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues *environment.Environment) (*HelmState, error) {
state, err := c.Parse(content, baseDir, file)
if err != nil {
return nil, err
}
if !evaluateBases {
if len(state.Bases) > 0 {
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
}
} else {
state, err = c.loadBases(envValues, state, baseDir)
if err != nil {
return nil, err
}
}
state, err = c.LoadEnvValues(state, envName, envValues, evaluateBases)
if err != nil {
return nil, err
}
state.FilePath = file
vals, err := state.Env.GetMergedValues()
if err != nil {
return nil, fmt.Errorf("rendering values: %w", err)
}
state.RenderedValues = vals
return state, nil
}
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
layers := []*HelmState{}
for _, b := range st.Bases {
base, err := c.LoadFile(envValues, baseDir, b, false)
if err != nil {
return nil, err
}
layers = append(layers, base)
}
layers = append(layers, st)
for i := 1; i < len(layers); i++ {
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
return nil, err
}
}
return layers[0], nil
}
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name]
if ok {
var err error
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values, c.remote)
if err != nil {
return nil, err
}
if len(envSpec.Secrets) > 0 {
var envSecretFiles []string
for _, urlOrPath := range envSpec.Secrets {
resolved, skipped, err := st.storage().resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
continue
}
envSecretFiles = append(envSecretFiles, resolved...)
}
if err = c.scatterGatherEnvSecretFiles(st, envSecretFiles, envVals, readFile); err != nil {
return nil, err
}
}
} else if ctxEnv == nil && name != DefaultEnv && failOnMissingEnv {
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
}
newEnv := &environment.Environment{Name: name, Values: envVals}
if ctxEnv != nil {
intEnv := *ctxEnv
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
}
newEnv = &intEnv
}
return newEnv, nil
}
func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
var errs []error
helm := c.getHelm(st)
inputs := envSecretFiles
inputsSize := len(inputs)
type secretResult struct {
id int
result map[string]interface{}
err error
path string
}
type secretInput struct {
id int
path string
}
secrets := make(chan secretInput, inputsSize)
results := make(chan secretResult, inputsSize)
st.scatterGather(0, inputsSize,
func() {
for i, secretFile := range envSecretFiles {
secrets <- secretInput{i, secretFile}
}
close(secrets)
},
func(id int) {
for secret := range secrets {
release := &ReleaseSpec{}
flags := st.appendConnectionFlags([]string{}, helm, release)
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), secret.path, flags...)
if err != nil {
results <- secretResult{secret.id, nil, err, secret.path}
continue
}
defer func() {
if err := c.DeleteFile(decFile); err != nil {
c.logger.Warnf("removing decrypted file %s: %w", decFile, err)
}
}()
bytes, err := readFile(decFile)
if err != nil {
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
continue
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
continue
}
// All the nested map key should be string. Otherwise we get strange errors due to that
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
// See https://github.com/roboll/helmfile/issues/677
vals, err := maputil.CastKeysToStrings(m)
if err != nil {
results <- secretResult{secret.id, nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", secret.path, err), secret.path}
continue
}
results <- secretResult{secret.id, vals, nil, secret.path}
}
},
func() {
sortedSecrets := make([]secretResult, inputsSize)
for i := 0; i < inputsSize; i++ {
result := <-results
sortedSecrets[result.id] = result
}
close(results)
for _, result := range sortedSecrets {
if result.err != nil {
errs = append(errs, result.err)
} else {
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
errs = append(errs, fmt.Errorf("failed to load environment secrets file \"%s\": %v", result.path, err))
}
}
}
},
)
if len(errs) > 0 {
for _, err := range errs {
st.logger.Error(err)
}
return fmt.Errorf("Failed loading environment secrets with %d errors", len(errs))
}
return nil
}
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}, remote *remote.Remote) (map[string]interface{}, error) {
envVals := map[string]interface{}{}
valuesEntries := append([]interface{}{}, entries...)
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger, remote)
var err error
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
if err != nil {
return nil, err
}
return envVals, nil
}
+486
View File
@@ -0,0 +1,486 @@
package state
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/testhelper"
"go.uber.org/zap"
. "gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
c := &StateCreator{
logger: logger,
readFile: ioutil.ReadFile,
abs: filepath.Abs,
DeleteFile: os.Remove,
Strict: true,
}
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil)
}
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_NonexistentEnv(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")
}
}
type stateTestEnv struct {
Files map[string]string
WorkDir string
}
func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState {
t.Helper()
testFs := testhelper.NewTestFs(testEnv.Files)
if testFs.Cwd == "" {
testFs.Cwd = "/"
}
yamlContent, ok := testEnv.Files[file]
if !ok {
t.Fatalf("no file named %q registered", file)
}
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, testFs.DirectoryExistsAt, nil, nil, "", r).
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return state
}
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 }}
releaseName: {{ .Release.Name }}
releaseNamespace: {{ .Release.Namespace }}
`)
expectedValues := `env: production
releaseName: myrelease
releaseNamespace: mynamespace
`
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, testFs.DirectoryExistsAt, nil, nil, "", r).
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
actual := state.Env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
release := state.Releases[0]
state.ApplyOverrides(&release)
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, 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_OverrideNamespace(t *testing.T) {
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
production:
values:
- foo.yaml
- bar.yaml.gotmpl
# A.k.a helmfile apply --namespace myns
namespace: myns
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 }}
releaseName: {{ .Release.Name }}
releaseNamespace: {{ .Release.Namespace }}
overrideNamespace: {{ .Namespace }}
`)
expectedValues := `env: production
releaseName: myrelease
releaseNamespace: myns
overrideNamespace: myns
`
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, testFs.DirectoryExistsAt, nil, nil, "", r).
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
actual := state.Env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
release := state.Releases[0]
state.ApplyOverrides(&release)
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, 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}},
{LabelFilter{negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, 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)
}
}
}
}
func TestReadFromYaml_Helmfiles_Selectors(t *testing.T) {
tests := []struct {
path string
content []byte
wantErr bool
helmfiles []SubHelmfileSpec
}{
{
path: "working/selector",
content: []byte(`helmfiles:
- simple/helmfile.yaml
- path: path/prefix/selector.yaml
selectors:
- name=zorba
- foo=bar
- path: path/prefix/empty/selector.yaml
selectors: []
- path: path/prefix/inherits/selector.yaml
selectorsInherited: true
`),
wantErr: false,
helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml", Selectors: nil, SelectorsInherited: false},
{Path: "path/prefix/selector.yaml", Selectors: []string{"name=zorba", "foo=bar"}, SelectorsInherited: false},
{Path: "path/prefix/empty/selector.yaml", Selectors: []string{}, SelectorsInherited: false},
{Path: "path/prefix/inherits/selector.yaml", Selectors: nil, SelectorsInherited: true},
},
},
{
path: "failing2/selector",
content: []byte(`helmfiles:
- path: failing2/helmfile.yaml
wrongkey:
`),
wantErr: true,
},
{
path: "failing3/selector",
content: []byte(`helmfiles:
- path: failing3/helmfile.yaml
selectors: foo
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- path: failing4/helmfile.yaml
selectors:
`),
wantErr: true,
},
{
path: "failing4/selector",
content: []byte(`helmfiles:
- path: failing4/helmfile.yaml
selectors:
- colon: not-authorized
`),
wantErr: true,
},
{
path: "failing6/selector",
content: []byte(`helmfiles:
- selectors:
- whatever
`),
wantErr: true,
},
{
path: "failing7/selector",
content: []byte(`helmfiles:
- path: foo/bar
selectors:
- foo=bar
selectorsInherited: true
`),
wantErr: true,
},
}
for _, test := range tests {
st, err := createFromYaml(test.content, test.path, DefaultEnv, logger)
if err != nil {
if test.wantErr {
continue
} else {
t.Error("unexpected error:", err)
}
}
Assert(t, cmp.DeepEqual(st.Helmfiles, test.helmfiles), "for path %v", test.path)
}
}
+16
View File
@@ -0,0 +1,16 @@
package state
type EnvironmentSpec struct {
Values []interface{} `yaml:"values,omitempty"`
Secrets []string `yaml:"secrets,omitempty"`
KubeContext string `yaml:"kubeContext,omitempty"`
// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
// under `environments.NAME.values`.
//
// Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
//
// Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
// a message about the missing file at the log-level.
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
}
+92
View File
@@ -0,0 +1,92 @@
package state
import (
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/tmpl"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"path/filepath"
)
type EnvironmentValuesLoader struct {
storage *Storage
readFile func(string) ([]byte, error)
logger *zap.SugaredLogger
remote *remote.Remote
}
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger, remote *remote.Remote) *EnvironmentValuesLoader {
return &EnvironmentValuesLoader{
storage: storage,
readFile: readFile,
logger: logger,
remote: remote,
}
}
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []interface{}) (map[string]interface{}, error) {
result := map[string]interface{}{}
for _, entry := range valuesEntries {
maps := []interface{}{}
switch strOrMap := entry.(type) {
case string:
urlOrPath := strOrMap
localPath, err := ld.remote.Locate(urlOrPath)
if err == nil {
urlOrPath = localPath
}
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
continue
}
for _, f := range files {
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
bytes, err := r.RenderToBytes(f)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, 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\n\nOffending YAML:\n%s", f, err, bytes)
}
maps = append(maps, m)
if ld.logger != nil {
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
}
}
case map[interface{}]interface{}:
maps = append(maps, strOrMap)
default:
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
}
for _, m := range maps {
// All the nested map key should be string. Otherwise we get strange errors due to that
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
// See https://github.com/roboll/helmfile/issues/677
vals, err := maputil.CastKeysToStrings(m)
if err != nil {
return nil, err
}
if err := mergo.Merge(&result, &vals, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
}
}
}
return result, nil
}
+120
View File
@@ -0,0 +1,120 @@
package state
import (
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/remote"
"go.uber.org/zap"
"io/ioutil"
"path/filepath"
"testing"
)
func newLoader() *EnvironmentValuesLoader {
log, err := zap.NewDevelopment(zap.AddStacktrace(zap.DebugLevel))
if err != nil {
panic(err)
}
sugar := log.Sugar()
storage := &Storage{
FilePath: "./helmfile.yaml",
basePath: ".",
glob: filepath.Glob,
logger: sugar,
}
readFile := func(s string) ([]byte, error) { return []byte{}, nil }
dirExists := func(d string) bool { return false }
fileExists := func(f string) bool { return false }
return NewEnvironmentValuesLoader(storage, ioutil.ReadFile, sugar, remote.NewRemote(sugar, "/tmp", readFile, dirExists, fileExists))
}
// See https://github.com/roboll/helmfile/pull/1169
func TestEnvValsLoad_SingleValuesFile(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.5.yaml"})
if err != nil {
t.Fatal(err)
}
expected := map[string]interface{}{
"affinity": map[string]interface{}{},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
// See https://github.com/roboll/helmfile/issues/1150
func TestEnvValsLoad_OverwriteNilValue_Issue1150(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.1.yaml", "testdata/values.2.yaml"})
if err != nil {
t.Fatal(err)
}
expected := map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
// See https://github.com/roboll/helmfile/issues/1154
func TestEnvValsLoad_OverwriteWithNilValue_Issue1154(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.3.yaml", "testdata/values.4.yaml"})
if err != nil {
t.Fatal(err)
}
expected := map[string]interface{}{
"components": map[string]interface{}{
"etcd-operator": map[string]interface{}{
"version": "0.10.3",
},
"prometheus": nil,
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
// See https://github.com/roboll/helmfile/issues/1168
func TestEnvValsLoad_OverwriteEmptyValue_Issue1168(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/issues/1168/addons.yaml", "testdata/issues/1168/addons2.yaml"})
if err != nil {
t.Fatal(err)
}
expected := map[string]interface{}{
"addons": map[string]interface{}{
"mychart": map[string]interface{}{
"skip": false,
"name": "mychart",
"namespace": "kube-system",
"chart": "stable/mychart",
"version": "1.0.0",
},
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf(diff)
}
}
+211
View File
@@ -0,0 +1,211 @@
package state
import (
"fmt"
"os"
"path/filepath"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/variantdev/chartify"
)
type Dependency struct {
Chart string `yaml:"chart"`
Version string `yaml:"version"`
Alias string `yaml:"alias"`
}
func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) ([]string, error) {
for _, adopt := range release.Adopt {
flags = append(flags, "--adopt", adopt)
}
return flags, nil
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
}
func directoryExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsDir()
}
type Chartify struct {
Opts *chartify.ChartifyOpts
Clean func()
}
func (st *HelmState) downloadChartWithGoGetter(r *ReleaseSpec) (string, error) {
pathElems := []string{
remote.DefaultCacheDir,
}
if r.Namespace != "" {
pathElems = append(pathElems, r.Namespace)
}
if r.KubeContext != "" {
pathElems = append(pathElems, r.KubeContext)
}
pathElems = append(pathElems, r.Name)
cacheDir := filepath.Join(pathElems...)
return st.goGetterChart(r.Chart, r.Directory, cacheDir, r.ForceGoGetter)
}
func (st *HelmState) goGetterChart(chart, dir, cacheDir string, force bool) (string, error) {
if dir != "" && chart == "" {
chart = dir
}
_, err := remote.Parse(chart)
if err != nil {
if force {
return "", fmt.Errorf("Parsing url from dir failed due to error %q.\nContinuing the process assuming this is a regular Helm chart or a local dir.", err.Error())
}
} else {
r := remote.NewRemote(st.logger, st.basePath, st.readFile, directoryExistsAt, fileExistsAt)
fetchedDir, err := r.Fetch(chart, cacheDir)
if err != nil {
return "", fmt.Errorf("fetching %q: %v", chart, err)
}
chart = fetchedDir
}
return chart, nil
}
func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSpec, chart string, workerIndex int) (*Chartify, func(), error) {
c := &Chartify{
Opts: &chartify.ChartifyOpts{
WorkaroundOutputDirIssue: true,
EnableKustomizeAlphaPlugins: true,
ChartVersion: release.Version,
Namespace: release.Namespace,
ID: ReleaseToID(release),
},
}
var filesNeedCleaning []string
clean := func() {
st.removeFiles(filesNeedCleaning)
}
var shouldRun bool
dir := filepath.Join(st.basePath, chart)
if stat, _ := os.Stat(dir); stat != nil && stat.IsDir() {
if exists, err := st.fileExists(filepath.Join(dir, "Chart.yaml")); err == nil && !exists {
shouldRun = true
}
}
for _, d := range release.Dependencies {
chart := d.Chart
if st.directoryExistsAt(chart) {
var err error
// Otherwise helm-dependency-up on the temporary chart generated by chartify ends up errors like:
// Error: directory /tmp/chartify945964195/myapp-57fb4495cf/test/integration/charts/httpbin not found]
// which is due to that the temporary chart is generated outside of the current working directory/basePath,
// and therefore the relative path in `chart` points to somewhere inexistent.
chart, err = filepath.Abs(filepath.Join(st.basePath, chart))
if err != nil {
return nil, clean, err
}
}
c.Opts.AdhocChartDependencies = append(c.Opts.AdhocChartDependencies, chartify.ChartDependency{
Alias: d.Alias,
Chart: chart,
Version: d.Version,
})
shouldRun = true
}
jsonPatches := release.JSONPatches
if len(jsonPatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
if err != nil {
return nil, clean, err
}
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
for _, f := range generatedFiles {
c.Opts.JsonPatches = append(c.Opts.JsonPatches, f)
}
shouldRun = true
}
strategicMergePatches := release.StrategicMergePatches
if len(strategicMergePatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
if err != nil {
return nil, clean, err
}
for _, f := range generatedFiles {
c.Opts.StrategicMergePatches = append(c.Opts.StrategicMergePatches, f)
}
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
shouldRun = true
}
transformers := release.Transformers
if len(transformers) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, transformers, release.MissingFileHandler)
if err != nil {
return nil, clean, err
}
for _, f := range generatedFiles {
c.Opts.Transformers = append(c.Opts.Transformers, f)
}
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
shouldRun = true
}
if release.ForceNamespace != "" {
c.Opts.OverrideNamespace = release.ForceNamespace
shouldRun = true
}
if shouldRun {
generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex)
if err != nil {
return nil, clean, err
}
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
c.Opts.ValuesFiles = generatedFiles
setFlags, err := st.setFlags(release.SetValues)
if err != nil {
return nil, clean, fmt.Errorf("rendering set value entry for release %s: %v", release.Name, err)
}
c.Opts.SetFlags = setFlags
c.Opts.TemplateData = st.newReleaseTemplateData(release)
c.Opts.TemplateFuncs = st.newReleaseTemplateFuncMap(dir)
return c, clean, nil
}
return nil, clean, nil
}
+211
View File
@@ -0,0 +1,211 @@
package state
import (
"fmt"
"github.com/roboll/helmfile/pkg/tmpl"
"gopkg.in/yaml.v2"
)
func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*ReleaseSpec, error) {
var result *ReleaseSpec
var err error
result, err = r.Clone()
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\": %v", r.Name, err)
}
{
ts := result.Name
result.Name, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".name = \"%s\": %v", r.Name, ts, err)
}
}
{
ts := result.Chart
result.Chart, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".chart = \"%s\": %v", r.Name, ts, err)
}
}
{
ts := result.Namespace
result.Namespace, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".namespace = \"%s\": %v", r.Name, ts, err)
}
}
{
ts := result.Version
result.Version, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
}
}
if result.WaitTemplate != nil {
ts := *result.WaitTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
}
result.WaitTemplate = &resultTmpl
}
if result.InstalledTemplate != nil {
ts := *result.InstalledTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
}
result.InstalledTemplate = &resultTmpl
}
if result.TillerlessTemplate != nil {
ts := *result.TillerlessTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
}
result.TillerlessTemplate = &resultTmpl
}
if result.VerifyTemplate != nil {
ts := *result.VerifyTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
}
result.VerifyTemplate = &resultTmpl
}
for key, val := range result.Labels {
ts := val
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".labels[%s] = \"%s\": %v", r.Name, key, ts, err)
}
result.Labels[key] = s.String()
}
if len(result.ValuesTemplate) > 0 {
for i, t := range result.ValuesTemplate {
switch ts := t.(type) {
case map[interface{}]interface{}:
serialized, err := yaml.Marshal(ts)
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
}
s, err := renderer.RenderTemplateContentToBuffer([]byte(serialized))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, serialized, err)
}
var deserialized map[interface{}]interface{}
if err := yaml.Unmarshal(s.Bytes(), &deserialized); err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
}
result.ValuesTemplate[i] = deserialized
}
}
var newvals []interface{}
newvals = append(newvals, result.ValuesTemplate...)
result.Values = append(newvals, result.Values...)
}
for i, t := range result.Values {
switch ts := t.(type) {
case string:
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%s\": %v", r.Name, i, ts, err)
}
result.Values[i] = s.String()
}
}
for i, t := range result.Secrets {
switch ts := t.(type) {
case string:
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
}
result.Secrets[i] = s.String()
}
}
if len(result.SetValuesTemplate) > 0 {
for i, val := range result.SetValuesTemplate {
{
// name
ts := val.Name
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].name = \"%s\": %v", r.Name, i, ts, err)
}
result.SetValuesTemplate[i].Name = s.String()
}
{
// value
ts := val.Value
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].value = \"%s\": %v", r.Name, i, ts, err)
}
result.SetValuesTemplate[i].Value = s.String()
}
{
// file
ts := val.File
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].file = \"%s\": %v", r.Name, i, ts, err)
}
result.SetValuesTemplate[i].File = s.String()
}
for j, ts := range val.Values {
// values
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
}
result.SetValuesTemplate[i].Values[j] = s.String()
}
}
var newvals []SetValue
newvals = append(newvals, result.SetValuesTemplate...)
result.SetValues = append(newvals, result.SetValues...)
}
return result, nil
}
func (r ReleaseSpec) Clone() (*ReleaseSpec, error) {
serialized, err := yaml.Marshal(r)
if err != nil {
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
}
var deserialized ReleaseSpec
if err := yaml.Unmarshal(serialized, &deserialized); err != nil {
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
}
return &deserialized, nil
}
func (r ReleaseSpec) Desired() bool {
return r.Installed == nil || *r.Installed
}
+31
View File
@@ -0,0 +1,31 @@
package state
import (
"fmt"
)
const ReleaseErrorCodeFailure = 1
type ReleaseError struct {
*ReleaseSpec
err error
Code int
}
func (e *ReleaseError) Error() string {
return e.err.Error()
}
func NewReleaseError(release *ReleaseSpec, err error, code int) *ReleaseError {
return &ReleaseError{
ReleaseSpec: release,
err: err,
Code: code,
}
}
func newReleaseFailedError(release *ReleaseSpec, err error) *ReleaseError {
wrappedErr := fmt.Errorf("failed processing release %s: %v", release.Name, err.Error())
return NewReleaseError(release, wrappedErr, ReleaseErrorCodeFailure)
}
@@ -38,7 +38,7 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
return true
} else if rVal == v {
return false
}
+97
View File
@@ -0,0 +1,97 @@
package state
import (
"github.com/google/go-cmp/cmp"
"testing"
)
func TestSelectReleasesWithOverrides(t *testing.T) {
type testcase struct {
subject string
selector []string
want []string
}
testcases := []testcase{
{
subject: "multiple OR selectors (nillable label first)",
selector: []string{"type=bar", "name=nolabel2", "name=nolabel1"},
want: []string{"nolabel1", "nolabel2", "foo"},
},
{
subject: "multiple OR selectors (non-nillable label first)",
selector: []string{"name=foo", "type!=bar"},
want: []string{"nolabel1", "nolabel2", "foo"},
},
{
subject: "multiple AND conditions (nillable label first)",
selector: []string{"type!=bar,name!=nolabel2"},
want: []string{"nolabel1"},
},
{
subject: "multiple AND conditions (non-nillable label first)",
selector: []string{"name!=nolabel2,type!=bar"},
want: []string{"nolabel1"},
},
{
subject: "inequality on nillable label",
selector: []string{"type!=bar"},
want: []string{"nolabel1", "nolabel2"},
},
{
subject: "equality on nillable label",
selector: []string{"type=bar"},
want: []string{"foo"},
},
{
subject: "inequality on non-nillable label",
selector: []string{"name!=nolabel1"},
want: []string{"nolabel2", "foo"},
},
{
subject: "equality on non-nillable label",
selector: []string{"name=nolabel1"},
want: []string{"nolabel1"},
},
}
example := []byte(`releases:
- name: nolabel1
namespace: kube-system
chart: stable/nolabel
- name: nolabel2
namespace: default
chart: stable/nolabel
- name: foo
namespace: kube-system
chart: stable/foo
labels:
type: bar
`)
state := stateTestEnv{
Files: map[string]string{
"/helmfile.yaml": string(example),
},
WorkDir: "/",
}.MustLoadState(t, "/helmfile.yaml", "default")
for _, tc := range testcases {
state.Selectors = tc.selector
rs, err := state.GetSelectedReleasesWithOverrides()
if err != nil {
t.Fatalf("%s %s: %v", tc.selector, tc.subject, err)
}
var got []string
for _, r := range rs {
got = append(got, r.Name)
}
if d := cmp.Diff(tc.want, got); d != "" {
t.Errorf("%s %s: %s", tc.selector, tc.subject, d)
}
}
}
+3248
View File
File diff suppressed because it is too large Load Diff
+127
View File
@@ -0,0 +1,127 @@
package state
import (
"fmt"
"reflect"
"github.com/roboll/helmfile/pkg/tmpl"
"gopkg.in/yaml.v2"
)
func (st *HelmState) Values() map[string]interface{} {
if st.RenderedValues == nil {
panic("[bug] RenderedValues is nil")
}
return st.RenderedValues
}
func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]interface{}) releaseTemplateData {
tmplData := releaseTemplateData{
Environment: st.Env,
KubeContext: st.OverrideKubeContext,
Namespace: st.OverrideNamespace,
Chart: st.OverrideChart,
Values: vals,
Release: releaseTemplateDataRelease{
Name: release.Name,
Chart: release.Chart,
Namespace: release.Namespace,
Labels: release.Labels,
KubeContext: release.KubeContext,
},
}
tmplData.StateValues = &tmplData.Values
return tmplData
}
func getBoolRefFromStringTemplate(templateRef string) (*bool, error) {
var result bool
if err := yaml.Unmarshal([]byte(templateRef), &result); err != nil {
return nil, fmt.Errorf("failed deserialising string %s: %v", templateRef, err)
}
return &result, nil
}
func updateBoolTemplatedValues(r *ReleaseSpec) error {
if r.InstalledTemplate != nil {
if installed, err := getBoolRefFromStringTemplate(*r.InstalledTemplate); err != nil {
return fmt.Errorf("installedTemplate: %v", err)
} else {
r.InstalledTemplate = nil
r.Installed = installed
}
}
if r.WaitTemplate != nil {
if wait, err := getBoolRefFromStringTemplate(*r.WaitTemplate); err != nil {
return fmt.Errorf("waitTemplate: %v", err)
} else {
r.WaitTemplate = nil
r.Wait = wait
}
}
if r.TillerlessTemplate != nil {
if tillerless, err := getBoolRefFromStringTemplate(*r.TillerlessTemplate); err != nil {
return fmt.Errorf("tillerlessTemplate: %v", err)
} else {
r.TillerlessTemplate = nil
r.Tillerless = tillerless
}
}
if r.VerifyTemplate != nil {
if verify, err := getBoolRefFromStringTemplate(*r.VerifyTemplate); err != nil {
return fmt.Errorf("verifyTemplate: %v", err)
} else {
r.VerifyTemplate = nil
r.Verify = verify
}
}
return nil
}
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
r := *st
vals := st.Values()
for i, rt := range st.Releases {
if rt.KubeContext == "" {
rt.KubeContext = r.HelmDefaults.KubeContext
}
if rt.Labels == nil {
rt.Labels = map[string]string{}
}
for k, v := range st.CommonLabels {
rt.Labels[k] = v
}
successFlag := false
for it, prev := 0, &rt; it < 6; it++ {
tmplData := st.createReleaseTemplateData(prev, vals)
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
r, err := rt.ExecuteTemplateExpressions(renderer)
if err != nil {
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
}
if reflect.DeepEqual(prev, r) {
successFlag = true
if err := updateBoolTemplatedValues(r); err != nil {
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
}
st.Releases[i] = *r
break
}
prev = r
}
if !successFlag {
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %s", st.FilePath, rt.Name,
"recursive references can't be resolved")
}
}
return &r, nil
}
+262
View File
@@ -0,0 +1,262 @@
package state
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/roboll/helmfile/pkg/environment"
)
func boolPtrToString(ptr *bool) string {
if ptr == nil {
return "<nil>"
}
return fmt.Sprintf("&%t", *ptr)
}
func ptr(v interface{}) interface{} {
r := v
return reflect.ValueOf(r).Addr().Interface()
}
func TestHelmState_executeTemplates(t *testing.T) {
tests := []struct {
name string
input ReleaseSpec
want ReleaseSpec
}{
{
name: "Has template expressions in chart, values, secrets, version, labels",
input: ReleaseSpec{
Chart: "test-charts/{{ .Release.Name }}",
Version: "{{ .Release.Name }}-0.1",
Name: "test-app",
Namespace: "test-namespace-{{ .Release.Name }}",
ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
Secrets: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
Labels: map[string]string{"id": "{{ .Release.Name }}"},
},
want: ReleaseSpec{
Chart: "test-charts/test-app",
Version: "test-app-0.1",
Name: "test-app",
Namespace: "test-namespace-test-app",
Values: []interface{}{"config/test_env/test-app/values.yaml"},
Secrets: []interface{}{"config/test_env/test-app/secrets.yaml"},
Labels: map[string]string{"id": "test-app"},
},
},
{
name: "Has template expressions in name with recursive refs",
input: ReleaseSpec{
Chart: "test-chart",
Name: "{{ .Release.Labels.id }}-{{ .Release.Namespace }}",
Namespace: "dev",
Labels: map[string]string{"id": "{{ .Release.Chart }}"},
},
want: ReleaseSpec{
Chart: "test-chart",
Name: "test-chart-dev",
Namespace: "dev",
Labels: map[string]string{"id": "test-chart"},
},
},
{
name: "Has template expressions in boolean values",
input: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
Labels: map[string]string{"id": "app"},
InstalledTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "yes" "no" }}`),
VerifyTemplate: func(i string) *string { return &i }(`{{ true }}`),
Verify: func(i bool) *bool { return &i }(false),
WaitTemplate: func(i string) *string { return &i }(`{{ false }}`),
TillerlessTemplate: func(i string) *string { return &i }(`yes`),
},
want: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
Labels: map[string]string{"id": "app"},
Installed: func(i bool) *bool { return &i }(true),
Verify: func(i bool) *bool { return &i }(true),
Wait: func(i bool) *bool { return &i }(false),
Tillerless: func(i bool) *bool { return &i }(true),
},
},
{
name: "Has template in set-values",
input: ReleaseSpec{
Chart: "test-charts/chart",
Name: "test-app",
Namespace: "dev",
SetValuesTemplate: []SetValue{
SetValue{Name: "val1", Value: "{{ .Release.Name }}-val1"},
SetValue{Name: "val2", File: "{{ .Release.Name }}.yml"},
SetValue{Name: "val3", Values: []string{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
},
},
want: ReleaseSpec{
Chart: "test-charts/chart",
Name: "test-app",
Namespace: "dev",
SetValues: []SetValue{
SetValue{Name: "val1", Value: "test-app-val1"},
SetValue{Name: "val2", File: "test-app.yml"},
SetValue{Name: "val3", Values: []string{"test-app-val2", "test-app-val3"}},
},
},
},
{
name: "Has template in values (map)",
input: ReleaseSpec{
Chart: "test-charts/chart",
Verify: nil,
Name: "app",
Namespace: "dev",
ValuesTemplate: []interface{}{map[string]string{"key": "{{ .Release.Name }}-val0"}},
},
want: ReleaseSpec{
Chart: "test-charts/chart",
Verify: nil,
Name: "app",
Namespace: "dev",
Values: []interface{}{map[interface{}]interface{}{"key": "app-val0"}},
},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
},
RenderedValues: map[string]interface{}{},
}
r, err := state.ExecuteTemplates()
if err != nil {
t.Errorf("Unexpected error: %v", err)
t.FailNow()
}
actual := r.Releases[0]
if !reflect.DeepEqual(actual.Name, tt.want.Name) {
t.Errorf("expected Name %+v, got %+v", tt.want.Name, actual.Name)
}
if !reflect.DeepEqual(actual.Chart, tt.want.Chart) {
t.Errorf("expected Chart %+v, got %+v", tt.want.Chart, actual.Chart)
}
if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) {
t.Errorf("expected Namespace %+v, got %+v", tt.want.Namespace, actual.Namespace)
}
if diff := deep.Equal(actual.Values, tt.want.Values); diff != nil && len(actual.Values) > 0 {
t.Errorf("Values differs \n%+v", strings.Join(diff, "\n"))
}
if diff := deep.Equal(actual.Secrets, tt.want.Secrets); diff != nil && len(actual.Secrets) > 0 {
t.Errorf("Secrets differs \n%+v", strings.Join(diff, "\n"))
}
if diff := deep.Equal(actual.SetValues, tt.want.SetValues); diff != nil && len(actual.SetValues) > 0 {
t.Errorf("SetValues differs \n%+v", strings.Join(diff, "\n"))
}
if diff := deep.Equal(actual.Labels, tt.want.Labels); diff != nil && len(actual.Labels) > 0 {
t.Errorf("Labels differs \n%+v", strings.Join(diff, "\n"))
}
if !reflect.DeepEqual(actual.Version, tt.want.Version) {
t.Errorf("expected Version %+v, got %+v", tt.want.Version, actual.Version)
}
if !reflect.DeepEqual(actual.Installed, tt.want.Installed) {
t.Errorf("expected actual.Installed %+v, got %+v",
boolPtrToString(tt.want.Installed), boolPtrToString(actual.Installed),
)
}
if !reflect.DeepEqual(actual.Tillerless, tt.want.Tillerless) {
t.Errorf("expected actual.Tillerless %+v, got %+v",
boolPtrToString(tt.want.Tillerless), boolPtrToString(actual.Tillerless),
)
}
if !reflect.DeepEqual(actual.Verify, tt.want.Verify) {
t.Errorf("expected actual.Verify %+v, got %+v",
boolPtrToString(tt.want.Verify), boolPtrToString(actual.Verify),
)
}
if !reflect.DeepEqual(actual.Wait, tt.want.Wait) {
t.Errorf("expected actual.Wait %+v, got %+v",
boolPtrToString(tt.want.Wait), boolPtrToString(actual.Wait),
)
}
})
}
}
func TestHelmState_recursiveRefsTemplates(t *testing.T) {
tests := []struct {
name string
input ReleaseSpec
}{
{
name: "Has reqursive references",
input: ReleaseSpec{
Chart: "test-charts/{{ .Release.Name }}",
Verify: nil,
Name: "{{ .Release.Labels.id }}",
Namespace: "dev",
Labels: map[string]string{"id": "app-{{ .Release.Name }}"},
},
},
{
name: "Has unresolvable boolean templates",
input: ReleaseSpec{
Name: "app-dev",
Chart: "test-charts/app",
Verify: nil,
Namespace: "dev",
WaitTemplate: func(i string) *string { return &i }("hi"),
},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
},
RenderedValues: map[string]interface{}{},
}
r, err := state.ExecuteTemplates()
if err == nil {
t.Errorf("Expected error, got valid response: %v", r)
t.FailNow()
}
})
}
}
+61
View File
@@ -0,0 +1,61 @@
package state
import (
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/helmexec"
"io/ioutil"
"os"
"testing"
)
func TestGoGetter(t *testing.T) {
logger := helmexec.NewLogger(os.Stderr, "warn")
testcases := []struct {
chart, dir string
force bool
out, err string
}{
{
chart: "raw/incubator",
dir: "",
force: false,
out: "raw/incubator",
err: "",
},
}
for i, tc := range testcases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
d, err := ioutil.TempDir("", "testgogetter")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
st := &HelmState{
logger: logger,
readFile: ioutil.ReadFile,
basePath: d,
}
out, err := st.goGetterChart(tc.chart, tc.dir, "", false)
if diff := cmp.Diff(tc.out, out); diff != "" {
t.Fatalf("Unexpected out:\n%s", diff)
}
var errMsg string
if err != nil {
errMsg = err.Error()
}
if diff := cmp.Diff(tc.err, errMsg); diff != "" {
t.Fatalf("Unexpected err:\n%s", diff)
}
})
}
}
+257
View File
@@ -0,0 +1,257 @@
package state
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/variantdev/dag/pkg/dag"
)
type result struct {
release ReleaseSpec
err error
}
func (st *HelmState) scatterGather(concurrency int, items int, produceInputs func(), receiveInputsAndProduceIntermediates func(int), aggregateIntermediates func()) {
if concurrency < 1 || concurrency > items {
concurrency = items
}
for _, r := range st.Releases {
if r.Tillerless != nil {
if *r.Tillerless {
concurrency = 1
}
} else if st.HelmDefaults.Tillerless {
concurrency = 1
}
}
// WaitGroup is required to wait until goroutine per job in job queue cleanly stops.
var waitGroup sync.WaitGroup
waitGroup.Add(concurrency)
go produceInputs()
for w := 1; w <= concurrency; w++ {
go func(id int) {
receiveInputsAndProduceIntermediates(id)
waitGroup.Done()
}(w)
}
aggregateIntermediates()
// Wait until all the goroutines to gracefully finish
waitGroup.Wait()
}
func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency int,
do func(ReleaseSpec, int) error) []error {
return st.iterateOnReleases(helm, concurrency, st.Releases, do)
}
func (st *HelmState) iterateOnReleases(helm helmexec.Interface, concurrency int, inputs []ReleaseSpec,
do func(ReleaseSpec, int) error) []error {
var errs []error
inputsSize := len(inputs)
releases := make(chan ReleaseSpec)
results := make(chan result)
st.scatterGather(
concurrency,
inputsSize,
func() {
for _, release := range inputs {
releases <- release
}
close(releases)
},
func(id int) {
for release := range releases {
err := do(release, id)
st.logger.Debugf("release %q processed", release.Name)
results <- result{release: release, err: err}
}
},
func() {
for range inputs {
r := <-results
if r.err != nil {
errs = append(errs, fmt.Errorf("release \"%s\" failed: %v", r.release.Name, r.err))
}
}
},
)
if len(errs) != 0 {
return errs
}
return nil
}
type PlanOptions struct {
Reverse bool
IncludeNeeds bool
SkipNeeds bool
SelectedReleases []ReleaseSpec
}
func (st *HelmState) PlanReleases(opts PlanOptions) ([][]Release, error) {
marked, err := st.SelectReleasesWithOverrides()
if err != nil {
return nil, err
}
groups, err := SortedReleaseGroups(marked, opts)
if err != nil {
return nil, err
}
return groups, nil
}
func SortedReleaseGroups(releases []Release, opts PlanOptions) ([][]Release, error) {
reverse := opts.Reverse
groups, err := GroupReleasesByDependency(releases, opts)
if err != nil {
return nil, err
}
if reverse {
for i, j := 0, len(groups)-1; i < j; i, j = i+1, j-1 {
groups[i], groups[j] = groups[j], groups[i]
}
}
return groups, nil
}
func GroupReleasesByDependency(releases []Release, opts PlanOptions) ([][]Release, error) {
idToReleases := map[string][]Release{}
idToIndex := map[string]int{}
d := dag.New()
for i, r := range releases {
id := ReleaseToID(&r.ReleaseSpec)
idToReleases[id] = append(idToReleases[id], r)
idToIndex[id] = i
// Only compute dependencies from non-filtered releases
if !r.Filtered {
var needs []string
for i := 0; i < len(r.Needs); i++ {
n := r.Needs[i]
needs = append(needs, n)
}
d.Add(id, dag.Dependencies(needs))
}
}
var ids []string
for id := range idToReleases {
ids = append(ids, id)
}
var selectedReleaseIDs []string
for _, r := range opts.SelectedReleases {
id := ReleaseToID(&r)
selectedReleaseIDs = append(selectedReleaseIDs, id)
}
plan, err := d.Plan(dag.SortOptions{
Only: selectedReleaseIDs,
WithDependencies: opts.IncludeNeeds,
WithoutDependencies: opts.SkipNeeds,
})
if err != nil {
if ude, ok := err.(*dag.UnhandledDependencyError); ok {
msgs := make([]string, len(ude.UnhandledDependencies))
for i, ud := range ude.UnhandledDependencies {
id := ud.Id
ds := make([]string, len(ud.Dependents))
for i, d := range ud.Dependents {
ds[i] = fmt.Sprintf("%q", d)
}
var dsHumanized string
if len(ds) < 3 {
dsHumanized = strings.Join(ds, " and ")
} else {
dsHumanized = strings.Join(ds[:len(ds)-1], ", ")
dsHumanized += ", and " + ds[len(ds)-1]
}
var verb string
if len(ds) == 1 {
verb = "depends"
} else {
verb = "depend"
}
idComponents := strings.Split(id, "/")
name := idComponents[len(idComponents)-1]
msg := fmt.Sprintf(
"release %s %s on %q which does not match the selectors. "+
"Please add a selector like \"--selector name=%s\", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies",
dsHumanized,
verb,
id,
name,
)
msgs[i] = msg
}
return nil, errors.New(msgs[0])
}
return nil, err
}
var result [][]Release
for groupIndex := 0; groupIndex < len(plan); groupIndex++ {
dagNodesInGroup := plan[groupIndex]
var idsInGroup []string
var releasesInGroup []Release
for _, node := range dagNodesInGroup {
idsInGroup = append(idsInGroup, node.Id)
}
// Make the helmfile behavior deterministic for reproducibility and ease of testing
// We try to keep the order of definitions to keep backward-compatibility
// See https://github.com/roboll/helmfile/issues/988
sort.Slice(idsInGroup, func(i, j int) bool {
ii := idToIndex[idsInGroup[i]]
ij := idToIndex[idsInGroup[j]]
return ii < ij
})
for _, id := range idsInGroup {
rs, ok := idToReleases[id]
if !ok {
panic(fmt.Errorf("bug: unexpectedly failed to get releases for id %q: %v", id, ids))
}
releasesInGroup = append(releasesInGroup, rs...)
}
result = append(result, releasesInGroup)
}
return result, nil
}
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
package state
import (
"fmt"
"go.uber.org/zap"
"net/url"
"path/filepath"
"sort"
)
type Storage struct {
logger *zap.SugaredLogger
FilePath string
basePath string
glob func(string) ([]string, error)
}
func NewStorage(forFile string, logger *zap.SugaredLogger, glob func(string) ([]string, error)) *Storage {
return &Storage{
FilePath: forFile,
basePath: filepath.Dir(forFile),
logger: logger,
glob: glob,
}
}
func (st *Storage) resolveFile(missingFileHandler *string, tpe, path string) ([]string, bool, error) {
title := fmt.Sprintf("%s file", tpe)
files, err := st.ExpandPaths(path)
if err != nil {
return nil, false, err
}
var handlerId string
if missingFileHandler != nil {
handlerId = *missingFileHandler
} else {
handlerId = MissingFileHandlerError
}
if len(files) == 0 {
switch handlerId {
case MissingFileHandlerError:
return nil, false, fmt.Errorf("%s matching \"%s\" does not exist in \"%s\"", title, path, st.basePath)
case MissingFileHandlerWarn:
st.logger.Warnf("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
case MissingFileHandlerInfo:
st.logger.Infof("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
case MissingFileHandlerDebug:
st.logger.Debugf("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
default:
available := []string{
MissingFileHandlerError,
MissingFileHandlerWarn,
MissingFileHandlerInfo,
MissingFileHandlerDebug,
}
return nil, false, fmt.Errorf("invalid missing file handler \"%s\" while processing \"%s\" in \"%s\": it must be one of %s", handlerId, path, st.FilePath, available)
}
}
return files, false, nil
}
func (st *Storage) ExpandPaths(globPattern string) ([]string, error) {
result := []string{}
absPathPattern := st.normalizePath(globPattern)
matches, err := st.glob(absPathPattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
result = append(result, matches...)
return result, nil
}
// normalizes relative path to absolute one
func (st *Storage) normalizePath(path string) string {
u, _ := url.Parse(path)
if u.Scheme != "" || filepath.IsAbs(path) {
return path
} else {
return st.JoinBase(path)
}
}
// JoinBase returns an absolute path in the form basePath/relative
func (st *Storage) JoinBase(relPath string) string {
return filepath.Join(st.basePath, relPath)
}
+90
View File
@@ -0,0 +1,90 @@
package state
import (
"errors"
"fmt"
"hash/fnv"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/davecgh/go-spew/spew"
)
func createTempValuesFile(release *ReleaseSpec, data interface{}) (*os.File, error) {
p, err := tempValuesFilePath(release, data)
if err != nil {
return nil, err
}
f, err := os.Create(*p)
if err != nil {
return nil, err
}
return f, nil
}
func tempValuesFilePath(release *ReleaseSpec, data interface{}) (*string, error) {
id, err := generateValuesID(release, data)
if err != nil {
panic(err)
}
workDir := os.Getenv("HELMFILE_TEMPDIR")
if workDir == "" {
workDir, err = ioutil.TempDir(os.TempDir(), "helmfile")
if err != nil {
panic(err)
}
}
d := filepath.Join(workDir, id)
info, err := os.Stat(d)
if err != nil && !errors.Is(err, os.ErrNotExist) {
panic(err)
} else if info == nil {
}
return &d, nil
}
func generateValuesID(release *ReleaseSpec, data interface{}) (string, error) {
var id []string
if release.Namespace != "" {
id = append(id, release.Namespace)
}
id = append(id, release.Name, "values")
hash, err := HashObject([]interface{}{release, data})
if err != nil {
return "", err
}
id = append(id, hash)
return strings.Join(id, "-"), nil
}
func HashObject(obj interface{}) (string, error) {
hash := fnv.New32a()
hash.Reset()
printer := spew.ConfigState{
Indent: " ",
SortKeys: true,
DisableMethods: true,
SpewKeys: true,
}
printer.Fprintf(hash, "%#v", obj)
sum := fmt.Sprint(hash.Sum32())
return SafeEncodeString(sum), nil
}
+33
View File
@@ -0,0 +1,33 @@
/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// See k8s.io/apimachinery/pkg/util/rand/rand.go
package state
const (
// We omit vowels from the set of available characters to reduce the chances
// of "bad words" being formed.
alphanums = "bcdfghjklmnpqrstvwxz2456789"
)
// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and
// ensures that strings generated from hash functions appear consistent throughout the API.
func SafeEncodeString(s string) string {
r := make([]byte, len(s))
for i, b := range []rune(s) {
r[i] = alphanums[(int(b) % len(alphanums))]
}
return string(r)
}

Some files were not shown because too many files have changed in this diff Show More