Compare commits

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

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

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

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

**Why an another command?**

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

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

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

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

Resolves #511

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

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

Resolves #465
Fixes #427
Fixes #357
Ref #460
2019-03-29 12:38:16 +09:00
KUOKA Yusuke 870d33f418 fix: helmfile delete should not stop on uninstalled release(s) (#509)
Resolves #481
2019-03-29 10:11:19 +09:00
KUOKA Yusuke d3c5417177 fix: prevent confusing error messages when installed: false (#508)
This removes `release: "your_release_name" not found` errors seen for releases with `installed: false` when running `helmfile sync` and `helmfile apply`.

The problem was that helmfile had been running `helm status` to detect releases to be deleted. helmfile now use `helm list ^YOUR_RELEASE_NAME$` to detect if the release is currently installed or not, which emits no error-like logs on against uninstalled releases.

Fixes #507

Fixes #507
2019-03-28 18:14:29 +09:00
KUOKA Yusuke 0639714136 feat: tpl template function (#504)
Add `tpl` template function that looks exactly like helm's one.

Resolves #420
2019-03-25 18:08:47 +09:00
sgandon 056d150856 doc(readme): changed from chart to helmfile (#501) 2019-03-22 21:35:42 +09:00
Yusuke KUOKA 9bcec38b3b fix sha256 checksum of the helm release archive, for successful helmfile release 2019-03-20 17:57:46 +09:00
Yusuke KUOKA 3e40e90a2a releasing: automatically checkout the master branch back 2019-03-20 17:51:47 +09:00
KUOKA Yusuke acbb416028 fix: absolute paths to helmfiles should work (#498)
Fixes #488
2019-03-20 17:45:48 +09:00
Mike Eves c4eb95f3ec Fix: Release options should override helm defaults (#497)
Options specified in releases (e.g. `recreatePods`) should override the respective options set in `helmDefaults`. Currently, `helmDefaults` takes precedence.

In the below example, `--force` should not be passed as an additional deployment argument:
```
helmDefaults:
  force: true

releases:
  - name: example
    namespace: example
    chart: some/repo
    version: ~1.24.1
    force: false
```

Fixes #492
2019-03-20 11:22:26 +09:00
Mike Eves f2996e2452 Support --atomic as a first class directive in helmfile (#491)
Resolves #487
2019-03-19 19:17:26 +09:00
KUOKA Yusuke 79ae2df71f fix: Always getting deprecated warning (#493)
Fixes #485
2019-03-19 19:15:37 +09:00
Dan O'Brien 8f7ab740bd minor typo (#496) 2019-03-19 19:14:52 +09:00
KUOKA Yusuke 841580f8b3 fix: helmfile sync should NOT continue when secret decryption fails (#490)
Fixes #336
2019-03-15 10:36:24 +09:00
kinzal c6c54fdbed fixes read absolute path woth readFile (#475) 2019-02-15 12:49:35 +09:00
KUOKA Yusuke 35fd5ff117 fix: remote file provided to set.file should not break helmfile (#474)
Fixes #473
2019-02-14 18:13:32 +09:00
KUOKA Yusuke f2bdcb9113 fix: sync should not hang on installed: false (#464)
Fixes #423
2019-02-04 17:16:58 +09:00
Yusuke KUOKA 9cfd6aeae7 Ensure helm-diff is installed for integration testing 2019-02-04 17:13:17 +09:00
Yusuke KUOKA 27da8a6b7a fix integration test 2019-02-04 15:15:56 +09:00
KUOKA Yusuke c6236a15bb feat: Opt-out dep-builds and repo-updates (#463)
* Improve code organization

To make sure it is still readable after upcoming changes to helmfile

* feat: `helmfile deps` to update dependencies of all the local charts

Resolves #450

* feat: helmfile updates repos and build deps by default

But not update deps. Use `helmfile deps` to update deps, and provide `--skip-deps` to skip updating repos and builds deps in sync/diff/apply/template

Resolves #415

* Improve integration test coverage
2019-02-04 12:07:25 +09:00
KUOKA Yusuke b894012c80 feat: Automatically update repos on diffing by default (#462)
Resolves #452
2019-02-04 11:12:34 +09:00
Shane Starcher fb256b0161 support labels for namespace and chart (#459) 2019-01-31 11:25:29 +09:00
Simon Li abeccc5e8e Fix indentation in writing-helmfile.md example (#454) 2019-01-25 23:32:33 +09:00
KUOKA Yusuke f84ed93754 fix: helmfle should not break when run without -f (#453)
Fixes #451
2019-01-25 12:13:56 +09:00
KUOKA Yusuke bf42d2519d fix: Relative path in helmfile not relative to file in multi file setup (#448)
Fixes #431
2019-01-24 10:48:01 +09:00
Maxim Mironenko 99ce8570c7 helmfiles examples link URL fix (#447)
* helmfiles examples link URL fix

* Update README.md

Co-Authored-By: maximmi <mmironenko@chanzuckerberg.com>
2019-01-24 10:46:59 +09:00
KUOKA Yusuke 571f351a8f feat: configurable concurrency for helmfile test (#442)
`helmfile test --concurency N` to set a concurrency number.
It is automatically ceiled at the number of releases just to reduce wasting computing resources.

Also, I've refactored the scatter-gather logic scattered across the code-base.

Resolves #433
2019-01-23 15:46:24 +09:00
KUOKA Yusuke b9a097ed14 fix: first-pass rendering of helmfile.yaml (#441)
There was a regression introduced by #439 that broke the first-pass rendering completely. This fixes that.
2019-01-22 15:55:03 +09:00
Yusuke KUOKA 344c20d790 release: fix make tasks for auto-tagging 2019-01-22 11:25:55 +09:00
KUOKA Yusuke 5f52f96709 fix missingFileHandler (#440)
- Fix panics when `missingFileHandler` is not defined
- Fix `missingFileHandler: Error` had been writing errors at the Warn level
- Add `Info` and `Debug` as valid values

Ref https://github.com/roboll/helmfile/pull/439#issuecomment-456155521
2019-01-22 11:22:53 +09:00
Yusuke KUOKA 171eacfb3c release: fix hack/semtag when there are one or more git remotes before origin 2019-01-22 01:29:28 +09:00
Yusuke KUOKA 7d976e5271 Semi-automate minor/patch version tagging 2019-01-22 01:23:22 +09:00
KUOKA Yusuke f813ac2642 feat: Release Template (#439)
This feature is supposed to help advanced use-cases like Conventional Directory Structure explained in several issues like #428.

Newly added configuration keys `templates`, `missingFileHandler`, and the ability to defer executing template expressions in `values`, `secrets`, `namespace`, and `chart` of releases allows you to abstract away repetitions into a reusable template:

```yaml
templates:
  default: &default
    missingFileHandler: Warn
    namespace: "{{`{{ .Release.Name }}`}}"
    chart: stable/{{`{{ .Release.Name }}`}}
    values:
    - config/{{`{{ .Release.Name }}`}}/values.yaml
    - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml
    secrets:
    - config/{{`{{ .Release.Name }}`}}/secrets.yaml
    - config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml

releases:
- name: envoy
  <<: *default
```

See the updated documentation for more details.

Resolves #428
2019-01-22 01:19:07 +09:00
KUOKA Yusuke 23178b398c docs: Remove go get from the recommended installation methods (#437)
Add a contribution guide instead and note about the `go get` way there according to the discussion made in the relevant issue.

Resolves #393
2019-01-17 12:03:58 +09:00
Vincent Behar fea62032e9 fix: cleanup hook executed too early for apply command (#426)
fixes https://github.com/roboll/helmfile/issues/412

the `apply` command first runs the `diff` command, which triggers the execution of the `cleanup` hooks at the end of the `diff`.
2019-01-01 01:33:16 +09:00
Vincent Behar 1f7a8ddbe3 fix: prepare hook is not executed for diff command (#425)
fixes https://github.com/roboll/helmfile/issues/424

note that the "cleanup" hook is already executed as it should
2019-01-01 01:30:25 +09:00
Raphael Sampaio 413ab4f55b adds Homebrew as an installation method (#418) 2018-12-21 12:06:33 +09:00
Hung Tran Duc 39bc28c23f feat: configurable common path prefix for values files (#408)
Removes duplicated declaration path for cleaner helmfile, for example, this configuration.

```
- chart: projectABC
  name: {{ .Environment.Name }}-projectABC
  namespace: abc
  version: {{ .Environment.Values.projectABC.chart.version }}
  valuesPathPrefix: ../values/{{ .Environment.Name }}/
  values:
  - common.yaml
  - project-abc.yaml
  - platform.yaml
  - ...
  secrets:
  - secrets.yaml
```
2018-12-13 18:07:50 +09:00
Matteo Gazzetta 4b22d4fc78 Update Dockerfile and CircleCI (#417) 2018-12-11 07:01:23 +09:00
Eric Chin 2a35b6864c Remove extra appending of chartName for lint and template (#406)
Closes #407
2018-12-11 06:54:57 +09:00
Anatoly Rugalev 4b23213ce4 Added new installation method via AUR (#405) 2018-11-27 14:13:45 -08:00
Anatoly Rugalev 971cbcf1db Added path normalization in - set directive. (#404)
Fixes #399
2018-11-27 14:13:04 -08:00
Anatoly Rugalev d898dfbd0d Updated documentation related to deprecation of context directive (#403) 2018-11-27 14:12:40 -08:00
Anatoly Rugalev b41b44c313 Fixes #383. Deprecated context directive in favor of helmDefaults.kubeContext (#401) 2018-11-22 12:39:03 +09:00
Sam Weston b3e27db8b3 Improve documentation of Environment Secrets (#395) 2018-11-09 16:38:23 +09:00
harmjanblok 9cab7b00f6 Bump helm to v2.11.0 (#394) 2018-11-09 16:36:25 +09:00
Sam Weston bfc86de92d Correct minor spelling and grammar issues in README.md (#385) 2018-10-23 15:21:00 +09:00
Martin Mauch 5ad916003f Add instructions for installing via scoop on Windows (#372) 2018-10-16 16:22:08 +09:00
KUOKA Yusuke b0f9062e08 fix: incomplete environment values merging (#378)
Fixes #377

Ref https://github.com/imdario/mergo/pull/81
2018-10-03 22:39:37 +09:00
KUOKA Yusuke 770c3daa5f feat: get and getOrNil template funcs to allow defaulting in templates (#370)
* feat: `get` and `getOrNil` template funcs to allow defaulting in templates

Ref #357

* Add docs about missing keys and default values in templates
2018-09-28 11:44:49 +09:00
KUOKA Yusuke 6f0dc6e069 bump go to 1.11 (#376)
In go 1.11, the code formatter seems to have changed its formatting rule a bit, that resulted in `make fmt` producing changes introduced in this commit/pr.
2018-09-28 10:48:07 +09:00
KUOKA Yusuke f23a43c80c fix(ci,doc): follow up for default non-interactivity of helmfile (#375)
The integration test was failing due to that it was still using `--auto-approve` flag which was removed. The documentation was still refering to `--auto-approve` and had no explanation about the new `--interactive` flag. This fixes all these issues.

This a follow up for #368
See https://github.com/roboll/helmfile/pull/374#issuecomment-425291468 for more context.
2018-09-28 10:40:57 +09:00
David Genest 942f9a20b0 fix first pass rendering crash on syntax error (#374) 2018-09-28 10:27:11 +09:00
KUOKA Yusuke 429c42d429 doc: mark release as installed/uninstalled (#371)
Resolves #362
2018-09-27 11:17:00 +09:00
KUOKA Yusuke 7d1787d0b6 One Ctrl-C should exit helmfile gracefully (#369)
Perhaps in latest commits I unexpectedly changed it to require double Ctrl-C to actually interrupt. This fixes it so that one Ctrl-C exists helmfile.

On the way to the fix, I changed the exit-code to 128 + SIGNAL(2 for SIGINT, 15 for SIGTERM) according to common *nix commands.
2018-09-27 04:36:34 +09:00
KUOKA Yusuke 98be623701 helmfile should be non-interactive by default (#368)
Request for user confirmation only when a global `--interactive` flag is provided.

Resolves #354
2018-09-27 04:36:14 +09:00
KUOKA Yusuke 76122738c8 fix: cleanup hook commands output should be printed when debug logging enabled (#367)
There was a race condition due to that helmfile had not been waiting for goroutines to exit

Ref #363
2018-09-27 03:21:58 +09:00
KUOKA Yusuke 973d47bfc0 fix: sync repos always failing (#366) 2018-09-27 02:32:56 +09:00
KUOKA Yusuke 2eb13c25c5 fix: helmfile should pass release names to helm template (#365)
Fixes #360
2018-09-27 02:22:18 +09:00
KUOKA Yusuke b94265122f feat: least frequent repository update (#356)
Prevents helmfile from consuming unnecessarily much time in running `helm repo update` over and over.

helmfile now marks which repository was updated, and skip second and further `helm repo update` when all the `repositories` found in a helmfile.yaml was marked as already updated.

Let's say you had two helmfiles, the first one with repositories `foo` and `bar`, an the second one with only `bar`. `helmfile repos` will run `helm update repo` for the first helmfile, marking `foo` and `bar` as already updated. The second helmfile.yaml contains only `bar`, which is marked as already updated. So helmfile won't run `helm repo update` for the second.

This applies to all the helmfile command that results in `helm repo update`, like `repos`, `sync`, `diff` and so on.

Resolves #335
2018-09-27 02:10:11 +09:00
KUOKA Yusuke f2b610afdf feat: helmfile repos updates all repos regardless of selector and releases (#353)
Resolves #338
2018-09-27 01:44:05 +09:00
KUOKA Yusuke 6cba77d4f2 fix(hook): event hooks should run on remote charts, too (#364)
And fixed the bug that "diff" was not triggering "prepare" hooks.

Ref #363
2018-09-27 01:01:25 +09:00
KUOKA Yusuke b9de22b256 feat: prepare and cleanup release event hooks (#349)
Resolves #295
Resolves #330
Resolves #329 (Supports templating of only `releases[].hooks[].command` and `args` right now
Resolves #324
2018-09-21 10:35:12 +09:00
KUOKA Yusuke 4da13b44ee fix: --helm-binary does not work with sync when updating repo (#352)
Fixes #350
2018-09-20 09:31:16 +09:00
KUOKA Yusuke 5d1a92d3a5 fix: Forbid extraneous arguments (#348)
Resolves #296
2018-09-20 09:12:12 +09:00
KUOKA Yusuke 9808849986 feat: Merge multiple docs inside a single helmfile.yaml (#270)
Resolves #254
2018-09-19 15:42:01 +09:00
Dan Helfman 1ade353c1a Use --namespace value within Helmfile template (#343)
* Use --namespace value within Helmfile template (#326).

* Tabs and spaces.

* Tab/spaces fix.

* Pacifying "go fmt".

* Hard-coding namespace in test convenience function.

* MR feedback: Add comment to exported identifier.

* MR feedback: Put comment in correct location.

* fix(ci): fix never-ending build issue by auto-approving `helmfile delete`
2018-09-17 21:28:45 +09:00
Terry Corley 6453768f2e Fix small typo in error string in state.go (#345) 2018-09-17 20:27:39 +09:00
KUOKA Yusuke cf31d02f25 feat: Option to mark release as not installed (#342)
Resolves #197
2018-09-16 22:44:47 +09:00
KUOKA Yusuke c5bc932ba2 feat: Prompt before delete (#341)
Resolves #182
2018-09-16 22:43:29 +09:00
KUOKA Yusuke 81d796c167 fix: readFile and exec should run in the directory the template file resides in (#340)
Fixes #299
2018-09-16 22:43:17 +09:00
KUOKA Yusuke 37ea442515 feat: delete in reverse order (#339)
Resolves #312
2018-09-16 22:42:13 +09:00
David Genest 43a54486a1 docs: add precision on what value files will be rendered (#337) 2018-09-14 21:44:38 +09:00
David Genest b3da9a9a07 fix: evaluate helm-binary BEFORE any helm invocation (#333)
closes #332
2018-09-13 09:59:35 +09:00
David Genest 30155d8742 fix: use channel to harvest download results (#328)
Fixes #327
2018-09-12 09:12:23 +09:00
David Genest 7bfb58c0e4 feat: double render the helmfile (#308)
This allows using the environment values defined in the environments: section of helmfile.yaml to be used from other sections of the file.

This works by having two template renderers, the first-pass and the second-pass renderer.

The first-pass render renders a helmfile.yaml template with replacing template functions has side-effects with noop. So, use only funcs that don't have side-effects to compose your environment values.

Then the second-pass renderer renders the same helmfile.yaml template, but with the environment values loaded by the first-pass renderer.

The implementation uses a buffer instead of re-reading the file twice.

Resolves #297
2018-09-12 08:55:42 +09:00
KUOKA Yusuke 751e549253 fix: prevent panicing on invalid selector format (#323)
Fixes #322
2018-09-09 17:20:44 +09:00
KUOKA Yusuke 4f073167b2 fix: prevent panicing when an undefined environment name was specified via --environment (#321)
Fixes #320
2018-09-09 08:35:15 +09:00
KUOKA Yusuke 7c65b2ed2c fix: unexpected no releases found for any helmfiles on helmfiles: config (#318)
Fixes #315
Fixes #316
2018-09-08 13:50:51 +09:00
Shane Starcher 595c70f85b feat: add devel flag support (#313)
Resolves #161

Changelog:

* add support for devel flag

* allow for explicit disable of the devel default
2018-09-08 08:32:14 +09:00
KUOKA Yusuke 8f96f0aff5 fix: do not panic on invalid yaml (#314)
Fixes #311
2018-09-08 02:15:39 +09:00
Takuma Hashimoto c3934495fe fix: {{ .Environment.Name }} being always blank in helmfile.yaml
Correctly sets the environment name passed via the flag `--environment NAME`, so that it can be referenced by `{{ .Environment.Name }}`
2018-09-06 18:12:32 +09:00
KUOKA Yusuke a205f0d586 fix: concurrency issue in diff and sync (#309)
fixes #301
2018-09-06 10:14:10 +09:00
David Genest 2e44da1b54 ensure helmfiles are searched relative to base helmfile (#307) 2018-09-06 10:08:45 +09:00
Karl Stoney d0a4008965 Add missing repos on lint command. (#305)
https://github.com/roboll/helmfile/issues/303
2018-09-06 10:07:43 +09:00
Karl Stoney af121b85b5 feat: Concurrent chart download in template and lint commands
This enables `helmfile lint` and `helmfile template` commands to fetch and untar all the required charts concurrently. The concurrency is configurable via the `--concurrency` flag, that defaults to `0`.

Ref #292
2018-09-05 08:19:57 +09:00
KUOKA Yusuke 1c3bfcca10 feat(environment): Allow missing environment in helmfile.yaml, partly. (#294)
helmfile as of today ensures that all the targeted helmfile.yaml to have the specified environment defined in it.
That is, `helmfile --environment prod -f helmfile.d/ sync` fails if any helmfile under `helmfile.d/` is missing the `production` environment.

This changes the validation logic, so that helmfile fails only when all the helmfiles miss the environment.

Resolves #279
2018-09-04 22:04:37 +09:00
Hans Kristian Flaatten 7d7ca74a05 docs: update link to documentation for latest released version (#293) 2018-09-04 21:49:21 +09:00
KUOKA Yusuke f1ac50bf44 feat: apply run sync against only affected releases (#291)
Enhance the `diff` functionality to be able to return affected releases that has any changes,
so that the succeeding `sync` can be run against only the affected releases.

This provides us extra idempotency.

Resolves #277
2018-09-04 21:48:43 +09:00
KUOKA Yusuke 60843cc224 fix: Setting concurrency>1 with single release breaks sync (#290)
Fixes #287
2018-09-04 20:59:24 +09:00
Karl Stoney 53dea091e3 Fix set in helmfile template and helmfile lint (#289)
This removes --concurrenty flag from `helmfile template` and `helmfile lint`. YAGNI.
Also separates out the downloadCharts into a function so it can be used by lint too.

Fixes #288
2018-09-04 20:55:32 +09:00
KUOKA Yusuke 5510de4765 feat(apply): update repos and and deps before running diff (#286)
Resolves #271
2018-09-04 16:12:43 +09:00
Karl Stoney 93c5d4c219 feat: helmfile template (#284)
`helmfile template` runs `helm template` over releases within the helmfiles, and provide you a stream of generated yaml documents of Kubernetes resources via stdout.

Resolves #283
2018-09-04 11:31:43 +09:00
KUOKA Yusuke 8a90e5320c feat: Ability to call arbitrary command from a template (#282)
Resolves #244
2018-09-03 16:48:03 +09:00
rmartinez3 6be53b1bcc WIP: adding to be albe to set multiple flags for --set, -f, --value in args (#278)
* adding to be albe to set multiple flags for --set, -f, --value in args

* add test for passing a set flag with two or more key-values
2018-09-03 16:45:02 +09:00
KUOKA Yusuke b0cc7ba96c fix: sync partially run helm upgrade even one of releases failed while loading values.yaml (#281)
Fixes #280
2018-09-03 16:43:57 +09:00
KUOKA Yusuke 54f1567294 feat: --suppress-secrets of diff and apply commands (#272)
Adds `--suppress-secrets` to `helmfile apply` and `helmfile diff`, so that the diff command omits the contents of secrets from its output. This is a security feature that should always be turned on for CI/CD use-cases.

With `--suppress-secrets`, the output when there is any change looks like:

```
Comparing bar stable/grafana
default, baz-grafana, Secret (v1) has changed:
+ Changes suppressed on sensitive content of type Secret
```

Resolves #269
2018-09-02 21:21:29 +09:00
KUOKA Yusuke 046281c70f feat: Environment Secrets (#274)
Resolves #255
2018-09-02 21:07:35 +09:00
KUOKA Yusuke 98617f78e3 fix: --args "--set foo=bar" always result in "unknown flag: --set foo" error (#268)
Ref #248
2018-09-02 21:06:38 +09:00
KUOKA Yusuke ed0854a5c0 feat: Environment and Environment Values (#267)
Resolves #253
2018-08-31 22:59:27 +09:00
KUOKA Yusuke 7c793fdb88 feat: helmfiles: <ordered glob patterns of helmfiles> configuration (#266)
Resolves #247
2018-08-31 12:03:18 +09:00
Dan Wendorf b70956b5d8 Bump Helm 2.9.1 -> 2.10.0 (#265) 2018-08-31 10:17:51 +09:00
KUOKA Yusuke 3840605e04 feat: helmfile apply [--auto-approve] (#263)
This command syncs releases only if there is any difference between the desired and the current state. It asks for an confirmation by default. Provide `--auto-approve` flag after the `apply` command to skip it.

Resolves #205
2018-08-31 10:15:02 +09:00
KUOKA Yusuke bb3b44e511 fix: follow up for the relative paths improvement (#262)
Fixes for the bugs that are introduced by #261, that is values.yaml files specified in `values:` have redundant base path in their prefixes, and remaining .dec files after secrets decryption(https://github.com/roboll/helmfile/issues/251#issuecomment-417285854)
2018-08-30 21:59:59 +09:00
KUOKA Yusuke 421299c883 feat: All the paths are relative to helmfile.yaml (#261)
`helmfile lint` works with relative chart reference (#252)
The tempalte function `readFile` accepts the path relative to helmfile.yaml

Resolves #246
Fixes #252
2018-08-30 19:47:52 +09:00
KUOKA Yusuke 79f0e70ce8 fix: avoid "cross-device link" errors while decrypting secrets (#260)
ref https://github.com/roboll/helmfile/issues/251#issuecomment-417166296
2018-08-30 17:20:38 +09:00
KUOKA Yusuke 0c8a89cbaf fix: coordinate secret decryption (#259)
Fixes #258
2018-08-30 17:03:45 +09:00
KUOKA Yusuke b3ebd4cdd0 fix: Keep backward-compatibility broken after introduction of values file template (#257)
Fixes #249
2018-08-30 16:57:37 +09:00
Shane Starcher 9b71c64ef2 fix: use temporary file for secret file to prevent deletion collision (#250)
Fixes #167
2018-08-30 09:28:26 +09:00
KUOKA Yusuke 822cc13e72 feat: Template functions to replace sets (#242)
Resolves #227
2018-08-28 22:00:51 +09:00
KUOKA Yusuke 0ac8401d1e feat: feat: Configurable default values for --verify, --force, --recreate-pods, and --timeout (#241)
Resolves #230
2018-08-28 15:11:05 +09:00
KUOKA Yusuke 815ee1f85b feat: --force, --recreate-pods, --timeout as first-class citizens in helmfile.yaml (#239)
Resolves #229
2018-08-27 23:06:16 +09:00
KUOKA Yusuke 2c36640ad2 To reduce integration test flakiness 2018-08-27 23:04:16 +09:00
KUOKA Yusuke faaf172615 fix: panic on flag parsing error (#238)
This is a follow-up for #235.

We setup the default logger only after the flags are parsed successfully.
That's because we init the logger according to the --log-level flag value.
2018-08-27 22:58:25 +09:00
KUOKA Yusuke fcbd6aff29 fix: Relative path in KUBECONFIG envvar (#237)
Resolves #173
2018-08-27 22:53:17 +09:00
Shane Starcher 543a653864 improve error handling for value rendering (#235)
Fixes #233

Output on values render error:

```
err: failed to render [/Users/sstarcher/xxx/xxx/values.yaml], because of template: stringTemplate:10:18: executing "stringTemplate" at <requiredEnv "HELM_AC...>: error calling requiredEnv: required env var `HELM_ACCOUNT` is not set
```

Also removes panic and sets the output as `apps.Run()` can and will return errors.  Panic makes no sense.

Changelog:

* improve error handling for value rendering

* only output if error exists

* add exit status
2018-08-27 12:53:25 +09:00
rmartinez3 fa69ac2cfa fix: Args being reordered (#224)
Fixes #212
2018-08-27 10:41:00 +09:00
KUOKA Yusuke 2121fe57b2 Merge pull request #232 from mumoshu/set-array
feat: set array
2018-08-24 17:12:17 +09:00
Yusuke KUOKA cfcafa2e46 feat: set array
Fixes #148
2018-08-24 17:01:34 +09:00
KUOKA Yusuke b6d2ee9d43 Merge pull request #231 from mumoshu/partly-allow-duplicate-release-name
fix: helmfile should fail on duplicate release name after filtered by labels
2018-08-24 16:43:38 +09:00
Yusuke KUOKA 35542b5516 fix: helmfile should fail on duplicate release name after filtered by labels
This is a follow-up for #218, fixes the unintentional degradation that broken the use-case described in https://github.com/roboll/helmfile/issues/193#issuecomment-415434408
2018-08-24 16:18:08 +09:00
KUOKA Yusuke 45d0f1c8e4 Merge pull request #228 from mumoshu/set-file
feat: Support for helm's --set-file
2018-08-24 13:44:07 +09:00
KUOKA Yusuke 96d0781b85 Merge pull request #226 from mumoshu/usage-without-internet
doc: Using helmfile without an Internet connection
2018-08-24 13:43:53 +09:00
Yusuke KUOKA 48e65791c0 feat: Support for helm's --set-file
Resolves #207
2018-08-24 13:06:31 +09:00
Yusuke KUOKA 2398ad2f35 doc: Using helmfile without an Internet connection
Resolves #155
2018-08-24 12:10:18 +09:00
KUOKA Yusuke 013276f576 Merge pull request #225 from mumoshu/unmatched-selector-exit-code
feat: exit code 2 on unmatched selectors
2018-08-24 11:47:10 +09:00
Yusuke KUOKA 3a16b7ed91 feat: exit code 2 on unmatched selectors
Along with the fix for the bug introduced via #217

Resolves #219
2018-08-24 11:40:30 +09:00
KUOKA Yusuke a5d42e741b Merge pull request #223 from mumoshu/show-which-release-failed
feat: emit error message containing which release in which helmfile an upgrade failed
2018-08-23 12:00:15 +09:00
Yusuke KUOKA 313b5de378 feat: emit error message containing which release in which helmfile an upgrade failed
Resolves #66
2018-08-23 11:45:43 +09:00
KUOKA Yusuke 0541731350 Merge pull request #222 from mumoshu/wait-for-release
feat: `wait: true` in each release
2018-08-23 10:43:01 +09:00
Yusuke KUOKA b4623f0515 feat: wait: true in each release
To instruct `helm upgrade` to wait for resources via `--wait`

Resolves #65
2018-08-23 10:37:15 +09:00
KUOKA Yusuke ffa40beff9 Merge pull request #221 from mumoshu/env-files
doc: Instruction to use env files
2018-08-23 09:47:01 +09:00
Yusuke KUOKA 4fae044f0e doc: Instruction to use env files
Closes #203
2018-08-23 09:44:42 +09:00
KUOKA Yusuke d8aa9f305b Merge pull request #218 from mumoshu/should-fail-on-duplicate-release-name
fix: helmfile should fail on duplicate release name
2018-08-23 09:32:03 +09:00
KUOKA Yusuke c402759363 Merge pull request #217 from mumoshu/less-verbose-unmatched-selector-logging
fix: Make logging on unmatched selectors less verbose
2018-08-23 09:27:32 +09:00
Yusuke KUOKA e84ff05a63 fix: helmfile should fail on duplicate release name
Fixes #193
2018-08-23 09:26:39 +09:00
Yusuke KUOKA fcb75fa851 fix: Make logging on unmatched selectors less verbose
Fixes #200
2018-08-23 09:23:55 +09:00
KUOKA Yusuke d609dab848 feat: Template values files (#216)
Resolves #97
2018-08-22 18:53:32 +09:00
KUOKA Yusuke 6c89fac8e5 Merge pull request #215 from mumoshu/diff-target-namespace
feat(diff): Pass namespace to helm-diff
2018-08-22 18:51:10 +09:00
KUOKA Yusuke 3550e5f335 Merge pull request #213 from chadasapp/master
Typofix: required_env -> requiredEnv
2018-08-22 15:05:29 +09:00
Yusuke KUOKA 127dd857df feat(diff): Pass namespace to helm-diff
Tested manually by running `helmfile diff` with debug logging:

```yaml
helmDefaults:
  tillerNamespace: foo

charts:
  - name: grafana
    namespace: grafana
    chart: stable/grafana
```

```console
$ ./helmfile --log-level debug diff
Comparing grafana stable/grafana
exec: helm diff upgrade --allow-unreleased grafana stable/grafana --namespace grafana --tiller-namespace=foo
```

Resolves #179
2018-08-22 11:20:55 +09:00
Chad Horohoe 2cf2dbb868 Typofix: required_env -> requiredEnv
The name is `requiredEnv` and I cannot find any references to `required_env` in the repository anywhere.
2018-08-20 11:10:01 -07:00
KUOKA Yusuke 545a4a3efa Merge pull request #208 from cloudposse/add-examples
Add link to more examples
2018-08-20 21:32:26 +09:00
KUOKA Yusuke 0ed1355573 Merge pull request #206 from InTheCloudDan/patch-1
remove outdated warning.
2018-08-07 15:28:05 +09:00
Erik Osterman 9fef3f7d0f Add link to more examples 2018-08-06 23:20:49 -07:00
KUOKA Yusuke 0d457c4df0 Merge pull request #201 from maver1ck/helmfile-111
Add --args to delete
2018-08-03 09:36:48 +09:00
KUOKA Yusuke 6be52bfbbb Merge pull request #199 from mumoshu/optional-detailed-exitcode-for-diff
feat: Optional detailed exitcodes for `helmfile diff`
2018-08-03 09:36:08 +09:00
Dan O'Brien 089c158084 remove outdated warning. 2018-08-02 20:19:29 -04:00
Maciej Bryński d67573299c Add --args to delete 2018-07-31 10:18:31 +02:00
Yusuke KUOKA 3b4ce90a5a feat: Optional detailed exitcodes for helmfile diff
Adds the `--detailed-exitcode` to the `helmfile diff` command to return `1` on failure, and `2` when no error but diff is seen.

This feature requires the latest `helm-diff` containing https://github.com/databus23/helm-diff/pull/78, and `helm` containing https://github.com/helm/helm/pull/4367.

This is verified to work by manually running commands like the followings:

```bash
./helmfile --helm-binary helm211dev -f ./examples/helmfile.d diff --detailed-exitcode; echo $?
./helmfile --helm-binary helm211dev -f ./examples/helmfile.d diff; echo $?
```

Note that, in above example commands, `helm211dev` is a custom `helm` binary that is built from helm's master branch containing [the necessary enhancement to allow propagate non-zero plugin exit code](https://github.com/helm/helm/pull/4367).
2018-07-26 00:16:47 +09:00
KUOKA Yusuke 92d09a3e92 Merge pull request #198 from mumoshu/fix-ci-flakiness
fix(ci): Reduce flakiness
2018-07-25 23:24:44 +09:00
Yusuke KUOKA 1a5558a306 fix(ci): Reduce flakiness
Fix #103
2018-07-25 11:21:01 +09:00
KUOKA Yusuke ee3632cebc Merge pull request #191 from mumoshu/fix-cmd-descrs
Fix command descriptions
2018-07-19 12:02:07 +09:00
KUOKA Yusuke 2297093f84 Merge pull request #190 from mumoshu/fix-helmfile.d-defaulting
fix: `helmfile sync` doesn't recognize the default `helmfile.d/` directory
2018-07-19 12:01:49 +09:00
KUOKA Yusuke 506de12fcd fix: helmfile -f dir/ -l k=v sync fails on first helmfile w/ no match (#188)
Fixes #187

Changelog:

* fix: `helmfile -f dir/ -l k=v sync` fails on first helmfile w/ no match

* fix(ux): present the filename if the selector did not match any releases

```console
$ helmfile -f helmfile.d/ -l foo=bar sync
2018/07/12 23:37:29 specified selector did not match any releases in helmfile.d/00-backend.yaml
2018/07/12 23:37:29 specified selector did not match any releases in helmfile.d/01-frontend.yaml
2018/07/12 23:37:29 err: specified selector did not match any releases in any helmfile
```
2018-07-19 12:01:14 +09:00
rmartinez3 01fe5b4213 Adding merging for helmDefaults args and --args, adding tillerNamspace and kubeContext to helmDefaults (#186)
Add tillerNamespace and kubeContext to helmDefaults. Also add merging of args capability. 

People can start adding additional fields under helmDefaults on top of this. The additional fields are merged to args as long as they are implemented using the framework introduced by this commit.

Changelog:

* adding merging for helm defaults args and --args, adding tillernamspace, kubecontext to helmdefaults, adding getargs to lint command

* adding docs for helmdefaults

* checking if empty default values
2018-07-19 11:53:09 +09:00
Simon Li 06a1d245ae Multi-level logging (#185)
Closes #93

Summary of changes:

- Adds the [uber-go-/zap](https://github.com/uber-go/zap) library for leveled logging (69040996e0bcbd2f211d0d8d576147027e928e3b)
- A new CLI arg `--log-level` which can take the values `debug` `info` `warn` `error` `fatal` `panic`. The default is `info`, and the existing `--quiet` flag is treated as `--log-level warn`
- All `helm.exec` calls are preceded by a one-line summary. The current information on the full helm command line arguments `exec: helm exec ... ` is only output at `debug` level. This means sensitive command line arguments which may include passwords and `--set` arguments should be hidden by default.
2018-07-19 11:33:57 +09:00
Yusuke KUOKA 438d19852a Fix command descriptions
Resolves #170
2018-07-12 16:09:46 +09:00
Yusuke KUOKA c74db5d281 fix: helmfile sync doesn't recognize the default helmfile.d/ directory
Fixes #189
2018-07-12 14:13:56 +09:00
KUOKA Yusuke 35732f3a93 Bugfix for helm-binary flag. to global (#184)
Two follow-up commits for #181 

* Make the --helm-binary flag global
* Add helm-binary support to the delete command
2018-07-05 11:56:09 +09:00
hori-ryota 7f37c6b39f Add helm-binary opt to delete command 2018-07-04 22:10:17 +09:00
hori-ryota fa0ff32b00 Bugfix for helm-binary flag. to global 2018-07-04 19:01:46 +09:00
Hori Ryota 9f57f8be7d Add the option to specify an alternative helm binary (#181)
`helm` may not be version compatible. Add an option to specify an alternative `helm` command on runtime, possibly according to the version of tiller running on your cluster.
2018-07-03 22:19:02 +09:00
KUOKA Yusuke 88e84e5ae8 Merge pull request #177 from amnk/amnk/issue-176
Add helm-secrets plugin to Dockerfile
2018-06-26 22:37:44 +09:00
KUOKA Yusuke c52504cf8e Merge pull request #178 from Scukerman/master
Fix typo in README.md
2018-06-26 22:33:37 +09:00
Scukerman a8212587b8 Fix typo in README.md 2018-06-26 16:25:42 +03:00
amnk e1040c2aa1 Add helm-secrets plugin to Dockerfile
Closes: #176
2018-06-26 10:42:47 +02:00
rmartinez3 58bc2a2997 adding args to be added in helmfile along with context to be set in helmspec (#171)
Ref #114 

Changelog:

* adding args to be added in helmfile along with context to be set in helmspec

* making command args take precedence
2018-06-26 10:12:38 +09:00
KUOKA Yusuke af1914c575 Merge pull request #172 from mumoshu/diff-against-new-release
feat: `helmfile diff` also works against non-existent releases
2018-06-25 16:15:09 +09:00
Yusuke KUOKA da337e8724 feat: helmfile diff also works against non-existent releases
Resolves #127
2018-06-14 22:45:26 +09:00
Johan Lyheden 6856c6e979 Add helmfile lint support (#162)
The use case is to have a list of helmfile releases version controlled together with all settings and have a CI pipeline that will lint all releases with settings before running sync. The new functionality was mostly copy pasted from the Diff implementation with some extra handling for fetching remote charts.

Notes:

* Added release name to chart path to avoid potential race condition when fetching the chart
2018-06-14 22:35:09 +09:00
KUOKA Yusuke 2fba241122 Ordered helmfile(s) from a directory (#160)
Resolves #151 #137

Notes:

- You can have either helmfile.d or helmfile.yaml/charts.yaml, not both
2018-06-14 22:20:15 +09:00
KUOKA Yusuke df264e2736 Merge pull request #165 from cmeury/no-match-error
fail when no releases match selector
2018-06-12 23:58:33 +09:00
Cedric Meury 575198424a fail when no releases match selector 2018-06-12 14:19:06 +02:00
KUOKA Yusuke afa45930c1 Merge pull request #159 from wendorf/dockerfile-helm-diff
Dockerfile installs helm-diff
2018-06-01 12:06:06 +09:00
KUOKA Yusuke 3044255f7a Merge pull request #156 from wendorf/dockerfile-helm-2.9.1
Docker image uses Helm v2.9.1 (from v2.8.2)
2018-06-01 12:04:35 +09:00
Dan Wendorf 98d3897262 Dockerfile installs helm-diff
Enables the `helmfile diff` command

- git and bash are required to install helm-diff
2018-05-31 15:52:33 -07:00
Dan Wendorf a9a8fb8388 Docker image uses Helm v2.9.1 (from v2.8.2) 2018-05-31 13:43:50 -07:00
Dan Wendorf fb6ac7bba2 Dockerfile uses ARG instead of ENV
Allows customization at image build time
2018-05-31 13:41:36 -07:00
Dan O'Brien 1a4f342f25 Add basic repository authentication (#154)
* basic repository authentication via new `username` and `password` keys

* add warning to readme
2018-05-31 10:42:38 +09:00
KUOKA Yusuke 1768e5dea7 Add tests for helmfile test (#152)
Follow up for #150
2018-05-22 17:19:10 +09:00
Mike Eves 37f6ae8557 Add helmfile test sub-command (#150)
**Feature**
An additional sub-command to the helmfile binary; helmfile test

**Why**
Helm provides helm test (https://github.com/kubernetes/helm/blob/master/docs/chart_tests.md) as a method to run automated tests post chart install to ensure things are working as they should be.

It would be nice to be able to run something like helmfile test against a particular helmfile in order to run helm tests against all charts/releases defined in the file. Either as part of the sync (i.e. helmfile sync --test) to be ran immediately after the corresponding chart is installed, or as a separate command ran after a sync (i.e. helmfile test).

A chart without tests will exit with a 0 status, so it can be safely ran against any charts.

**Notes**

`--cleanup` (bool) & `--timeout` (default: 300) are available as first class arguments. Additional arguments can be passed to the helm binary as with other sub commands using `--args=`

Resolves #144
2018-05-22 17:12:48 +09:00
KUOKA Yusuke 5735efa8c0 Merge pull request #147 from mumoshu/fix-static-linking-for-linux-binary-on-circle
Fix static linking for linux binaries built on CircleCI
2018-05-17 16:22:49 +09:00
Yusuke KUOKA f08b981814 Fix static linking for linux binaries built on CircleCI
Fixes #146
2018-05-16 22:46:35 +09:00
KUOKA Yusuke a7e35b7316 Merge pull request #142 from mumoshu/make-purge-optional
Make purge optional when running "helmfile delete"
2018-05-16 08:59:35 +09:00
KUOKA Yusuke f288032d60 Merge pull request #145 from ConnectedHomes/absolute-value-path
Don't prefix base path to absolute path
2018-05-15 22:36:40 +09:00
David Bliss 4fe8d988d5 Don't prefix base path to absolute path
If an entry in the `values` section of a helmfile is an absolute
path, then it would be erroneously prefixed with the base path.

This would cause `helmfile sync` to fail because the value.yaml file
would not exist at the incorrectly prefixed path.
2018-05-15 11:22:38 +01:00
KUOKA Yusuke 09f19047bd Merge pull request #143 from mumoshu/fix-helm-diff-deprecation-warning
Fix the helm-diff deprecation warning
2018-05-11 21:56:15 +09:00
Yusuke KUOKA 52507de39b Mention about the minimum ver. of helm-diff 2018-05-11 21:52:08 +09:00
Yusuke KUOKA ac70e45bff Fix the helm-diff deprecation warning
Resolves #130
2018-05-11 16:51:44 +09:00
Yusuke KUOKA 3979c18ad3 Make purge optional when running "helmfile delete"
`helmfile delete` has been implying `--purge` but it is not the case since this change.

The new behavior is `helmfile delete --purge` to actually purge releases.
Run just `helmfile delete` to delete releases but not purge them.

Resolves #71
2018-05-11 16:41:02 +09:00
KUOKA Yusuke fb781be560 Merge pull request #139 from int128/inline-values
Inline values support
2018-05-11 14:34:25 +09:00
Hidetake Iwata 5db3ba2112 Add inline values support
This allows `values` accepts the following types:
- string type for values file path
- map type for inline values
2018-05-11 11:55:01 +09:00
KUOKA Yusuke 568d750f75 Merge pull request #141 from mumoshu/always-reset-values
Force `--reset-values` on sync
2018-05-11 03:05:45 +09:00
Yusuke KUOKA c41dbc4c9f Force --reset-values on sync
Resolves #63
2018-05-11 03:04:36 +09:00
KUOKA Yusuke 26ffdc2f24 Merge pull request #136 from AXOOM/feat/arg_escaping
Escape names and values before passing to Helm
2018-05-10 09:27:45 +09:00
Bastian Eicher eea4746ae2 Escape names and values before passing to Helm
This is required to handle values with commas and/or curly braces.
2018-05-08 14:43:58 +02:00
KUOKA Yusuke c5d530632b Merge pull request #135 from AXOOM/feat/required_envs
Introduce template function `required_env`
2018-05-06 19:35:04 +09:00
Andreas Bieber 77204d9a8c Introduce template function requiredEnv
The new template function `requiredEnv` ensures that the given env var is set and not empty.
2018-05-05 21:36:59 +02:00
KUOKA Yusuke 67ddd7f1ae Merge pull request #128 from cmeury/retry-curl
retry curl to reduce risk of race condition
2018-05-01 12:08:42 +09:00
Cedric Meury 10255aecc7 retry curl to reduce risk of race condition 2018-04-29 22:13:16 +02:00
KUOKA Yusuke 89ca84fb21 Merge pull request #129 from cmeury/install-ghr
install ghr for releasing
2018-04-28 23:33:15 +09:00
KUOKA Yusuke bdf7209434 Merge pull request #126 from cmeury/chart-missing-error
fail fast on syntax error by strictly unmarshalling
2018-04-28 23:31:34 +09:00
Cedric Meury 763c5424b6 install ghr for releasing 2018-04-28 11:40:23 +02:00
Cedric Meury e4c016142d add unit tests and upgrade minikube, helm and k8s; set automountServiceAccountToken for 2.9.0 compatibility 2018-04-28 00:19:01 +02:00
Cedric Meury 2a1cdb0253 upgrade gopkg.in/yaml from v1 to v2 to strictly unmarshal 2018-04-28 00:17:56 +02:00
Cedric Meury 8c6137ee62 adding simple unit test for sync releases 2018-04-28 00:17:56 +02:00
KUOKA Yusuke e4b3ba444c Merge pull request #125 from cmeury/users
add users file to track production use
2018-04-27 22:16:41 +09:00
KUOKA Yusuke ad34be579d Merge pull request #124 from cmeury/build-container
build consistent statically linked binary for docker image
2018-04-27 22:14:45 +09:00
KUOKA Yusuke 44241f675f Merge pull request #70 from cmeury/status-command
status command retrieves release status
2018-04-27 22:09:12 +09:00
Cedric Meury 10cdae5dab table format 2018-04-27 13:39:33 +02:00
Cedric Meury 930ea98245 add users file to track production use 2018-04-27 13:38:17 +02:00
Cedric Meury 151fe0716e keep it simple 2018-04-27 11:21:43 +02:00
Cedric Meury 2af039dcfc don't use gox for single build 2018-04-27 11:20:18 +02:00
Cedric Meury 5f9d3f818c build consistent statically linked binary for docker image
* use golang 1.10.1 images everywhere
* introduce a `static-linux` target to build a statically linked 64-bit binary only
* use that binary for the official image and the circle build and tests
* use a multi-stage dockerfile for a consistent build environment
2018-04-27 11:11:35 +02:00
Cedric Meury 579fa4c765 status command retrieves release status 2018-04-27 10:31:29 +02:00
KUOKA Yusuke efe5a2e56b Merge pull request #121 from roboll/up-to-date-release-note
Ensure the release note to be up-to-date
2018-04-26 20:01:10 +09:00
KUOKA Yusuke b564c4ad4c Ensure the release note to be up-to-date 2018-04-26 20:00:59 +09:00
KUOKA Yusuke ae2777f6ad Merge pull request #120 from roboll/fix-release
Fix failing automated release
2018-04-26 19:51:14 +09:00
KUOKA Yusuke 652e001d45 Fix failing automated release
Follow up for 71fa693 and #98
2018-04-26 19:51:01 +09:00
KUOKA Yusuke 9b0d0d4cba Merge pull request #115 from sstarcher/chart_detection
support relative path for is local check
2018-04-26 06:13:22 +09:00
KUOKA Yusuke d8465c4ca5 Merge pull request #116 from AXOOM/master
Fix resolving relative paths on windows
2018-04-25 22:31:57 +09:00
Andreas Bieber 46f119b224 Fix resolving relative paths on windows
path.Dir() does not support Windows-style directory separators (backslashes). This caused invocations such as "helmfile -f subdir\helmfile.yml" to fail. Fixed by using filepath.Dir() instead.
2018-04-25 14:40:45 +02:00
Shane Starcher df5d2e01d5 updating tests to use local files 2018-04-24 14:10:46 -04:00
Shane Starcher cf01221f3a support relative path for is local check 2018-04-24 09:01:42 -04:00
KUOKA Yusuke ca26d757f7 Merge pull request #113 from gavvvr/patch-1
Add root CAs to docker image
2018-04-23 10:46:14 +09:00
Kirill Gavrilov a28279233e Add root CAs to docker image 2018-04-22 01:09:54 +03:00
KUOKA Yusuke e85b87828a Merge pull request #110 from sstarcher/empty_filter
support selector that is empty
2018-04-18 15:49:25 +09:00
Shane Starcher 789362dfd4 support the same selector repeated 2018-04-17 11:10:46 -04:00
KUOKA Yusuke 79c7d1501a Merge pull request #106 from roboll/update-readme-for-breaking-change
Update README not to confuse new-comers due to breaking change in master
2018-04-12 10:46:03 +09:00
KUOKA Yusuke 883cd1e3f3 Update README not to confuse new-comers due to breaking change in master 2018-04-11 21:15:49 +09:00
Shane Starcher 71fa693997 templating the entire helmfile and adding some documentation (#98)
* templating the entire helmfile and adding some documentation

* updating vendor and using dep

* update to golang 1.10

* updating docs
2018-04-10 20:19:43 +09:00
118 changed files with 20486 additions and 15561 deletions
+130 -32
View File
@@ -4,65 +4,113 @@ jobs:
build:
docker:
- image: circleci/golang:1.7
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.13.4
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
- 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.13.4
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.13.4
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.7
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.13.4
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
# thanks to https://raw.githubusercontent.com/weaveworks/launcher/master/.circleci/config.yml
integration_tests:
machine: true
machine:
image: circleci/classic:201808-01
environment:
CHANGE_MINIKUBE_NONE_USER: true
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
steps:
- checkout
- run: mkdir ~/build
- attach_workspace:
at: ~/build
- run: cp ~/build/helmfile ~/project/helmfile
- run:
name: Install kubectl
command: |
curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.8.4/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
- run:
name: Install minikube
command: |
curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.25.0/minikube-linux-amd64
chmod +x minikube
sudo mv minikube /usr/local/bin/
- run:
name: Install helm
environment:
HELM_VERSION: v2.15.2
command: |
HELM_VERSION=v2.8.2
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: Start minikube
name: Deploy minikube
environment:
CHANGE_MINIKUBE_NONE_USER: true
K8S_VERSION: v1.12.3
MINIKUBE_VERSION: v0.30.0
command: |
sudo minikube start --vm-driver=none
sudo chown -R $USER.$USER ~/.minikube
sudo chown -R $USER.$USER ~/.kube
minikube update-context
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
@@ -72,35 +120,85 @@ jobs:
export TERM=xterm
make integration
integration_tests_helm3:
machine:
image: circleci/classic:201808-01
environment:
CHANGE_MINIKUBE_NONE_USER: true
MINIKUBE_WANTUPDATENOTIFICATION: false
MINIKUBE_WANTREPORTERRORPROMPT: false
steps:
- checkout
- run: mkdir ~/build
- attach_workspace:
at: ~/build
- run: cp ~/build/helmfile ~/project/helmfile
- run:
name: Install helm
environment:
HELM_VERSION: v3.0.0
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://get.helm.sh/${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
- run:
name: Execute integration tests
command: |
export TERM=xterm
HELMFILE_HELM3=1 make integration
# GITHUB_TOKEN env var must be setup in circleci console
release:
docker:
- image: circleci/golang:1.7
working_directory: /go/src/github.com/roboll/helmfile
- image: circleci/golang:1.13.4
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.*/
- integration_tests_helm3:
requires:
- build
- release:
filters:
branches:
+3 -1
View File
@@ -1,3 +1,5 @@
dist/
.idea/
helmfile
helmfile
helmfile.lock
vendor/
+39
View File
@@ -0,0 +1,39 @@
# DCO + License
By contributing to `helmfile`, you accept and agree to the following DCO and license terms and
conditions for your present and future Contributions submitted to the `helmfile` project.
[DCO](https://developercertificate.org/)
[License](https://github.com/roboll/helmfile/blob/master/LICENSE)
# Developing helmfile
Locate your `GOPATH`, usually `~/go`, and run:
```console
$ go get github.com/roboll/helmfile
$ cd $GOPATH/src/github.com/roboll/helmfile
$ git checkout -b your-shiny-new-feature origin/master
...
$ git commit -m 'feat: do whatever for whatever purpose
This adds ... by:
- Adding ...
- Changing ...
- Removing...
Resolves #ISSUE_NUMBER
'
$ hub fork
$ git push YOUR_GITHUB_USER your-shiny-new-feature
$ hub pull-request
```
Note that the above tutorial uses [hub](https://github.com/github/hub) just for ease of explanation.
Please use whatever tool or way to author your pull request!
+37 -7
View File
@@ -1,14 +1,44 @@
FROM alpine:3.7
FROM golang:1.13.6-alpine3.11 as builder
ENV HELM_VERSION=v2.8.2
ENV HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ENV HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ENV HELM_SHA256="614b5ac79de4336b37c9b26d528c6f2b94ee6ccacb94b0f4b8d9583a8dd122d3"
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=v2.15.2
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="a9d2db920bd4b3d824729bbe1ff3fa57ad27760487581af6e5d3156d1b3c2511"
RUN 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/ && \
rm ${HELM_FILENAME} && rm -r /linux-amd64
COPY dist/helmfile_linux_amd64 /usr/local/bin/helmfile
# 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.
ENV KUBECTL_VERSION="v1.14.5"
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
RUN curl -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"]
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/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 /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
CMD ["/usr/local/bin/helmfile"]
+44
View File
@@ -0,0 +1,44 @@
FROM golang:1.13.6-alpine3.11 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.0.2
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="c6b7aa7e4ffc66e8abb4be328f71d48c643cb8f398d95c74d075cfb348710e1d"
RUN 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.
ENV KUBECTL_VERSION="v1.14.5"
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
RUN curl -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 plugin install https://github.com/databus23/helm-diff --version v3.0.0-rc.7 && \
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
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
ENV HELMFILE_HELM3="1"
CMD ["/usr/local/bin/helmfile"]
+36 -4
View File
@@ -26,22 +26,31 @@ integration:
.PHONY: integration
cross:
gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!arm !mips !mipsle !mips64 !mips64le !s390x' -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 GOFLAGS=-mod=vendor 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 github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
.PHONY: install
clean:
rm dist/helmfile_*
.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
@ghr -b ${BODY} -t ${GITHUB_TOKEN} -u ${ORG} -replace ${TAG} dist
@ghr -b ${BODY} -t ${GITHUB_TOKEN} -u ${ORG} -recreate ${TAG} dist
.PHONY: release
image: cross
image:
docker build -t quay.io/${ORG}/helmfile:${TAG} .
run: image
@@ -50,6 +59,13 @@ 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
@@ -57,3 +73,19 @@ tools:
TAG = $(shell git describe --tags --abbrev=0 HEAD)
LAST = $(shell git describe --tags --abbrev=0 HEAD^)
BODY = "`git log ${LAST}..HEAD --oneline --decorate` `printf '\n\#\#\# [Build Info](${BUILD_URL})'`"
release/minor:
git checkout master
git pull --rebase origin master
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
git checkout -b autorelease origin/master
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s minor'
git checkout master
release/patch:
git checkout master
git pull --rebase origin master
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
git checkout -b autorelease origin/master
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s patch'
git checkout master
+948 -60
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
# Helmfile Users
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 | 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 |
| [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 |
| [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 |
<!-- TABLE_END -->
+152
View File
@@ -0,0 +1,152 @@
# Shared Configuration Across Teams
Assume you have a two or more teams, each works for a different internal or external service, like:
- Product 1
- Product 2
- Observability
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: product2-api
chart: saas-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
This works, but what if you wanted to a separate cluster per service to achieve smaller blast radius?
Let's start by creating a `helmfile.yaml` for each service.
`product1/helmfile.yaml`:
```yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
`product2/helmfile.yaml`:
```yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
You will (of course!) notice this isn't DRY.
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
`observability/helmfile.yaml`:
```yaml
- name: observability-prometheus-operator
chart: stable/prometheus-operator
# snip
- name: observability-process-exporter
chart: stable/prometheus-operator
# snip
```
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
`product1/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product1-api
chart: product1-charts/api
# snip
- name: product1-web
chart: product1-charts/web
# snip
```
`product2/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
releases:
- name: product2-api
chart: product2-charts/api
# snip
- name: product2-web
chart: product2-charts/web
# snip
```
## Using sub-helmfile as a template
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
`shared/helmfile.yaml`:
```yaml
releases:
- name: product{{ env "PRODUCT_ID" }}-api
chart: product{{ env "PRODUCT_ID" }}-charts/api
# snip
- name: product{{ env "PRODUCT_ID" }}-web
chart: product{{ env "PRODUCT_ID" }}-charts/web
# snip
```
Then you only need one single product helmfile
`product/helmfile.yaml`:
```yaml
helmfiles:
- ../observability/helmfile.yaml
- ../shared/helmfile.yaml
```
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
```console
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
```
+358
View File
@@ -0,0 +1,358 @@
# The Helmfile Best Practices Guide
This guide covers the Helmfile’s considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
## Missing keys and Default values
helmfile tries its best to inform users for noticing potential mistakes.
One example of how helmfile achieves it is that, `helmfile` fails when you tried to access missing keys in environment values.
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
```
{{ .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:
```
{{ .Values | getOrNil "eventApi.replicas" }}
```
This result in printing `<no value` in your template, that may or may not result in a failure.
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
```
{{ .Values | getOrNil "eventApi.replicas" | default 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 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`:
```yaml
releases:
# *snip*
- name: heapster
namespace: kube-system
chart: stable/heapster
version: 0.3.2
values:
- "./config/heapster/values.yaml"
- "./config/heapster/{{ .Environment.Name }}.yaml"
secrets:
- "./config/heapster/secrets.yaml"
- "./config/heapster/{{ .Environment.Name }}-secrets.yaml"
- name: kubernetes-dashboard
namespace: kube-system
chart: stable/kubernetes-dashboard
version: 0.10.0
values:
- "./config/kubernetes-dashboard/values.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
secrets:
- "./config/kubernetes-dashboard/secrets.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
```
This is where Helmfile's advanced feature called Release Template comes handy.
It allows you to abstract away the repetitions in releases into a template, which is then included and executed by using YAML anchor/alias:
```yaml
templates:
default: &default
chart: stable/{{`{{ .Release.Name }}`}}
namespace: kube-system
# This prevents helmfile exiting when it encounters a missing file
# Valid values are "Error", "Warn", "Info", "Debug". The default is "Error"
# Use "Debug" to make missing files errors invisible at the default log level(--log-level=INFO)
missingFileHandler: Warn
values:
- config/{{`{{ .Release.Name }}`}}/values.yaml
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml
secrets:
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml
releases:
- name: heapster
<<: *default
- name: kubernetes-dashboard
<<: *default
```
Release Templating supports the following parts of release definition:
- basic fields: `name`, `namespace`, `chart`, `version`
- boolean fields: `installed`, `wait`, `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 Release Values
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 whichi 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:
```
bases:
- environments.yaml
releases:
- name: metricbaet
chart: stable/metricbeat
- name: myapp
chart: mychart
```
Whereas `environments.yaml` contained well-known environments:
```yaml
environments:
development:
production:
```
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
```yaml
---
# environments.yaml
environments:
development:
production:
---
# helmfile.yaml
releases:
- name: myapp
chart: mychart
- name: metricbaet
chart: stable/metricbeat
```
Finally the resulting YAML documents are merged in the order of occurrence,
so that your `helmfile.yaml` becomes:
```yaml
environments:
development:
production:
releases:
- name: metricbaet
chart: stable/metricbeat
- name: myapp
chart: mychart
```
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: metricbaet
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 helmfiel 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"
```
+48
View File
@@ -0,0 +1,48 @@
module github.com/roboll/helmfile
go 1.13
require (
cloud.google.com/go v0.47.0 // indirect
github.com/Azure/azure-sdk-for-go v35.0.0+incompatible // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/aws/aws-sdk-go v1.25.22 // indirect
github.com/go-test/deep v1.0.3
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
github.com/google/go-cmp v0.3.1
github.com/gosuri/uitable v0.0.3
github.com/hashicorp/go-getter v1.3.0
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
github.com/hashicorp/go-version v1.2.0
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/urfave/cli v1.20.0
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
github.com/variantdev/vals v0.0.0-20191123015158-21b16052816c
go.mozilla.org/sops v0.0.0-20190912205235-14a22d7a7060 // indirect
go.opencensus.io v0.22.1 // indirect
go.uber.org/multierr v1.1.0
go.uber.org/zap v1.9.1
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
google.golang.org/api v0.13.0 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d // indirect
gotest.tools v2.2.0+incompatible
)
+1061
View File
File diff suppressed because it is too large Load Diff
Executable
+623
View File
@@ -0,0 +1,623 @@
#!/usr/bin/env bash
PROG=semtag
PROG_VERSION="v0.1.0"
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
IDENTIFIER_REGEX="^\-([0-9A-Za-z-]+)\.([0-9A-Za-z-]+)*$"
# Global variables
FIRST_VERSION="v0.0.0"
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
hasversiontag="false"
scope="patch"
displayonly="false"
forcetag="false"
forcedversion=
versionname=
identifier=
HELP="\
Usage:
$PROG
$PROG getlast
$PROG getfinal
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
$PROG --help
$PROG --version
Options:
-s The scope that must be increased, can be major, minor or patch.
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
where X, Y and Z are positive integers, PRERELEASE is an optionnal
string composed of alphanumeric characters describing if the build is
a release candidate, alpha or beta version, with a number.
BUILD is also an optional string composed of alphanumeric
characters and hyphens.
Setting the scope as 'auto', the script will chose the scope between
'minor' and 'patch', depending on the amount of lines added (<10% will
choose patch).
-v Specifies manually the version to be tagged, must be a valid semantic version
in the format X.Y.Z where X, Y and Z are positive integers.
-o Output the version only, shows the bumped version, but doesn't tag.
-f Forces to tag, even if there are unstaged or uncommited changes.
Commands:
--help Print this help message.
--version Prints the program's version.
get Returns both current final version and last tagged version.
getlast Returns the latest tagged version.
getfinal Returns the latest tagged final version.
getcurrent Returns the current version, based on the latest one, if there are uncommited or
unstaged changes, they will be reflected in the version, adding the number of
pending commits, current branch and commit hash.
final Tags the current build as a final version, this only can be done on the master branch.
candidate Tags the current build as a release candidate, the tag will contain all
the commits from the last final version.
alpha Tags the current build as an alpha version, the tag will contain all
the commits from the last final version.
beta Tags the current build as a beta version, the tag will contain all
the commits from the last final version."
# Commands and options
ACTION="getlast"
ACTION="$1"
shift
# We get the parameters
while getopts "v:s:of" opt; do
case $opt in
v)
forcedversion="$OPTARG"
;;
s)
scope="$OPTARG"
;;
o)
displayonly="true"
;;
f)
forcetag="true"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
# $1 The string containing the version in semantic format
# $2 The variable to store the result array:
# position 0: major number
# position 1: minor number
# position 2: patch number
# position 3: identifier (or prerelease identifier)
# position 4: build info
function explode_version {
local __version=$1
local __result=$2
if [[ $__version =~ $SEMVER_REGEX ]] ; then
local __major=${BASH_REMATCH[1]}
local __minor=${BASH_REMATCH[2]}
local __patch=${BASH_REMATCH[3]}
local __prere=${BASH_REMATCH[4]}
local __build=${BASH_REMATCH[5]}
eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")"
else
eval "$__result="
fi
}
# Compare two versions and returns -1, 0 or 1
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the result
function compare_versions {
local __first
local __second
explode_version $1 __first
explode_version $2 __second
local lv=$3
# Compares MAJOR, MINOR and PATCH
for i in 0 1 2; do
local __numberfirst=${__first[$i]}
local __numbersecond=${__second[$i]}
case $(($__numberfirst - $__numbersecond)) in
0)
;;
-[0-9]*)
eval "$lv=-1"
return 0
;;
[0-9]*)
eval "$lv=1"
return 0
;;
esac
done
# Identifiers should compare with the ASCII order.
local __identifierfirst=${__first[3]}
local __identifiersecond=${__second[3]}
if [[ -n "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
if [[ "$__identifierfirst" > "$__identifiersecond" ]]; then
eval "$lv=1"
return 0
elif [[ "$__identifierfirst" < "$__identifiersecond" ]]; then
eval "$lv=-1"
return 0
fi
elif [[ -z "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
eval "$lv=1"
return 0
elif [[ -n "$__identifierfirst" ]] && [[ -z "$__identifiersecond" ]]; then
eval "$lv=-1"
return 0
fi
eval "$lv=0"
}
# Returns the last version of two
# $1 The first version to compare
# $2 The second version to compare
# $3 The variable where to store the last one
function get_latest_of_two {
local __first=$1
local __second=$2
local __result
local __latest=$3
compare_versions $__first $__second __result
case $__result in
0)
eval "$__latest=$__second"
;;
-1)
eval "$__latest=$__second"
;;
1)
eval "$__latest=$__first"
;;
esac
}
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
# $1 The identifier in the format -id.#
# $2 The vferiable where to store the 2 size array
function explode_identifier {
local __identifier=$1
local __result=$2
if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then
local __id=${BASH_REMATCH[1]}
local __number=${BASH_REMATCH[2]}
if [[ -z "$__number" ]]; then
__number=1
fi
eval "$__result=(\"$__id\" \"$__number\")"
else
eval "$__result="
fi
}
# Gets a list of tags and assigns the base and latest versions
# Receives an array with the tags containing the versions
# Assigns to the global variables finalversion and lastversion the final version and the latest version
function get_latest {
local __taglist=("$@")
local __tagsnumber=${#__taglist[@]}
local __current
case $__tagsnumber in
0)
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
;;
1)
__current=${__taglist[0]}
explode_version $__current ver
if [ -n "$ver" ]; then
if [ -n "${ver[3]}" ]; then
finalversion=$FIRST_VERSION
else
finalversion=$__current
fi
lastversion=$__current
else
finalversion=$FIRST_VERSION
lastversion=$FIRST_VERSION
fi
;;
*)
local __lastpos=$(($__tagsnumber-1))
for i in $(seq 0 $__lastpos)
do
__current=${__taglist[i]}
explode_version ${__taglist[i]} ver
if [ -n "$ver" ]; then
if [ -z "${ver[3]}" ]; then
get_latest_of_two $finalversion $__current finalversion
get_latest_of_two $lastversion $finalversion lastversion
else
get_latest_of_two $lastversion $__current lastversion
fi
fi
done
;;
esac
if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then
hasversiontag="true"
else
hasversiontag="false"
fi
}
# Gets the next version given the provided scope
# $1 The version that is going to be bumped
# $2 The scope to bump
# $3 The variable where to stoer the result
function get_next_version {
local __exploded
local __fromversion=$1
local __scope=$2
local __result=$3
explode_version $__fromversion __exploded
case $__scope in
major)
__exploded[0]=$((${__exploded[0]}+1))
__exploded[1]=0
__exploded[2]=0
;;
minor)
__exploded[1]=$((${__exploded[1]}+1))
__exploded[2]=0
;;
patch)
__exploded[2]=$((${__exploded[2]}+1))
;;
esac
eval "$__result=v${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
}
function bump_version {
## First we try to get the next version based on the existing last one
if [ "$scope" == "auto" ]; then
get_scope_auto scope
fi
local __candidatefromlast=$FIRST_VERSION
local __explodedlast
explode_version $lastversion __explodedlast
if [[ -n "${__explodedlast[3]}" ]]; then
# Last version is not final
local __idlast
explode_identifier ${__explodedlast[3]} __idlast
# We get the last, given the desired id based on the scope
__candidatefromlast="v${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
if [[ -n "$identifier" ]]; then
local __nextid="$identifier.1"
if [ "$identifier" == "${__idlast[0]}" ]; then
# We target the same identifier as the last so we increase one
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
__candidatefromlast="$__candidatefromlast-$__nextid"
else
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
__candidatefromlast="$__candidatefromlast-$__nextid"
local __comparedwithlast
compare_versions $__candidatefromlast $lastversion __comparedwithlast
if [ "$__comparedwithlast" == -1 ]; then
get_next_version $__candidatefromlast $scope __candidatefromlast
__candidatefromlast="$__candidatefromlast-$__nextid"
fi
fi
fi
fi
# Then we try to get the version based on the latest final one
local __candidatefromfinal=$FIRST_VERSION
get_next_version $finalversion $scope __candidatefromfinal
if [[ -n "$identifier" ]]; then
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
fi
# Finally we compare both candidates
local __resultversion
local __result
compare_versions $__candidatefromlast $__candidatefromfinal __result
case $__result in
0)
__resultversion=$__candidatefromlast
;;
-1)
__resultversion="$__candidatefromfinal"
;;
1)
__resultversion=$__candidatefromlast
;;
esac
eval "$1=$__resultversion"
}
function increase_version {
local __version=
if [ -z $forcedversion ]; then
bump_version __version
else
if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then
compare_versions $forcedversion $lastversion __result
if [ $__result -le 0 ]; then
echo "Version can't be lower than last version: $lastversion"
exit 1
fi
else
echo "Non valid version to bump"
exit 1
fi
__version=$forcedversion
fi
if [ "$displayonly" == "true" ]; then
echo "$__version"
else
if [ "$forcetag" == "false" ]; then
check_git_dirty_status
fi
local __commitlist
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
__commitlist="$(git log --pretty=oneline | cat)"
else
__commitlist="$(git log --pretty=oneline $finalversion... | cat)"
fi
# If we are forcing a bump, we add bump to the commit list
if [[ -z $__commitlist && "$forcetag" == "true" ]]; then
__commitlist="bump"
fi
if [[ -z $__commitlist ]]; then
echo "No commits since the last final version, not bumping version"
else
if [[ -z $versionname ]]; then
versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
fi
local __message="$versionname
$__commitlist"
# We check we have info on the user
local __username=$(git config user.name)
if [ -z "$__username" ]; then
__username=$(id -u -n)
git config user.name $__username
fi
local __useremail=$(git config user.email)
if [ -z "$__useremail" ]; then
__useremail=$(hostname)
git config user.email "$__username@$__useremail"
fi
git tag -a $__version -m "$__message"
# If we have a remote, we push there
local __remote=${SEMTAG_REMOTE:-$(git remote)}
if [[ -n $__remote ]]; then
git push $__remote $__version > /dev/null
if [ $? -eq 0 ]; then
echo "$__version"
else
echo "Error pushing the tag $__version to $__remote"
exit 1
fi
else
echo "$__version"
fi
fi
fi
}
function check_git_dirty_status {
local __repostatus=
get_work_tree_status __repostatus
if [ "$__repostatus" == "uncommitted" ]; then
echo "ERROR: You have uncommitted changes"
git status --porcelain
exit 1
fi
if [ "$__repostatus" == "unstaged" ]; then
echo "ERROR: You have unstaged changes"
git status --porcelain
exit 1
fi
}
# Get the total amount of lines of code in the repo
function get_total_lines {
local __empty_id="$(git hash-object -t tree /dev/null)"
local __changes="$(git diff --numstat $__empty_id | cat)"
local __added_deleted=$1
get_changed_lines "$__changes" $__added_deleted
}
# Get the total amount of lines of code since the provided tag
function get_sincetag_lines {
local __sincetag=$1
local __changes="$(git diff --numstat $__sincetag | cat)"
local __added_deleted=$2
get_changed_lines "$__changes" $__added_deleted
}
function get_changed_lines {
local __changes_numstat=$1
local __result=$2
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat"
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
local __total_added=0
local __total_deleted=0
for i in "${__changes_array[@]}"
do
if [[ $i =~ $__diff_regex ]] ; then
local __added=${BASH_REMATCH[1]}
local __deleted=${BASH_REMATCH[2]}
__total_added=$(( $__total_added+$__added ))
__total_deleted=$(( $__total_deleted+$__deleted ))
fi
done
eval "$2=( $__total_added $__total_deleted )"
}
function get_scope_auto {
local __verbose=$2
local __total=0
local __since=0
local __scope=
get_total_lines __total
get_sincetag_lines $finalversion __since
local __percentage=0
if [ "$__total" != "0" ]; then
local __percentage=$(( 100*$__since/$__total ))
if [ $__percentage -gt "10" ]; then
__scope="minor"
else
__scope="patch"
fi
fi
eval "$1=$__scope"
if [[ -n "$__verbose" ]]; then
echo "[Auto Scope] Percentage of lines changed: $__percentage"
echo "[Auto Scope] : $__scope"
fi
}
function get_work_tree_status {
# Update the index
git update-index -q --ignore-submodules --refresh > /dev/null
eval "$1="
if ! git diff-files --quiet --ignore-submodules -- > /dev/null
then
eval "$1=unstaged"
fi
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null
then
eval "$1=uncommitted"
fi
}
function get_current {
if [ "$hasversiontag" == "true" ]; then
local __commitcount="$(git rev-list $lastversion.. --count)"
else
local __commitcount="$(git rev-list --count HEAD)"
fi
local __status=
get_work_tree_status __status
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
eval "$1=$lastversion"
else
local __buildinfo="$(git rev-parse --short HEAD)"
local __currentbranch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$__currentbranch" != "master" ]; then
__buildinfo="$__currentbranch.$__buildinfo"
fi
local __suffix=
if [ "$__commitcount" != "0" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__commitcount"
fi
if [ -n "$__status" ]; then
if [ -n "$__suffix" ]; then
__suffix="$__suffix."
fi
__suffix="$__suffix$__status"
fi
__suffix="$__suffix+$__buildinfo"
if [ "$lastversion" == "$finalversion" ]; then
scope="patch"
identifier=
local __bumped=
bump_version __bumped
eval "$1=$__bumped-dev.$__suffix"
else
eval "$1=$lastversion.$__suffix"
fi
fi
}
function init {
git fetch > /dev/null
TAGS="$(git tag)"
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS"
get_latest ${TAG_ARRAY[@]}
currentbranch="$(git rev-parse --abbrev-ref HEAD)"
}
case $ACTION in
--help)
echo -e "$HELP"
;;
--version)
echo -e "${PROG}: $PROG_VERSION"
;;
final)
init
diff=$(git diff master | cat)
if [ "$forcetag" == "false" ]; then
if [ -n "$diff" ]; then
echo "ERROR: Branch must be updated with master for final versions"
exit 1
fi
fi
increase_version
;;
alpha|beta)
init
identifier="$ACTION"
increase_version
;;
candidate)
init
identifier="rc"
increase_version
;;
getlast)
init
echo "$lastversion"
;;
getfinal)
init
echo "$finalversion"
;;
getcurrent)
init
get_current current
echo "$current"
;;
get)
init
echo "Current final version: $finalversion"
echo "Last tagged version: $lastversion"
;;
*)
echo "'$ACTION' is not a valid command, see --help for available commands."
;;
esac
-95
View File
@@ -1,95 +0,0 @@
package helmexec
import (
"fmt"
"io"
"strings"
)
const (
command = "helm"
)
type execer struct {
runner Runner
writer io.Writer
kubeContext string
extra []string
}
func New(writer io.Writer, kubeContext string) *execer {
return &execer{
writer: writer,
kubeContext: kubeContext,
runner: &ShellRunner{},
}
}
func (helm *execer) SetExtraArgs(args ...string) {
helm.extra = args
}
func (helm *execer) AddRepo(name, repository, certfile, keyfile string) error {
var args []string
args = append(args, "repo", "add", name, repository)
if certfile != "" && keyfile != "" {
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
}
out, err := helm.exec(args...)
helm.write(out)
return err
}
func (helm *execer) UpdateRepo() error {
out, err := helm.exec("repo", "update")
helm.write(out)
return err
}
func (helm *execer) UpdateDeps(chart string) error {
out, err := helm.exec("dependency", "update", chart)
helm.write(out)
return err
}
func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
out, err := helm.exec(append([]string{"upgrade", "--install", name, chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) DecryptSecret(name string) (string, error) {
out, err := helm.exec(append([]string{"secrets", "dec", name})...)
helm.write(out)
return name + ".dec", err
}
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
out, err := helm.exec(append([]string{"diff", name, chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) DeleteRelease(name string) error {
out, err := helm.exec("delete", "--purge", name)
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.write([]byte(fmt.Sprintf("exec: helm %s\n", strings.Join(cmdargs, " "))))
return helm.runner.Execute(command, cmdargs)
}
func (helm *execer) write(out []byte) {
if helm.writer != nil {
helm.writer.Write(out)
}
}
-195
View File
@@ -1,195 +0,0 @@
package helmexec
import (
"bytes"
"io"
"reflect"
"testing"
)
// 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(writer io.Writer, kubeContext string) *execer {
execer := New(writer, kubeContext)
execer.runner = &mockRunner{}
return execer
}
// Test methods
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
helm := New(buffer, "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(new(bytes.Buffer), "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_AddRepo(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem")
expected := "exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev\n"
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 = "exec: helm repo add myRepo https://repo.example.com/ --kube-context dev\n"
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
helm := MockExecer(&buffer, "dev")
helm.UpdateRepo()
expected := "exec: helm repo update --kube-context dev\n"
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
helm := MockExecer(&buffer, "dev")
helm.SyncRelease("release", "chart", "--timeout 10", "--wait")
expected := "exec: helm upgrade --install release chart --timeout 10 --wait --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SyncRelease("release", "chart")
expected = "exec: helm upgrade --install release chart --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_UpdateDeps(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")
helm.UpdateDeps("./chart/foo")
expected := "exec: helm dependency update ./chart/foo --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.SetExtraArgs("--verify")
helm.UpdateDeps("./chart/foo")
expected = "exec: helm dependency update ./chart/foo --verify --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_DecryptSecret(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")
helm.DecryptSecret("secretName")
expected := "exec: helm secrets dec secretName --kube-context dev\n"
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
helm := MockExecer(&buffer, "dev")
helm.DiffRelease("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.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
buffer.Reset()
helm.DiffRelease("release", "chart")
expected = "exec: helm diff release chart --kube-context dev\n"
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
helm := MockExecer(&buffer, "dev")
helm.DeleteRelease("release")
expected := "exec: helm delete --purge release --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_exec(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "")
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(nil, "dev")
ret, _ := helm.exec("diff")
if len(ret) != 0 {
t.Error("helmexec.exec() - expected empty return value")
}
buffer.Reset()
helm = MockExecer(&buffer, "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)
}
}
-14
View File
@@ -1,14 +0,0 @@
package helmexec
type Interface interface {
SetExtraArgs(args ...string)
AddRepo(name, repository, certfile, keyfile string) error
UpdateRepo() error
UpdateDeps(chart string) error
SyncRelease(name, chart string, flags ...string) error
DiffRelease(name, chart string, flags ...string) error
DeleteRelease(name string) error
DecryptSecret(name string) (string, error)
}
-30
View File
@@ -1,30 +0,0 @@
package helmexec
import (
"io/ioutil"
"os"
"os/exec"
)
const (
tmpPrefix = "helmfile-"
tmpSuffix = "-exec"
)
type Runner interface {
Execute(cmd string, args []string) ([]byte, error)
}
type ShellRunner struct{}
func (shell ShellRunner) Execute(cmd string, args []string) ([]byte, error) {
dir, err := ioutil.TempDir("", tmpPrefix+cmd+tmpSuffix)
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = 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}"
+512 -187
View File
@@ -2,48 +2,91 @@ package main
import (
"fmt"
"io"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/pkg/app/version"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
DefaultHelmfile = "helmfile.yaml"
DeprecatedHelmfile = "charts.yaml"
)
var logger *zap.SugaredLogger
var Version string
func configureLogging(c *cli.Context) error {
// Valid levels:
// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
logLevel := c.GlobalString("log-level")
if c.GlobalBool("quiet") {
logLevel = "warn"
}
var level zapcore.Level
err := level.Set(logLevel)
if err != nil {
return err
}
logger = helmexec.NewLogger(os.Stderr, logLevel)
if c.App.Metadata == nil {
// Auto-initialised in 1.19.0
// https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19
c.App.Metadata = make(map[string]interface{})
}
c.App.Metadata["logger"] = logger
return nil
}
func main() {
app := cli.NewApp()
app.Name = "helmfile"
app.Usage = ""
app.Version = Version
app.Flags = []cli.Flag{
cliApp := cli.NewApp()
cliApp.Name = "helmfile"
cliApp.Usage = ""
cliApp.Version = version.Version
cliApp.Flags = []cli.Flag{
cli.StringFlag{
Name: "helm-binary, b",
Usage: "path to helm binary",
Value: "helm",
},
cli.StringFlag{
Name: "file, f",
Value: DefaultHelmfile,
Usage: "load config from `FILE`",
Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference",
},
cli.StringFlag{
Name: "environment, e",
Usage: "specify the environment name. defaults to `default`",
},
cli.StringSliceFlag{
Name: "state-values-set",
Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)",
},
cli.StringSliceFlag{
Name: "state-values-file",
Usage: "specify state values in a YAML file",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "silence output",
Usage: "Silence output. Equivalent to log-level warn",
},
cli.StringFlag{
Name: "kube-context",
Usage: "Set kubectl context. Uses current context by default",
},
cli.BoolFlag{
Name: "no-color",
Usage: "Output without color",
},
cli.StringFlag{
Name: "log-level",
Usage: "Set log level, default info",
},
cli.StringFlag{
Name: "namespace, n",
Usage: "Set namespace. Uses the namespace set in the context by default",
Usage: "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}",
},
cli.StringSliceFlag{
Name: "selector, l",
@@ -52,9 +95,36 @@ func main() {
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease`,
},
cli.BoolFlag{
Name: "allow-no-matching-release",
Usage: `Do not exit with an error code if the provided selector has no matching releases.`,
},
cli.BoolFlag{
Name: "interactive, i",
Usage: "Request confirmation before attempting to modify clusters",
},
}
app.Commands = []cli.Command{
cliApp.Before = configureLogging
cliApp.Commands = []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",
},
cli.BoolFlag{
Name: "skip-repos",
Usage: "skip running `helm repo update` before running `helm dependency build`",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Deps(c)
}),
},
{
Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)",
@@ -65,30 +135,23 @@ func main() {
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
state, helm, err := before(c)
if err != nil {
return err
}
args := c.String("args")
if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...)
}
errs := state.SyncRepos(helm)
return clean(state, errs)
},
Action: action(func(run *app.App, c configImpl) error {
return run.Repos(c)
}),
},
{
Name: "charts",
Usage: "sync charts from state file (helm upgrade --install)",
Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
@@ -99,78 +162,128 @@ func main() {
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
state, helm, err := before(c)
if err != nil {
return err
}
args := c.String("args")
if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...)
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
errs := state.SyncReleases(helm, values, workers)
return clean(state, errs)
},
Action: action(func(run *app.App, c configImpl) error {
return run.DeprecatedSyncCharts(c)
}),
},
{
Name: "diff",
Usage: "diff charts from state file against env (helm diff)",
Usage: "diff releases from state file against env (helm diff)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.BoolFlag{
Name: "sync-repos",
Usage: "enable a repo sync prior to diffing",
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
cli.BoolFlag{
Name: "detailed-exitcode",
Usage: "return a non-zero exit code when there are changes",
},
cli.BoolFlag{
Name: "suppress-secrets",
Usage: "suppress secrets in the output. highly recommended to specify on CI/CD use-cases",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.IntFlag{
Name: "context",
Value: 0,
Usage: "output NUM lines of context around changes",
},
},
Action: func(c *cli.Context) error {
state, helm, err := before(c)
if err != nil {
return err
}
args := c.String("args")
if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...)
}
if c.Bool("sync-repos") {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
for _, err := range errs {
fmt.Printf("err: %s\n", err.Error())
}
os.Exit(1)
}
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
errs := state.DiffReleases(helm, values, workers)
return clean(state, errs)
Action: action(func(run *app.App, c configImpl) error {
return run.Diff(c)
}),
},
{
Name: "template",
Usage: "template releases from state file against env (helm template)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm template",
},
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.StringFlag{
Name: "output-dir",
Usage: "output directory to pass to helm template (helm template --output-dir)",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent downloads of release charts",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Template(c)
}),
},
{
Name: "lint",
Usage: "lint charts from state file (helm lint)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent downloads of release charts",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Lint(c)
}),
},
{
Name: "sync",
Usage: "sync all resources from state file (repos, charts and local chart deps)",
Usage: "sync all resources from state file (repos, releases and chart deps)",
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
@@ -185,135 +298,347 @@ func main() {
Value: "",
Usage: "pass args to helm exec",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
state, helm, err := before(c)
if err != nil {
return err
}
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
for _, err := range errs {
fmt.Printf("err: %s\n", err.Error())
}
os.Exit(1)
}
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
for _, err := range errs {
fmt.Printf("err: %s\n", err.Error())
}
os.Exit(1)
}
args := c.String("args")
if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...)
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
errs := state.SyncReleases(helm, values, workers)
return clean(state, errs)
Action: action(func(run *app.App, c configImpl) error {
return run.Sync(c)
}),
},
{
Name: "apply",
Usage: "apply all resources from state file only when there are changes",
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.IntFlag{
Name: "context",
Value: 0,
Usage: "output NUM lines of context around changes",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.BoolFlag{
Name: "suppress-secrets",
Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Apply(c)
}),
},
{
Name: "status",
Usage: "retrieve status of releases in state file",
Flags: []cli.Flag{
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Status(c)
}),
},
{
Name: "delete",
Usage: "delete charts from state file (helm delete)",
Action: func(c *cli.Context) error {
state, helm, err := before(c)
if err != nil {
return err
}
errs := state.DeleteReleases(helm)
return clean(state, errs)
Usage: "DEPRECATED: delete releases from state file (helm delete)",
Flags: []cli.Flag{
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.BoolFlag{
Name: "purge",
Usage: "purge releases i.e. free release names and histories",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Delete(c)
}),
},
{
Name: "destroy",
Usage: "deletes and then purges releases",
Flags: []cli.Flag{
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Destroy(c)
}),
},
{
Name: "test",
Usage: "test releases from state file (helm test)",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "cleanup",
Usage: "delete test pods upon completion",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass additional args to helm exec",
},
cli.IntFlag{
Name: "timeout",
Value: 300,
Usage: "maximum time for tests to run before being considered failed",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Test(c)
}),
},
{
Name: "build",
Usage: "output compiled helmfile state(s) as YAML",
Flags: []cli.Flag{},
Action: action(func(run *app.App, c configImpl) error {
return run.PrintState(c)
}),
},
{
Name: "list",
Usage: "list releases defined in state file",
Flags: []cli.Flag{},
Action: action(func(run *app.App, c configImpl) error {
return run.ListReleases(c)
}),
},
}
err := app.Run(os.Args)
err := cliApp.Run(os.Args)
if err != nil {
log.Printf("err: %s", err.Error())
os.Exit(1)
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(3)
}
}
func before(c *cli.Context) (*state.HelmState, helmexec.Interface, error) {
file := c.GlobalString("file")
quiet := c.GlobalBool("quiet")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
labels := c.GlobalStringSlice("selector")
type configImpl struct {
c *cli.Context
st, err := state.ReadFromFile(file)
if err != nil {
if strings.Contains(err.Error(), fmt.Sprintf("open %s:", DefaultHelmfile)) {
var fallbackErr error
st, fallbackErr = state.ReadFromFile(DeprecatedHelmfile)
if fallbackErr != nil {
return nil, nil, fmt.Errorf("failed to read %s and %s: %v", file, DeprecatedHelmfile, err)
set map[string]interface{}
}
func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
conf := configImpl{
c: c,
}
optsSet := c.GlobalStringSlice("state-values-set")
if len(optsSet) > 0 {
set := map[string]interface{}{}
for i := range optsSet {
ops := strings.Split(optsSet[i], ",")
for j := range ops {
op := strings.SplitN(ops[j], "=", 2)
k := maputil.ParseKey(op[0])
v := op[1]
maputil.Set(set, k, v)
}
log.Printf("warn: charts.yaml is loaded: charts.yaml is deprecated in favor of helmfile.yaml. See https://github.com/roboll/helmfile/issues/25 for more information")
} else {
return nil, nil, fmt.Errorf("failed to read %s: %v", file, err)
}
conf.set = set
}
if st.Context != "" {
if kubeContext != "" {
log.Printf("err: Cannot use option --kube-context and set attribute context.")
os.Exit(1)
}
kubeContext = st.Context
return conf, nil
}
func (c configImpl) Set() []string {
return c.c.StringSlice("set")
}
func (c configImpl) SkipRepos() bool {
return c.c.Bool("skip-repos")
}
func (c configImpl) Values() []string {
return c.c.StringSlice("values")
}
func (c configImpl) Args() string {
return c.c.String("args")
}
func (c configImpl) OutputDir() string {
return c.c.String("output-dir")
}
func (c configImpl) Concurrency() int {
return c.c.Int("concurrency")
}
func (c configImpl) HasCommandName(name string) bool {
return c.c.Command.HasName(name)
}
// DiffConfig
func (c configImpl) SkipDeps() bool {
return c.c.Bool("skip-deps")
}
func (c configImpl) DetailedExitcode() bool {
return c.c.Bool("detailed-exitcode")
}
func (c configImpl) SuppressSecrets() bool {
return c.c.Bool("suppress-secrets")
}
// DeleteConfig
func (c configImpl) Purge() bool {
return c.c.Bool("purge")
}
// TestConfig
func (c configImpl) Cleanup() bool {
return c.c.Bool("cleanup")
}
func (c configImpl) Timeout() int {
return c.c.Int("timeout")
}
// GlobalConfig
func (c configImpl) HelmBinary() string {
return c.c.GlobalString("helm-binary")
}
func (c configImpl) KubeContext() string {
return c.c.GlobalString("kube-context")
}
func (c configImpl) Namespace() string {
return c.c.GlobalString("namespace")
}
func (c configImpl) FileOrDir() string {
return c.c.GlobalString("file")
}
func (c configImpl) Selectors() []string {
return c.c.GlobalStringSlice("selector")
}
func (c configImpl) StateValuesSet() map[string]interface{} {
return c.set
}
func (c configImpl) StateValuesFiles() []string {
return c.c.GlobalStringSlice("state-values-file")
}
func (c configImpl) Interactive() bool {
return c.c.GlobalBool("interactive")
}
func (c configImpl) NoColor() bool {
return c.c.GlobalBool("no-color")
}
func (c configImpl) Context() int {
return c.c.Int("context")
}
func (c configImpl) Logger() *zap.SugaredLogger {
return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
}
func (c configImpl) Env() string {
env := c.c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
if namespace != "" {
if st.Namespace != "" {
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
os.Exit(1)
}
st.Namespace = namespace
}
if len(labels) > 0 {
err = st.FilterReleases(labels)
return env
}
func action(do func(*app.App, configImpl) error) func(*cli.Context) error {
return func(implCtx *cli.Context) error {
conf, err := NewUrfaveCliConfigImpl(implCtx)
if err != nil {
log.Print(err)
os.Exit(1)
return err
}
a := app.New(conf)
a.ErrorHandler = func(err error) error {
return toCliError(implCtx, err)
}
return do(a, conf)
}
var writer io.Writer
if !quiet {
writer = os.Stdout
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)}
clean(st, errs)
}()
return st, helmexec.New(writer, kubeContext), nil
}
func clean(state *state.HelmState, errs []error) error {
if errs == nil {
errs = []error{}
}
cleanErrs := state.Clean()
if cleanErrs != nil {
errs = append(errs, cleanErrs...)
}
if errs != nil && len(errs) > 0 {
for _, err := range errs {
fmt.Printf("err: %s\n", err.Error())
func toCliError(c *cli.Context, err error) error {
if err != nil {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
noMatchingExitCode := 3
if c.GlobalBool("allow-no-matching-release") {
noMatchingExitCode = 0
}
return cli.NewExitError(e.Error(), noMatchingExitCode)
case *app.Error:
return cli.NewExitError(e.Error(), e.Code())
default:
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
}
os.Exit(1)
}
return nil
return err
}
+1226
View File
File diff suppressed because it is too large Load Diff
+3754
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
package app
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
// Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
//
// Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
func AskForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true
} else if response == "n" || response == "no" {
return false
}
}
}
+147
View File
@@ -0,0 +1,147 @@
package app
import "go.uber.org/zap"
type ConfigProvider interface {
Args() string
HelmBinary() string
FileOrDir() string
KubeContext() string
Namespace() 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
SkipDeps() bool
SuppressSecrets() bool
NoColor() bool
Context() int
concurrencyConfig
interactive
loggingConfig
}
type SyncConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
concurrencyConfig
loggingConfig
}
type DiffConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
SuppressSecrets() bool
DetailedExitcode() bool
NoColor() bool
Context() int
concurrencyConfig
}
type DeleteConfigProvider interface {
Args() string
Purge() bool
interactive
loggingConfig
concurrencyConfig
}
type DestroyConfigProvider interface {
Args() string
interactive
loggingConfig
concurrencyConfig
}
type TestConfigProvider interface {
Args() string
Timeout() int
Cleanup() bool
concurrencyConfig
}
type LintConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
concurrencyConfig
}
type TemplateConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
OutputDir() string
concurrencyConfig
}
type StatusesConfigProvider interface {
Args() string
concurrencyConfig
}
type StateConfigProvider interface {
}
type concurrencyConfig interface {
Concurrency() int
}
type loggingConfig interface {
Logger() *zap.SugaredLogger
}
type interactive interface {
Interactive() bool
}
+22
View File
@@ -0,0 +1,22 @@
package app
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/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/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())
}
+35
View File
@@ -0,0 +1,35 @@
package app
import (
"github.com/roboll/helmfile/pkg/state"
)
type Context struct {
updatedRepos map[string]struct{}
}
func NewContext() Context {
return Context{
updatedRepos: map[string]struct{}{},
}
}
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
var errs []error
allUpdated := true
for _, r := range st.Repositories {
_, exists := ctx.updatedRepos[r.Name]
allUpdated = allUpdated && exists
}
if !allUpdated {
errs = st.SyncRepos(helm)
for _, r := range st.Repositories {
ctx.updatedRepos[r.Name] = struct{}{}
}
}
return errs
}
+220
View File
@@ -0,0 +1,220 @@
package app
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"sort"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/variantdev/vals"
"go.uber.org/zap"
)
type desiredStateLoader struct {
KubeContext string
Reverse bool
env string
namespace string
readFile func(string) ([]byte, error)
fileExists func(string) (bool, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger
helm helmexec.Interface
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)
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 ld.Reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
}
if ld.KubeContext != "" {
if st.HelmDefaults.KubeContext != "" {
return nil, errors.New("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
}
st.HelmDefaults.KubeContext = ld.KubeContext
}
if ld.namespace != "" {
if st.OverrideNamespace != "" {
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
}
st.OverrideNamespace = ld.namespace
}
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.helm, a.valsRuntime)
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) {
parts := bytes.Split(content, []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
}
if finalState == nil {
finalState = currentState
} else {
if err := mergo.Merge(finalState, currentState, mergo.WithOverride); err != nil {
return nil, err
}
}
env = &finalState.Env
ld.logger.Debugf("merged environment: %v", env)
}
return finalState, nil
}
+19
View File
@@ -0,0 +1,19 @@
package app
import (
"fmt"
"strings"
)
type NoMatchingHelmfileError struct {
selectors []string
env string
}
func (e *NoMatchingHelmfileError) Error() string {
return fmt.Sprintf(
"err: no releases found that matches specified selector(%s) and environment(%s), in any helmfile",
strings.Join(e.selectors, ", "),
e.env,
)
}
+28
View File
@@ -0,0 +1,28 @@
package app
import (
"github.com/roboll/helmfile/pkg/state"
"gopkg.in/yaml.v2"
)
type LoadOpts struct {
Selectors []string
Environment state.SubhelmfileEnvironmentSpec
// CalleePath is the absolute path to the file being loaded
CalleePath string
}
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
}
+125
View File
@@ -0,0 +1,125 @@
package app
import (
"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
Ask func(string) bool
}
func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run {
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) Deps(c DepsConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
if !c.SkipRepos() {
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
return errs
}
}
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) Status(c StatusesConfigProvider) []error {
workers := c.Concurrency()
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.ReleaseStatuses(r.helm, workers)
}
func (r *Run) Diff(c DiffConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
return errs
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
opts := &state.DiffOpts{
Context: c.Context(),
NoColor: c.NoColor(),
Set: c.Set(),
}
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true, opts)
return errs
}
func (r *Run) Test(c TestConfigProvider) []error {
cleanup := c.Cleanup()
timeout := c.Timeout()
concurrency := c.Concurrency()
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.TestReleases(r.helm, cleanup, timeout, concurrency)
}
func (r *Run) Lint(c LintConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
values := c.Values()
args := argparser.GetArgs(c.Args(), st)
workers := c.Concurrency()
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
opts := &state.LintOpts{
Set: c.Set(),
}
return st.LintReleases(helm, values, args, workers, opts)
}
+148
View File
@@ -0,0 +1,148 @@
package app
import (
"bytes"
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/tmpl"
"strings"
)
func prependLineNumbers(text string) string {
buf := bytes.NewBufferString("")
lines := strings.Split(text, "\n")
for i, line := range lines {
buf.WriteString(fmt.Sprintf("%2d: %s\n", i, line))
}
return buf.String()
}
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", 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
return firstPassEnv, nil
}
}
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.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
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", filename, prependLineNumbers(yamlBuf.String()))
}
if prestate != nil {
firstPassEnv = &prestate.Env
}
return firstPassEnv, prestate
}
type RenderOpts struct {
}
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 := map[string]interface{}{}
if prestate != nil {
prestate.Env = *finalEnv
vals, err = prestate.Values()
if err != nil {
return nil, err
}
}
if prestate != nil {
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", filename, prependLineNumbers(string(content)))
}
return nil, err
}
if r.logger != nil {
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
return yamlBuf, nil
}
+234
View File
@@ -0,0 +1,234 @@
package app
import (
"os"
"strings"
"testing"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/testhelper"
"gopkg.in/yaml.v2"
)
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
testfs := testhelper.NewTestFs(files)
return &desiredStateLoader{
env: env,
namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"),
readFile: testfs.ReadFile,
fileExists: testfs.FileExists,
abs: testfs.Abs,
glob: testfs.Glob,
}, testfs
}
func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
releases:
- name: {{ readFile "other/default/values.yaml" }}
chart: mychart1
`)
files := map[string]string{
"/path/to/default/values.yaml": ``,
"/path/to/other/default/values.yaml": `SecondPass`,
}
r, testfs := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if testfs.FileReaderCalls() > 2 {
t.Error("reader should be called only twice")
}
if state.Releases[0].Name != "SecondPass" {
t.Errorf("release name should have ben set as SecondPass")
}
}
func TestReadFromYaml_RenderTemplate(t *testing.T) {
defaultValuesYaml := `
releaseName: "hello"
conditionalReleaseTag: "yes"
`
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
releases:
- name: {{ .Environment.Values.releaseName }}
chart: mychart1
{{ if (eq .Environment.Values.conditionalReleaseTag "yes") }}
- name: conditionalRelease
{{ end }}
`)
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r, _ := makeLoader(files, "staging")
// test the double rendering
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(state.Releases) != 2 {
t.Fatal("there should be 2 releases")
}
if state.Releases[0].Name != "hello" {
t.Errorf("release name should be hello")
}
if state.Releases[1].Name != "conditionalRelease" {
t.Error("conditional release should have been present")
}
}
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
defaultValuesYaml := ``
yamlContent := []byte(`
environments:
staging:
values:
- default/values.yaml
production:
{{ if (eq .Environment.Values.releaseName "a") }} # line 8
releases:
- name: a
chart: mychart1
{{ end }}
`)
files := map[string]string{
"/path/to/default/values.yaml": defaultValuesYaml,
}
r, _ := makeLoader(files, "staging")
// test the double rendering
_, 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)
}
}
// This test shows that a gotmpl reference will get rendered correctly
// even if the pre-render disables the readFile and exec functions.
// This does not apply to .gotmpl files, which is a nice side-effect.
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
defaultValuesYamlGotmpl := `
releaseName: {{ readFile "nonIgnoredFile" }}
`
yamlContent := []byte(`
environments:
staging:
values:
- values.yaml.gotmpl
production:
{{ if (eq .Environment.Values.releaseName "release-a") }} # line 8
releases:
- name: a
chart: mychart1
{{ end }}
`)
files := map[string]string{
"/path/to/nonIgnoredFile": `release-a`,
"/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl,
}
r, _ := makeLoader(files, "staging")
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
var state state.HelmState
yaml.Unmarshal(rendered.Bytes(), &state)
if len(state.Releases) != 1 {
t.Fatal("there should be 1 release")
}
if state.Releases[0].Name != "a" {
t.Fatal("release should have been declared")
}
}
func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
yamlContent := []byte(`releases:
- name: {{ .Namespace }}-myrelease
chart: mychart
`)
files := map[string]string{}
r, _ := makeLoader(files, "staging")
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var state state.HelmState
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
if state.Releases[0].Name != "namespace-myrelease" {
t.Errorf("release name should be namespace-myrelease")
}
}
func TestReadFromYaml_HelmfileShouldBeResilentToTemplateErrors(t *testing.T) {
yamlContent := []byte(`environments:
staging:
production:
releases:
{{ if (eq .Environment.Name "production" }} # notice syntax error: unclosed left paren
- name: prod-myrelease
{{ else }}
- name: myapp
{{ end }}
chart: mychart
`)
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
+107
View File
@@ -0,0 +1,107 @@
package argparser
import (
"fmt"
"github.com/roboll/helmfile/pkg/state"
"strings"
)
type keyVal struct {
key string
val string
spaceFlag bool
}
type argMap struct {
//m map[string]string
m map[string][]*keyVal
flags []string
}
func (a *argMap) SetArg(flag, arg string, isSpace bool) {
if _, exists := a.m[flag]; !exists {
keyarg := &keyVal{key: flag, val: arg, spaceFlag: isSpace}
a.m[flag] = append(a.m[flag], keyarg)
a.flags = append(a.flags, flag)
} else if flag == "--set" || flag == "-f" || flag == "--values" {
keyarg := &keyVal{key: flag, val: arg, spaceFlag: isSpace}
a.m[flag] = append(a.m[flag], keyarg)
}
}
func newArgMap() *argMap {
return &argMap{m: map[string][]*keyVal{}}
}
func GetArgs(args string, state *state.HelmState) []string {
argsMap := newArgMap()
if len(args) > 0 {
argsVals := strings.Split(args, " ")
for index, arg := range argsVals {
if strings.HasPrefix(arg, "--") {
argVal := strings.SplitN(arg, "=", 2)
if len(argVal) > 1 {
arg := argVal[0]
value := argVal[1]
argsMap.SetArg(arg, value, false)
} else {
//check if next value is arg to flag
if index+1 < len(argsVals) {
nextVal := argsVals[index+1]
if strings.HasPrefix(nextVal, "--") {
argsMap.SetArg(arg, "", false)
} else {
argsMap.SetArg(arg, nextVal, true)
}
} else {
argsMap.SetArg(arg, "", false)
}
}
}
}
}
if len(state.HelmDefaults.Args) > 0 {
for _, arg := range state.HelmDefaults.Args {
var flag string
var val string
argsNum, _ := fmt.Sscanf(arg, "--%s %s", &flag, &val)
if argsNum == 2 {
argsMap.SetArg(flag, val, true)
} else {
argVal := strings.SplitN(arg, "=", 2)
argFirst := argVal[0]
if len(argVal) > 1 {
val = argVal[1]
argsMap.SetArg(argFirst, val, false)
} else {
argsMap.SetArg(argFirst, "", false)
}
}
}
}
var argArr []string
for _, flag := range argsMap.flags {
val := argsMap.m[flag]
for _, obj := range val {
if obj.val != "" {
if obj.spaceFlag {
argArr = append(argArr, obj.key, obj.val)
} else {
argArr = append(argArr, fmt.Sprintf("%s=%s", obj.key, obj.val))
}
} else {
argArr = append(argArr, fmt.Sprintf("%s", obj.key))
}
}
}
state.HelmDefaults.Args = argArr
return state.HelmDefaults.Args
}
+45
View File
@@ -0,0 +1,45 @@
package argparser
import (
"github.com/roboll/helmfile/pkg/state"
"strings"
"testing"
)
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}
receivedArgs := GetArgs(args, testState)
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, " "))
}
}
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}
receivedArgs := GetArgs(args, testState)
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, " "))
}
}
func compareArgs(expectedArgs string, args []string) bool {
if strings.Compare(strings.Join(args, " "), expectedArgs) != 0 {
return false
}
return true
}
+66
View File
@@ -0,0 +1,66 @@
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); err != nil {
return nil, err
}
}
return &copy, nil
}
+111
View File
@@ -0,0 +1,111 @@
package event
import (
"fmt"
"os"
"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"`
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
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,
}
}
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,
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
}
+128
View File
@@ -0,0 +1,128 @@
package event
import (
"fmt"
"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) 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", []string{}, true},
"foo",
true,
"",
},
{
"okhooké",
&Hook{"okhook2", []string{"foo"}, "ok", []string{}, false},
"foo",
true,
"",
},
{
"missinghook1",
&Hook{"okhook1", []string{"foo"}, "ok", []string{}, false},
"bar",
false,
"",
},
{
"nohook1",
nil,
"bar",
false,
"",
},
{
"nghook1",
&Hook{"nghook1", []string{"foo"}, "ng", []string{}, false},
"foo",
false,
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
},
{
"nghook2",
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}, false},
"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)
}
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.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)
}
}
if observedLogs.Len() != 0 && !hooks[0].ShowLogs {
t.Errorf("unexpected error for case \"%s\": Logs should not be created : %v", c.name, observedLogs.All())
}
}
}
+171
View File
@@ -0,0 +1,171 @@
package exectest
import (
"errors"
"fmt"
"strings"
"sync"
"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
UpdateDepsCallbacks map[string]func(string) error
DiffMutex *sync.Mutex
ChartsMutex *sync.Mutex
ReleasesMutex *sync.Mutex
}
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) error {
helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password}
return nil
}
func (helm *Helm) UpdateRepo() 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, 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) IsHelm3() bool {
return false
}
func (helm *Helm) sync(m *sync.Mutex, f func()) {
if m != nil {
m.Lock()
defer m.Unlock()
}
f()
}
+45
View File
@@ -0,0 +1,45 @@
package helmexec
import (
"os"
"path/filepath"
)
type HelmContext struct {
Tillerless bool
TillerNamespace string
HistoryMax int
WorkerIndex int
}
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{}
}
}
+348
View File
@@ -0,0 +1,348 @@
package helmexec
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type decryptedSecret struct {
mutex sync.RWMutex
bytes []byte
}
type execer struct {
helmBinary string
isHelm3 bool
runner Runner
logger *zap.SugaredLogger
kubeContext string
extra []string
decryptedSecretMutex sync.Mutex
decryptedSecrets map[string]*decryptedSecret
}
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 detectHelm3(helmBinary string, logger *zap.SugaredLogger, runner Runner) bool {
// Support explicit opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" {
return true
}
// Autodetect from `helm verison`
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil {
panic(err)
}
return strings.HasPrefix(string(bytes), "v3.")
}
// New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{
helmBinary: helmBinary,
isHelm3: detectHelm3(helmBinary, logger, runner),
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) error {
var args []string
args = append(args, "repo", "add", name, repository)
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{})
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) 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(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(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(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 {
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"
}
decFilename := strings.Replace(absPath, ".yaml", decSuffix, 1)
secretBytes, err := ioutil.ReadFile(decFilename)
if err != nil {
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()
}
tmpFile, err := ioutil.TempFile("", "secret")
if err != nil {
return "", err
}
_, err = tmpFile.Write(secret.bytes)
if err != nil {
return "", err
}
return tmpFile.Name(), 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{})
helm.write(out)
return err
}
func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags ...string) error {
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 {
helm.write(out)
return err
}
}
} else {
helm.write(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(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) 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(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(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(cmdargs, "--kube-context", helm.kubeContext)
}
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
helm.logger.Debug(cmd)
bytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
helm.logger.Debugf("%s: %s", cmd, bytes)
return bytes, err
}
func (helm *execer) info(out []byte) {
if len(out) > 0 {
helm.logger.Infof("%s", out)
}
}
func (helm *execer) write(out []byte) {
if len(out) > 0 {
fmt.Printf("%s\n", out)
}
}
func (helm *execer) IsHelm3() bool {
return helm.isHelm3
}
+522
View File
@@ -0,0 +1,522 @@
package helmexec
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"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, 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(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
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/", "ca.crt", "", "", "", "")
expected = `Adding repo myRepo https://repo.example.com/
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --kube-context dev
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --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
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
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
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(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
expected := `Upgrading release=release, chart=chart
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
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(HelmContext{}, "release", "chart")
expected = `Upgrading release=release, chart=chart
exec: helm upgrade --install --reset-values release chart --kube-context dev
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_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")
expected := `Upgrading release=release, chart=chart
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
exec: helm tiller run foo -- 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)
}
}
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
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
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("foo", "./chart/foo")
expected := `Building dependency release=foo, chart=./chart/foo
exec: helm dependency build ./chart/foo --kube-context dev
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("foo", "./chart/foo")
expected = `Building dependency release=foo, chart=./chart/foo
exec: helm dependency build ./chart/foo --verify --kube-context dev
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(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 secrets dec %s/secretName --kube-context dev
exec: helm secrets dec %s/secretName --kube-context dev:
Preparing to decrypt secret %s/secretName
Found secret in cache %s/secretName
`, cwd, cwd, cwd, cwd, cwd, cwd)
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(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
expected := `Comparing release=release, chart=chart
exec: helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
exec: helm diff upgrade --reset-values --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(HelmContext{}, "release", "chart")
expected = `Comparing release=release, chart=chart
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev:
`
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", "--timeout 10", "--wait")
expected := `Comparing release=release, chart=chart
exec: helm tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
exec: helm tiller run -- helm diff upgrade --reset-values --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)
}
}
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 delete release --kube-context dev
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(HelmContext{}, "release", "--purge")
expected := `Deleting release
exec: helm delete release --purge --kube-context dev
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(HelmContext{}, "release")
expected := `Testing release
exec: helm test release --kube-context dev
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(HelmContext{}, "release", "--cleanup", "--timeout", "60")
expected := `Testing release
exec: helm test release --cleanup --timeout 60 --kube-context dev
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(HelmContext{}, "myRelease")
expected := `Getting status myRelease
exec: helm status myRelease --kube-context dev
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, "")
env := map[string]string{}
helm.exec([]string{"version"}, env)
expected := `exec: helm version
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"}, env)
expected = `exec: helm diff release chart --timeout 10 --wait --kube-context dev
exec: helm diff release chart --timeout 10 --wait --kube-context dev:
`
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 version --kube-context dev
exec: helm version --kube-context dev:
`
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 version foo --kube-context dev
exec: helm version foo --kube-context dev:
`
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
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 lint path/to/chart --values file.yml --kube-context dev
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
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
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 template path/to/chart --name release --values file.yml --kube-context dev
exec: helm template path/to/chart --name release --values file.yml --kube-context dev:
`
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")
}
}
+36
View File
@@ -0,0 +1,36 @@
package helmexec
import (
"fmt"
"path/filepath"
"strings"
)
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
return ExitError{
Message: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
Code: exitStatus,
}
}
func indent(text string) string {
lines := strings.Split(text, "\n")
for i := range lines {
lines[i] = " " + lines[i]
}
return strings.Join(lines, "\n")
}
// 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
}
+28
View File
@@ -0,0 +1,28 @@
package helmexec
// Interface for executing helm commands
type Interface interface {
SetExtraArgs(args ...string)
SetHelmBinary(bin string)
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
UpdateRepo() 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, flags ...string) error
TemplateRelease(name, chart string, flags ...string) error
Fetch(chart 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
}
type DependencyUpdater interface {
UpdateDeps(chart string) error
IsHelm3() bool
}
+99
View File
@@ -0,0 +1,99 @@
package helmexec
import (
"bytes"
"errors"
"fmt"
"go.uber.org/zap"
"os"
"os/exec"
"strings"
"syscall"
)
const (
tmpPrefix = "helmfile-"
tmpSuffix = "-exec"
)
// Runner interface for shell commands
type Runner interface {
Execute(cmd string, args []string, env map[string]string) ([]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 combinedOutput(preparedCmd, shell.Logger)
}
func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]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
c.Stdout = &stdout
c.Stderr = &stderr
err := c.Run()
o := stdout.Bytes()
e := stderr.Bytes()
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, exitStatus, string(e))
default:
panic(fmt.Sprintf("unexpected error: %v", err))
}
}
return o, 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)
wanted[pair[0]] = pair[1]
}
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])
}
}
}
}
+262
View File
@@ -0,0 +1,262 @@
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) (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
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
}
+165
View File
@@ -0,0 +1,165 @@
package remote
import (
"fmt"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"os"
"testing"
)
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")
}
})
}
}
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)
}
+457
View File
@@ -0,0 +1,457 @@
package state
import (
"fmt"
"github.com/Masterminds/semver"
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"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
)
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\"", 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 resolve %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
}
+322
View File
@@ -0,0 +1,322 @@
package state
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/variantdev/vals"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
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)
helm helmexec.Interface
valsRuntime vals.Evaluator
Strict bool
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
}
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), helm helmexec.Interface, valsRuntime vals.Evaluator) *StateCreator {
return &StateCreator{
logger: logger,
readFile: readFile,
fileExists: fileExists,
abs: abs,
glob: glob,
Strict: true,
helm: helm,
valsRuntime: valsRuntime,
}
}
// 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
state.helm = c.helm
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
}
state.logger = c.logger
state.readFile = c.readFile
state.removeFile = os.Remove
state.fileExists = c.fileExists
state.glob = c.glob
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 := state.loadEnvValues(env, failOnMissingEnv, ctxEnv, c.readFile, c.glob)
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
e.Defaults, err = state.loadValuesEntries(nil, state.DefaultValues)
if 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
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 (st *HelmState) loadEnvValues(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)
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 = st.scatterGatherEnvSecretFiles(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); err != nil {
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
}
newEnv = &intEnv
}
return newEnv, nil
}
func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
var errs []error
inputs := envSecretFiles
inputsSize := len(inputs)
type secretResult struct {
result map[string]interface{}
err error
path string
}
secrets := make(chan string, inputsSize)
results := make(chan secretResult, inputsSize)
helm := st.helm
st.scatterGather(0, inputsSize,
func() {
for _, secretFile := range envSecretFiles {
secrets <- secretFile
}
close(secrets)
},
func(id int) {
for path := range secrets {
release := &ReleaseSpec{}
flags := st.appendConnectionFlags([]string{}, release)
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
if err != nil {
results <- secretResult{nil, err, path}
continue
}
bytes, err := readFile(decFile)
if err != nil {
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
continue
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), 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{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
continue
}
results <- secretResult{vals, nil, path}
}
},
func() {
for i := 0; i < inputsSize; i++ {
result := <-results
if result.err != nil {
errs = append(errs, result.err)
} else {
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride); err != nil {
errs = append(errs, fmt.Errorf("failed to load environment secrets file \"%s\": %v", result.path, err))
}
}
}
close(results)
},
)
if len(errs) > 1 {
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{}) (map[string]interface{}, error) {
envVals := map[string]interface{}{}
valuesEntries := append([]interface{}{}, entries...)
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger)
var err error
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
if err != nil {
return nil, err
}
return envVals, nil
}
+351
View File
@@ -0,0 +1,351 @@
package state
import (
"io/ioutil"
"path/filepath"
"reflect"
"testing"
"github.com/roboll/helmfile/pkg/testhelper"
"go.uber.org/zap"
. "gotest.tools/assert"
"gotest.tools/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,
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_InexistentEnv(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
_, err := createFromYaml(yamlContent, yamlFile, "production", logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
production:
values:
- foo.yaml
- bar.yaml.gotmpl
releases:
- name: myrelease
namespace: mynamespace
chart: mychart
values:
- values.yaml.gotmpl
`)
fooYamlFile := "/example/path/to/foo.yaml"
fooYamlContent := []byte(`foo: foo
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
baz: "{{ readFile \"baz.txt\" }}"`)
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
barYamlContent := []byte(`foo: FOO
bar: {{ readFile "bar.txt" }}
`)
barTextFile := "/example/path/to/bar.txt"
barTextContent := []byte("BAR")
expected := map[string]interface{}{
"foo": "FOO",
"bar": "BAR",
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
"baz": "{{ readFile \"baz.txt\" }}",
}
valuesFile := "/example/path/to/values.yaml.gotmpl"
valuesContent := []byte(`env: {{ .Environment.Name }}`)
expectedValues := `env: production`
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil).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)
}
actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actualValues := string(actualValuesData)
if !reflect.DeepEqual(expectedValues, actualValues) {
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
}
}
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
releases: mychart
`)
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected an error for wrong key 'releases' which is not in struct")
}
}
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unxpected error: %v", err)
}
if state.Releases[0].Name != "myrelease" {
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
}
if state.Releases[0].Chart != "mychart" {
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
}
}
func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease1
chart: mychart1
releases:
- name: myrelease2
chart: mychart2
`)
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err == nil {
t.Error("expected error")
}
}
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
tier: frontend
foo: bar
- name: myrelease2
chart: mychart2
labels:
tier: frontend
- name: myrelease3
chart: mychart3
labels:
tier: backend
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, false}},
}
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestReadFromYaml_FilterNegatives(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease2
chart: mychart2
labels:
stage: post
- name: myrelease3
chart: mychart3
`)
cases := []struct {
filter LabelFilter
results []bool
}{
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
[]bool{true, false, false}},
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
}
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
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)
}
}
+15
View File
@@ -0,0 +1,15 @@
package state
type EnvironmentSpec struct {
Values []interface{} `yaml:"values,omitempty"`
Secrets []string `yaml:"secrets,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"`
}
+83
View File
@@ -0,0 +1,83 @@
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/tmpl"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"path/filepath"
)
type EnvironmentValuesLoader struct {
storage *Storage
readFile func(string) ([]byte, error)
logger *zap.SugaredLogger
}
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger) *EnvironmentValuesLoader {
return &EnvironmentValuesLoader{
storage: storage,
readFile: readFile,
logger: logger,
}
}
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
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); err != nil {
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
}
}
}
return result, nil
}
+55
View File
@@ -0,0 +1,55 @@
package state
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 _, d := range release.Dependencies {
var dep string
if d.Alias != "" {
dep += d.Alias + "="
}
dep += d.Chart
if d.Version != "" {
dep += ":" + d.Version
}
flags = append(flags, "--dependency", dep)
}
for _, adopt := range release.Adopt {
flags = append(flags, "--adopt", adopt)
}
jsonPatches := release.JSONPatches
if len(jsonPatches) > 0 {
generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler)
if err != nil {
return nil, err
}
for _, f := range generatedFiles {
flags = append(flags, "--json-patch", f)
}
release.generatedValues = append(release.generatedValues, generatedFiles...)
}
strategicMergePatches := release.StrategicMergePatches
if len(strategicMergePatches) > 0 {
generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler)
if err != nil {
return nil, err
}
for _, f := range generatedFiles {
flags = append(flags, "--strategic-merge-patch", f)
}
release.generatedValues = append(release.generatedValues, generatedFiles...)
}
return flags, nil
}
+208
View File
@@ -0,0 +1,208 @@
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, ts := range result.Secrets {
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
}
+21
View File
@@ -0,0 +1,21 @@
package state
import (
"fmt"
)
const ReleaseErrorCodeFailure = 1
type ReleaseError struct {
*ReleaseSpec
err error
Code int
}
func (e *ReleaseError) Error() string {
return fmt.Sprintf("failed processing release %s: %v", e.Name, e.err.Error())
}
func newReleaseError(release *ReleaseSpec, err error) *ReleaseError {
return &ReleaseError{release, err, ReleaseErrorCodeFailure}
}
@@ -15,14 +15,16 @@ type ReleaseFilter interface {
// LabelFilter matches a release with the given positive lables. Negative labels
// invert the match for cases such as tier!=backend
type LabelFilter struct {
positiveLabels map[string]string
negativeLabels map[string]string
positiveLabels [][]string
negativeLabels [][]string
}
// Match will match a release that has the same labels as the filter
func (l LabelFilter) Match(r ReleaseSpec) bool {
if len(l.positiveLabels) > 0 {
for k, v := range l.positiveLabels {
for _, element := range l.positiveLabels {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
return false
} else if rVal != v {
@@ -30,8 +32,11 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
}
}
}
if len(l.negativeLabels) > 0 {
for k, v := range l.negativeLabels {
for _, element := range l.negativeLabels {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
return true
} else if rVal == v {
@@ -45,19 +50,19 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
// ParseLabels takes a label in the form foo=bar,baz!=bat and returns a LabelFilter that will match the labels
func ParseLabels(l string) (LabelFilter, error) {
lf := LabelFilter{}
lf.positiveLabels = map[string]string{}
lf.negativeLabels = map[string]string{}
lf.positiveLabels = [][]string{}
lf.negativeLabels = [][]string{}
var err error
labels := strings.Split(l, ",")
for _, label := range labels {
if match, _ := regexp.MatchString("^[a-zA-Z0-9_-]+!=[a-zA-Z0-9_-]+$", label); match == true { // k!=v case
kv := strings.Split(label, "!=")
lf.negativeLabels[kv[0]] = kv[1]
lf.negativeLabels = append(lf.negativeLabels, kv)
} else if match, _ := regexp.MatchString("^[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+$", label); match == true { // k=v case
kv := strings.Split(label, "=")
lf.positiveLabels[kv[0]] = kv[1]
lf.positiveLabels = append(lf.positiveLabels, kv)
} else { // malformed case
err = fmt.Errorf("Malformed label: %s. Expected label in form k=v or k!=v", label)
return lf, fmt.Errorf("Malformed label: %s. Expected label in form k=v or k!=v", label)
}
}
return lf, err
+2030
View File
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
package state
import (
"fmt"
"reflect"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/tmpl"
"gopkg.in/yaml.v2"
)
func (st *HelmState) Values() (map[string]interface{}, error) {
vals := map[string]interface{}{}
if err := mergo.Merge(&vals, st.Env.Defaults, mergo.WithOverride); err != nil {
return nil, err
}
if err := mergo.Merge(&vals, st.Env.Values, mergo.WithOverride); err != nil {
return nil, err
}
vals, err := maputil.CastKeysToStrings(vals)
if err != nil {
return nil, err
}
return vals, nil
}
func (st *HelmState) mustLoadVals() map[string]interface{} {
vals, err := st.Values()
if err != nil {
panic(err)
}
return vals
}
func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
Environment: st.Env,
Namespace: st.OverrideNamespace,
Values: st.mustLoadVals(),
}
}
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, err := st.Values()
if err != nil {
return nil, err
}
for i, rt := range st.Releases {
successFlag := false
for it, prev := 0, &rt; it < 6; it++ {
tmplData := releaseTemplateData{
Environment: st.Env,
Release: *prev,
Values: 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
}
+256
View File
@@ -0,0 +1,256 @@
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: []string{"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: []string{"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: ".",
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
}
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: ".",
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
}
r, err := state.ExecuteTemplates()
if err == nil {
t.Errorf("Expected error, got valid response: %v", r)
t.FailNow()
}
})
}
}
+207
View File
@@ -0,0 +1,207 @@
package state
import (
"fmt"
"sort"
"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) {
st.logger.Debugf("worker %d/%d started", id, concurrency)
receiveInputsAndProduceIntermediates(id)
st.logger.Debugf("worker %d/%d finished", id, concurrency)
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("sending result for release: %s\n", release.Name)
results <- result{release: release, err: err}
st.logger.Debugf("sent result for release: %s\n", release.Name)
}
},
func() {
for i := range inputs {
st.logger.Debugf("receiving result %d", i)
r := <-results
if r.err != nil {
errs = append(errs, fmt.Errorf("release \"%s\" failed: %v", r.release.Name, r.err))
} else {
st.logger.Debugf("received result for release \"%s\"", r.release.Name)
}
st.logger.Debugf("received result for %d", i)
}
},
)
if len(errs) != 0 {
return errs
}
return nil
}
type PlanOpts struct {
Reverse bool
}
func (st *HelmState) PlanReleases(reverse bool) ([][]Release, error) {
marked, err := st.SelectReleasesWithOverrides()
if err != nil {
return nil, err
}
groups, err := SortedReleaseGroups(marked, reverse)
if err != nil {
return nil, err
}
return groups, nil
}
func SortedReleaseGroups(releases []Release, reverse bool) ([][]Release, error) {
groups, err := GroupReleasesByDependency(releases)
if err != nil {
return nil, err
}
if reverse {
sort.Slice(groups, func(i, j int) bool {
return j < i
})
}
return groups, nil
}
func GroupReleasesByDependency(releases []Release) ([][]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 {
d.Add(id, dag.Dependencies(r.Needs))
}
}
for _, r := range releases {
if !r.Filtered {
for _, n := range r.Needs {
if _, ok := idToReleases[n]; !ok {
id := ReleaseToID(&r.ReleaseSpec)
return nil, fmt.Errorf("%q has dependency to inexistent release %q", id, n)
}
}
}
}
plan, err := d.Plan()
if err != nil {
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 {
releases, ok := idToReleases[id]
if !ok {
panic(fmt.Errorf("bug: unexpectedly failed to get releases for id %q", id))
}
releasesInGroup = append(releasesInGroup, releases...)
}
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)
}
+30
View File
@@ -0,0 +1,30 @@
package state
import (
"github.com/roboll/helmfile/pkg/environment"
)
// TemplateSpec defines the structure of a reusable and composable template for helm releases.
type TemplateSpec struct {
ReleaseSpec `yaml:",inline"`
}
// EnvironmentTemplateData provides variables accessible while executing golang text/template expressions in helmfile and values YAML files
type EnvironmentTemplateData struct {
// Environment is accessible as `.Environment` from any template executed by the renderer
Environment environment.Environment
// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
Namespace string
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
Values map[string]interface{}
}
// releaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
type releaseTemplateData struct {
// Environment is accessible as `.Environment` from any template expression executed by the renderer
Environment environment.Environment
// Release is accessible as `.Release` from any template expression executed by the renderer
Release ReleaseSpec
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
Values map[string]interface{}
}
+52
View File
@@ -0,0 +1,52 @@
package state
import (
"path/filepath"
"regexp"
"strings"
)
func isLocalChart(chart string) bool {
regex, _ := regexp.Compile("^[.]?./")
matched := regex.MatchString(chart)
if matched {
return true
}
uriLike := strings.Index(chart, "://") > -1
if uriLike {
return false
}
return chart == "" ||
chart[0] == '/' ||
strings.Index(chart, "/") == -1 ||
len(strings.Split(chart, "/")) != 2
}
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
if isLocalChart(repoAndChart) {
return "", "", false
}
parts := strings.Split(repoAndChart, "/")
if len(parts) != 2 {
return "", "", false
}
repo := parts[0]
chart := parts[1]
return repo, chart, true
}
// normalizeChart allows for the distinction between a file path reference and repository references.
// - Any single (or double character) followed by a `/` will be considered a local file reference and
// be constructed relative to the `base path`.
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
func normalizeChart(basePath, chart string) string {
if !isLocalChart(chart) || chart[0] == '/' {
return chart
}
return filepath.Join(basePath, chart)
}
+147
View File
@@ -0,0 +1,147 @@
package state
import "testing"
func TestIsLocalChart(t *testing.T) {
testcases := []struct {
input string
expected bool
}{
{
input: "mychart",
expected: true,
},
{
input: "stable/mysql",
expected: false,
},
{
input: "./charts/myapp",
expected: true,
},
{
input: "charts/mysubsystem/myapp",
expected: true,
},
{
input: "./charts/mysubsystem/myapp",
expected: true,
},
{
input: "/charts/mysubsystem/myapp",
expected: true,
},
{
// Regression test case for:
// * https://github.com/roboll/helmfile/issues/675
// * https://github.com/roboll/helmfile/issues/687
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
expected: false,
},
{
input: "https://example.com/bar.tgz",
expected: false,
},
}
for i := range testcases {
testcase := testcases[i]
actual := isLocalChart(testcase.input)
if testcase.expected != actual {
t.Errorf("unexpected result: isLocalChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
}
}
}
func TestResolveRemortChart(t *testing.T) {
testcases := []struct {
input string
repo string
chart string
remote bool
}{
{
input: "mychart",
remote: false,
},
{
input: "stable/mysql",
repo: "stable",
chart: "mysql",
remote: true,
},
{
input: "./charts/myapp",
remote: false,
},
{
input: "charts/mysubsystem/myapp",
remote: false,
},
{
input: "./charts/mysubsystem/myapp",
remote: false,
},
{
input: "/charts/mysubsystem/myapp",
remote: false,
},
{
// Regression test case for:
// * https://github.com/roboll/helmfile/issues/675
// * https://github.com/roboll/helmfile/issues/687
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
remote: false,
},
{
input: "https://example.com/bar.tgz",
remote: false,
},
}
for i := range testcases {
testcase := testcases[i]
repo, chart, actual := resolveRemoteChart(testcase.input)
if testcase.remote != actual {
t.Fatalf("unexpected result: reolveRemoteChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.remote, actual)
}
if testcase.repo != repo {
t.Errorf("unexpected repo: %s: expected=%v, got=%v", testcase.input, testcase.repo, repo)
}
if testcase.chart != chart {
t.Errorf("unexpected chart: %s: expected=%v, got=%v", testcase.input, testcase.chart, chart)
}
}
}
func TestNormalizeChart(t *testing.T) {
testcases := []struct {
input string
expected string
}{
{
input: "mychart",
expected: "/path/to/mychart",
},
{
input: "/charts/mychart",
expected: "/charts/mychart",
},
}
for i := range testcases {
testcase := testcases[i]
actual := normalizeChart("/path/to", testcase.input)
if testcase.expected != actual {
t.Fatalf("unexpected result: normalizeChart(\"/path/to\", \"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package testhelper
import (
"bytes"
"fmt"
"io"
"math"
"strings"
"github.com/aryann/difflib"
)
func Diff(want, got string, context int) (string, bool) {
records := difflib.Diff(
strings.Split(want, "\n"),
strings.Split(got, "\n"),
)
w := &bytes.Buffer{}
changed := checkAndPrintRecords(w, records, context)
return w.String(), changed
}
func checkAndPrintRecords(w io.Writer, records []difflib.DiffRecord, context int) bool {
var changed bool
if context >= 0 {
distances := calculateDistances(records)
omitting := false
for i, diff := range records {
if diff.Delta != difflib.Common {
changed = true
}
if distances[i] > context {
if !omitting {
fmt.Fprintln(w, "...")
omitting = true
}
} else {
omitting = false
fmt.Fprintln(w, formatRecord(diff))
}
}
} else {
for _, diff := range records {
if diff.Delta != difflib.Common {
changed = true
}
fmt.Fprintln(w, formatRecord(diff))
}
}
return changed
}
func formatRecord(diff difflib.DiffRecord) string {
var prefix string
switch diff.Delta {
case difflib.RightOnly:
prefix = "+ "
case difflib.LeftOnly:
prefix = "- "
case difflib.Common:
prefix = " "
}
return prefix + diff.Payload
}
// Shamelessly and thankfully copied from https://github.com/databus23/helm-diff/blob/99b8474af7726ca6f57b37b0b8b8f3cd36c991e8/diff/diff.go#L116
func calculateDistances(diffs []difflib.DiffRecord) map[int]int {
distances := map[int]int{}
// Iterate forwards through diffs, set 'distance' based on closest 'change' before this line
change := -1
for i, diff := range diffs {
if diff.Delta != difflib.Common {
change = i
}
distance := math.MaxInt32
if change != -1 {
distance = i - change
}
distances[i] = distance
}
// Iterate backwards through diffs, reduce 'distance' based on closest 'change' after this line
change = -1
for i := len(diffs) - 1; i >= 0; i-- {
diff := diffs[i]
if diff.Delta != difflib.Common {
change = i
}
if change != -1 {
distance := change - i
if distance < distances[i] {
distances[i] = distance
}
}
}
return distances
}
+137
View File
@@ -0,0 +1,137 @@
package testhelper
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type TestFs struct {
Cwd string
dirs map[string]bool
files map[string]string
GlobFixtures map[string][]string
fileReaderCalls int
successfulReads []string
}
func NewTestFs(files map[string]string) *TestFs {
dirs := map[string]bool{}
for abs, _ := range files {
for d := filepath.Dir(abs); !dirs[d]; d = filepath.Dir(d) {
dirs[d] = true
fmt.Fprintf(os.Stderr, "testfs: recognized dir: %s\n", d)
}
}
return &TestFs{
Cwd: "/path/to",
dirs: dirs,
files: files,
successfulReads: []string{},
GlobFixtures: map[string][]string{},
}
}
func (f *TestFs) FileExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.files[path]
} else {
_, ok = f.files[filepath.Join(f.Cwd, path)]
}
return ok
}
func (f *TestFs) FileExists(path string) (bool, error) {
return f.FileExistsAt(path), nil
}
func (f *TestFs) DirectoryExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
_, ok = f.dirs[path]
} else {
_, ok = f.dirs[filepath.Join(f.Cwd, path)]
}
return ok
}
func (f *TestFs) ReadFile(filename string) ([]byte, error) {
var str string
var ok bool
if filename[0] == '/' {
str, ok = f.files[filename]
} else {
str, ok = f.files[filepath.Join(f.Cwd, filename)]
}
if !ok {
return []byte(nil), fmt.Errorf("no registered file found: %s", filename)
}
f.fileReaderCalls += 1
f.successfulReads = append(f.successfulReads, filename)
return []byte(str), nil
}
func (f *TestFs) SuccessfulReads() []string {
return f.successfulReads
}
func (f *TestFs) FileReaderCalls() int {
return f.fileReaderCalls
}
func (f *TestFs) Glob(relPattern string) ([]string, error) {
var pattern string
if relPattern[0] == '/' {
pattern = relPattern
} else {
pattern = filepath.Join(f.Cwd, relPattern)
}
fixtures, ok := f.GlobFixtures[pattern]
if ok {
return fixtures, nil
}
matches := []string{}
for name, _ := range f.files {
matched, err := filepath.Match(pattern, name)
if err != nil {
return nil, err
}
if matched {
matches = append(matches, name)
}
}
return matches, nil
}
func (f *TestFs) Abs(path string) (string, error) {
var p string
if path[0] == '/' {
p = path
} else {
p = filepath.Join(f.Cwd, path)
}
return filepath.Clean(p), nil
}
func (f *TestFs) Getwd() (string, error) {
return f.Cwd, nil
}
func (f *TestFs) Chdir(dir string) error {
if _, ok := f.dirs[dir]; ok {
f.Cwd = dir
return nil
}
return fmt.Errorf("unexpected chdir \"%s\"", dir)
}
+7
View File
@@ -0,0 +1,7 @@
package tmpl
type Context struct {
preRender bool
basePath string
readFile func(string) ([]byte, error)
}
+208
View File
@@ -0,0 +1,208 @@
package tmpl
import (
"fmt"
"gopkg.in/yaml.v2"
"io"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"text/template"
)
type Values = map[string]interface{}
func (c *Context) createFuncMap() template.FuncMap {
funcMap := template.FuncMap{
"exec": c.Exec,
"readFile": c.ReadFile,
"toYaml": ToYaml,
"fromYaml": FromYaml,
"setValueAtPath": SetValueAtPath,
"requiredEnv": RequiredEnv,
"get": get,
"getOrNil": getOrNil,
"tpl": c.Tpl,
}
if c.preRender {
// disable potential side-effect template calls
funcMap["exec"] = func(string, []interface{}, ...string) (string, error) {
return "", nil
}
funcMap["readFile"] = func(string) (string, error) {
return "", nil
}
}
return funcMap
}
func (c *Context) Exec(command string, args []interface{}, inputs ...string) (string, error) {
var input string
if len(inputs) > 0 {
input = inputs[0]
}
strArgs := make([]string, len(args))
for i, a := range args {
switch a.(type) {
case string:
strArgs[i] = a.(string)
default:
return "", fmt.Errorf("unexpected type of arg \"%s\" in args %v at index %d", reflect.TypeOf(a), args, i)
}
}
cmd := exec.Command(command, strArgs...)
cmd.Dir = c.basePath
writeErrs := make(chan error)
cmdErrs := make(chan error)
cmdOuts := make(chan []byte)
if len(input) > 0 {
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
go func(input string, stdin io.WriteCloser) {
defer stdin.Close()
defer close(writeErrs)
size := len(input)
var n int
var err error
i := 0
for {
n, err = io.WriteString(stdin, input[i:])
if err != nil {
writeErrs <- fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
break
}
i += n
if n == size {
break
}
}
}(input, stdin)
}
go func() {
defer close(cmdOuts)
defer close(cmdErrs)
bytes, err := cmd.Output()
if err != nil {
cmdErrs <- fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err)
} else {
cmdOuts <- bytes
}
}()
for {
select {
case bytes := <-cmdOuts:
return string(bytes), nil
case err := <-cmdErrs:
return "", err
case err := <-writeErrs:
return "", err
}
}
}
func (c *Context) ReadFile(filename string) (string, error) {
var path string
if filepath.IsAbs(filename) {
path = filename
} else {
path = filepath.Join(c.basePath, filename)
}
bytes, err := c.readFile(path)
if err != nil {
return "", err
}
return string(bytes), nil
}
func (c *Context) Tpl(text string, data interface{}) (string, error) {
buf, err := c.RenderTemplateToBuffer(text, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func ToYaml(v interface{}) (string, error) {
data, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}
func FromYaml(str string) (Values, error) {
m := Values{}
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
return nil, fmt.Errorf("%s, offending yaml: %s", err, str)
}
return m, nil
}
func SetValueAtPath(path string, value interface{}, values Values) (Values, error) {
var current interface{}
current = values
components := strings.Split(path, ".")
pathToMap := components[:len(components)-1]
key := components[len(components)-1]
for _, k := range pathToMap {
var elem interface{}
switch typedCurrent := current.(type) {
case map[string]interface{}:
v, exists := typedCurrent[k]
if !exists {
return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" does not exist", path, k)
}
elem = v
case map[interface{}]interface{}:
v, exists := typedCurrent[k]
if !exists {
return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" does not exist", path, k)
}
elem = v
default:
return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, k)
}
switch typedElem := elem.(type) {
case map[string]interface{}, map[interface{}]interface{}:
current = typedElem
default:
return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, k)
}
}
switch typedCurrent := current.(type) {
case map[string]interface{}:
typedCurrent[key] = value
case map[interface{}]interface{}:
typedCurrent[key] = value
default:
return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, key)
}
return values, nil
}
func RequiredEnv(name string) (string, error) {
if val, exists := os.LookupEnv(name); exists && len(val) > 0 {
return val, nil
}
return "", fmt.Errorf("required env var `%s` is not set", name)
}
+135
View File
@@ -0,0 +1,135 @@
package tmpl
import (
"fmt"
"path/filepath"
"reflect"
"testing"
)
func TestReadFile(t *testing.T) {
expected := `foo:
bar: BAR
`
expectedFilename := "values.yaml"
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(expected), nil
}}
actual, err := ctx.ReadFile(expectedFilename)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestReadFile_PassAbsPath(t *testing.T) {
expected := `foo:
bar: BAR
`
expectedFilename, _ := filepath.Abs("values.yaml")
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(expected), nil
}}
actual, err := ctx.ReadFile(expectedFilename)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestToYaml(t *testing.T) {
expected := `foo:
bar: BAR
`
vals := Values(map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "BAR",
},
})
actual, err := ToYaml(vals)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestFromYaml(t *testing.T) {
raw := `foo:
bar: BAR
`
expected := Values(map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "BAR",
},
})
actual, err := FromYaml(raw)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestSetValueAtPath_OneComponent(t *testing.T) {
input := map[string]interface{}{
"foo": "",
}
expected := map[string]interface{}{
"foo": "FOO",
}
actual, err := SetValueAtPath("foo", "FOO", input)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestSetValueAtPath_TwoComponents(t *testing.T) {
input := map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "",
},
}
expected := map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "FOO_BAR",
},
}
actual, err := SetValueAtPath("foo.bar", "FOO_BAR", input)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestTpl(t *testing.T) {
text := `foo: {{ .foo }}
`
expected := `foo: FOO
`
ctx := &Context{basePath: "."}
actual, err := ctx.Tpl(text, map[string]interface{}{"foo": "FOO"})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
+41
View File
@@ -0,0 +1,41 @@
package tmpl
import (
"bytes"
"github.com/Masterminds/sprig"
"text/template"
)
func (c *Context) stringTemplate() *template.Template {
funcMap := sprig.TxtFuncMap()
for name, f := range c.createFuncMap() {
funcMap[name] = f
}
tmpl := template.New("stringTemplate").Funcs(funcMap)
if c.preRender {
tmpl = tmpl.Option("missingkey=zero")
} else {
tmpl = tmpl.Option("missingkey=error")
}
return tmpl
}
func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) {
var t, parseErr = c.stringTemplate().Parse(s)
if parseErr != nil {
return nil, parseErr
}
var tplString bytes.Buffer
var d interface{}
if len(data) > 0 {
d = data[0]
}
var execErr = t.Execute(&tplString, d)
if execErr != nil {
return &tplString, execErr
}
return &tplString, nil
}
+252
View File
@@ -0,0 +1,252 @@
package tmpl
import (
"fmt"
"os"
"reflect"
"testing"
)
func TestRenderTemplate_Values(t *testing.T) {
valuesYamlContent := `foo:
bar: BAR
`
expected := `foo:
bar: FOO_BAR
`
expectedFilename := "values.yaml"
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(`{{ readFile "values.yaml" | fromYaml | setValueAtPath "foo.bar" "FOO_BAR" | toYaml }}`)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestRenderTemplate_WithData(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ .foo.bar }}
`
expected := `foo:
bar: FOO_BAR
`
expectedFilename := "values.yaml"
data := map[string]interface{}{
"foo": map[string]interface{}{
"bar": "FOO_BAR",
},
}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestRenderTemplate_AccessingMissingKeyWithGetOrNil(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ . | getOrNil "foo.bar" }}
`
expected := `foo:
bar: <no value>
`
expectedFilename := "values.yaml"
data := map[string]interface{}{}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestRenderTemplate_Defaulting(t *testing.T) {
valuesYamlContent := `foo:
bar: {{ . | getOrNil "foo.bar" | default "DEFAULT" }}
`
expected := `foo:
bar: DEFAULT
`
expectedFilename := "values.yaml"
data := map[string]interface{}{}
ctx := &Context{readFile: func(filename string) ([]byte, error) {
if filename != expectedFilename {
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
}
return []byte(valuesYamlContent), nil
}}
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func renderTemplateToString(s string, data ...interface{}) (string, error) {
ctx := &Context{readFile: func(filename string) ([]byte, error) {
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
}}
tplString, err := ctx.RenderTemplateToBuffer(s, data...)
if err != nil {
return "", err
}
return tplString.String(), nil
}
func Test_renderTemplateToString(t *testing.T) {
type args struct {
s string
envs map[string]string
data interface{}
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "simple replacement",
args: args{
s: "{{ env \"HF_TEST_VAR\" }}",
envs: map[string]string{
"HF_TEST_VAR": "content",
},
},
want: "content",
wantErr: false,
},
{
name: "two replacements",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" }}{{ env \"HF_TEST_BETA\" }}",
envs: map[string]string{
"HF_TEST_ALPHA": "first",
"HF_TEST_BETA": "second",
},
},
want: "firstsecond",
wantErr: false,
},
{
name: "replacement and comment",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" }}{{/* comment */}}",
envs: map[string]string{
"HF_TEST_ALPHA": "first",
},
},
want: "first",
wantErr: false,
},
{
name: "global template function",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" | len }}",
envs: map[string]string{
"HF_TEST_ALPHA": "abcdefg",
},
},
want: "7",
wantErr: false,
},
{
name: "get",
args: args{
s: `{{ . | get "Foo" }}, {{ . | get "Bar" "2" }}`,
envs: map[string]string{},
data: map[string]interface{}{
"Foo": "1",
},
},
want: "1, 2",
wantErr: false,
},
{
name: "env var not set",
args: args{
s: "{{ env \"HF_TEST_NONE\" }}",
envs: map[string]string{
"HF_TEST_THIS": "first",
},
},
want: "",
},
{
name: "undefined function",
args: args{
s: "{{ env foo }}",
envs: map[string]string{
"foo": "bar",
},
},
wantErr: true,
},
{
name: "required env var",
args: args{
s: "{{ requiredEnv \"HF_TEST\" }}",
envs: map[string]string{
"HF_TEST": "value",
},
},
want: "value",
wantErr: false,
},
{
name: "required env var not set",
args: args{
s: "{{ requiredEnv \"HF_TEST_NONE\" }}",
envs: map[string]string{},
},
wantErr: true,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.args.envs {
err := os.Setenv(k, v)
if err != nil {
t.Error("renderTemplateToString() could not set env var for testing")
}
}
got, err := renderTemplateToString(tt.args.s, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("renderTemplateToString() for %s = %v, want %v", tt.name, got, tt.want)
}
})
}
}
+80
View File
@@ -0,0 +1,80 @@
package tmpl
import (
"bytes"
"io/ioutil"
"fmt"
"strings"
)
type FileRenderer struct {
ReadFile func(string) ([]byte, error)
Context *Context
Data interface{}
}
func NewFileRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *FileRenderer {
return &FileRenderer{
ReadFile: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: data,
}
}
func NewFirstPassRenderer(basePath string, data interface{}) *FileRenderer {
return &FileRenderer{
ReadFile: ioutil.ReadFile,
Context: &Context{
preRender: true,
basePath: basePath,
readFile: ioutil.ReadFile,
},
Data: data,
}
}
func (r *FileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) {
content, err := r.ReadFile(file)
if err != nil {
return nil, err
}
return r.RenderTemplateContentToBuffer(content)
}
// RenderToBytes loads the content of the file.
// If its extension is `gotmpl` it treats the content as a go template and renders it.
func (r *FileRenderer) RenderToBytes(path string) ([]byte, error) {
var yamlBytes []byte
splits := strings.Split(path, ".")
if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" {
yamlBuf, err := r.RenderTemplateFileToBuffer(path)
if err != nil {
return nil, fmt.Errorf("failed to render [%s], because of %v", path, err)
}
yamlBytes = yamlBuf.Bytes()
} else {
var err error
yamlBytes, err = r.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load [%s]: %v", path, err)
}
}
return yamlBytes, nil
}
func (r *FileRenderer) RenderTemplateContentToBuffer(content []byte) (*bytes.Buffer, error) {
return r.Context.RenderTemplateToBuffer(string(content), r.Data)
}
func (r *FileRenderer) RenderTemplateContentToString(content []byte) (string, error) {
buf, err := r.Context.RenderTemplateToBuffer(string(content), r.Data)
if err != nil {
return "", err
}
return buf.String(), nil
}
+67
View File
@@ -0,0 +1,67 @@
package tmpl
import (
"fmt"
"github.com/roboll/helmfile/pkg/environment"
"reflect"
"testing"
)
var emptyEnvTmplData = map[string]interface{}{
"Environment": environment.EmptyEnvironment,
"Namespace": "",
}
func TestRenderToBytes_Gotmpl(t *testing.T) {
valuesYamlTmplContent := `foo:
bar: '{{ readFile "data.txt" }}'
`
dataFileContent := "FOO_BAR"
expected := `foo:
bar: 'FOO_BAR'
`
dataFile := "data.txt"
valuesTmplFile := "values.yaml.gotmpl"
r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename {
case valuesTmplFile:
return []byte(valuesYamlTmplContent), nil
case dataFile:
return []byte(dataFileContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
}, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesTmplFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := string(buf)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
func TestRenderToBytes_Yaml(t *testing.T) {
valuesYamlContent := `foo:
bar: '{{ readFile "data.txt" }}'
`
expected := `foo:
bar: '{{ readFile "data.txt" }}'
`
valuesFile := "values.yaml"
r := NewFileRenderer(func(filename string) ([]byte, error) {
switch filename {
case valuesFile:
return []byte(valuesYamlContent), nil
}
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
}, "", emptyEnvTmplData)
buf, err := r.RenderToBytes(valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := string(buf)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
+91
View File
@@ -0,0 +1,91 @@
package tmpl
import (
"fmt"
"reflect"
"strings"
)
type noValueError struct {
msg string
}
func (e *noValueError) Error() string {
return e.msg
}
func get(path string, varArgs ...interface{}) (interface{}, error) {
var defSet bool
var def interface{}
var obj interface{}
switch len(varArgs) {
case 1:
defSet = false
def = nil
obj = varArgs[0]
case 2:
defSet = true
def = varArgs[0]
obj = varArgs[1]
default:
return nil, fmt.Errorf("unexpected number of args pased to the template function get(path, [def, ]obj): expected 1 or 2, got %d, args was %v", len(varArgs), varArgs)
}
if path == "" {
return obj, nil
}
keys := strings.Split(path, ".")
var v interface{}
var ok bool
switch typedObj := obj.(type) {
case map[string]interface{}:
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
case map[interface{}]interface{}:
v, ok = typedObj[keys[0]]
if !ok {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
}
default:
maybeStruct := reflect.ValueOf(typedObj)
if maybeStruct.Kind() != reflect.Struct {
return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])}
} else if maybeStruct.NumField() < 1 {
return nil, &noValueError{fmt.Sprintf("no accessible struct fields for key \"%s\"", keys[0])}
}
f := maybeStruct.FieldByName(keys[0])
if !f.IsValid() {
if defSet {
return def, nil
}
return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)}
}
v = f.Interface()
}
if defSet {
return get(strings.Join(keys[1:], "."), def, v)
}
return get(strings.Join(keys[1:], "."), v)
}
func getOrNil(path string, o interface{}) (interface{}, error) {
v, err := get(path, o)
if err != nil {
switch err.(type) {
case *noValueError:
return nil, nil
default:
return nil, err
}
}
return v, nil
}
+128
View File
@@ -0,0 +1,128 @@
package tmpl
import (
"testing"
)
type EmptyStruct struct {
}
func TestGetStruct(t *testing.T) {
type Foo struct{ Bar string }
obj := struct{ Foo }{Foo{Bar: "Bar"}}
v1, err := get("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
_, err = get("Foo.baz", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
_, err = get("foo", EmptyStruct{})
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetMap(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
v1, err := get("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
_, err = get("Foo.baz", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGet_Default(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{}, "foo": 1}
v1, err := get("Foo.Bar", "Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := get("Baz", "Baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != "Baz" {
t.Errorf("unexpected value for path Baz in %v: expected=Baz, actual=%v", obj, v2)
}
_, err = get("foo.Bar", "fooBar", obj)
if err == nil {
t.Errorf("expected error but was not occurred")
}
}
func TestGetOrNilStruct(t *testing.T) {
type Foo struct{ Bar string }
obj := struct{ Foo }{Foo{Bar: "Bar"}}
v1, err := getOrNil("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := getOrNil("Foo.baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != nil {
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
}
}
func TestGetOrNilMap(t *testing.T) {
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
v1, err := getOrNil("Foo.Bar", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v1 != "Bar" {
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
}
v2, err := getOrNil("Foo.baz", obj)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if v2 != nil {
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
}
}
+30
View File
@@ -0,0 +1,30 @@
package tmpl
type templateTextRenderer struct {
ReadText func(string) ([]byte, error)
Context *Context
Data interface{}
}
type TextRenderer interface {
RenderTemplateText(text string) (string, error)
}
func NewTextRenderer(readFile func(filename string) ([]byte, error), basePath string, data interface{}) *templateTextRenderer {
return &templateTextRenderer{
ReadText: readFile,
Context: &Context{
basePath: basePath,
readFile: readFile,
},
Data: data,
}
}
func (r *templateTextRenderer) RenderTemplateText(text string) (string, error) {
buf, err := r.Context.RenderTemplateToBuffer(text, r.Data)
if err != nil {
return "", err
}
return buf.String(), nil
}
-506
View File
@@ -1,506 +0,0 @@
package state
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"text/template"
"github.com/roboll/helmfile/helmexec"
"bytes"
"path"
"regexp"
yaml "gopkg.in/yaml.v1"
)
type HelmState struct {
BaseChartPath string
Context string `yaml:"context"`
DeprecatedReleases []ReleaseSpec `yaml:"charts"`
Namespace string `yaml:"namespace"`
Repositories []RepositorySpec `yaml:"repositories"`
Releases []ReleaseSpec `yaml:"releases"`
}
type RepositorySpec struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
}
type ReleaseSpec struct {
// Chart is the name of the chart being installed to create this release
Chart string `yaml:"chart"`
Version string `yaml:"version"`
Verify bool `yaml:"verify"`
// Name is the name of this release
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Labels map[string]string `yaml:"labels"`
Values []string `yaml:"values"`
Secrets []string `yaml:"secrets"`
SetValues []SetValue `yaml:"set"`
// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
EnvValues []SetValue `yaml:"env"`
// generatedValues are values that need cleaned up on exit
generatedValues []string
}
type SetValue struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
func ReadFromFile(file string) (*HelmState, error) {
content, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return readFromYaml(content, file)
}
func readFromYaml(content []byte, file string) (*HelmState, error) {
var state HelmState
state.BaseChartPath, _ = filepath.Abs(path.Dir(file))
if err := yaml.Unmarshal(content, &state); err != nil {
return nil, 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{}
}
return &state, nil
}
var stringTemplateFuncMap = template.FuncMap{
"env": getEnvVar,
}
func stringTemplate() *template.Template {
return template.New("stringTemplate").Funcs(stringTemplateFuncMap)
}
func getEnvVar(envVarName string) (string, error) {
envVarValue, isSet := os.LookupEnv(envVarName)
if !isSet {
errMsg := fmt.Sprintf("Environment Variable '%s' is not set. Please make sure it is set and try again.", envVarName)
return "", errors.New(errMsg)
}
return envVarValue, nil
}
func renderTemplateString(s string) (string, error) {
var t, parseErr = stringTemplate().Parse(s)
if parseErr != nil {
return "", parseErr
}
var tplString bytes.Buffer
var execErr = t.Execute(&tplString, nil)
if execErr != nil {
return "", execErr
}
return tplString.String(), nil
}
func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if state.Namespace != "" {
spec.Namespace = state.Namespace
}
}
func (state *HelmState) SyncRepos(helm helmexec.Interface) []error {
errs := []error{}
for _, repo := range state.Repositories {
url, err := renderTemplateString(repo.URL)
if err != nil {
errs = append(errs, err)
continue
}
if err := helm.AddRepo(repo.Name, url, repo.CertFile, repo.KeyFile); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return errs
}
if err := helm.UpdateRepo(); err != nil {
return []error{err}
}
return nil
}
func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
errs := []error{}
jobQueue := make(chan *ReleaseSpec)
doneQueue := make(chan bool)
errQueue := make(chan error)
if workerLimit < 1 {
workerLimit = len(state.Releases)
}
for w := 1; w <= workerLimit; w++ {
go func() {
for release := range jobQueue {
nameRendered, err := renderTemplateString(release.Name)
if err != nil {
errQueue <- err
doneQueue <- true
}
state.applyDefaultsTo(release)
flags, flagsErr := flagsForRelease(helm, state.BaseChartPath, release)
if flagsErr != nil {
errQueue <- flagsErr
doneQueue <- true
continue
}
haveValueErr := false
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
errQueue <- err
haveValueErr = true
}
if _, err := os.Stat(valfile); os.IsNotExist(err) {
errQueue <- err
haveValueErr = true
}
flags = append(flags, "--values", valfile)
}
if haveValueErr {
doneQueue <- true
continue
}
chart := normalizeChart(state.BaseChartPath, release.Chart)
if err := helm.SyncRelease(nameRendered, chart, flags...); err != nil {
errQueue <- err
}
doneQueue <- true
}
}()
}
go func() {
for i := 0; i < len(state.Releases); i++ {
jobQueue <- &state.Releases[i]
}
close(jobQueue)
}()
for i := 0; i < len(state.Releases); {
select {
case err := <-errQueue:
errs = append(errs, err)
case <-doneQueue:
i++
}
}
if len(errs) != 0 {
return errs
}
return nil
}
func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
var wgRelease sync.WaitGroup
var wgError sync.WaitGroup
errs := []error{}
jobQueue := make(chan *ReleaseSpec, len(state.Releases))
errQueue := make(chan error)
if workerLimit < 1 {
workerLimit = len(state.Releases)
}
wgRelease.Add(len(state.Releases))
for w := 1; w <= workerLimit; w++ {
go func() {
for release := range jobQueue {
errs := []error{}
renderedName, err := renderTemplateString(release.Name)
if err != nil {
errs = append(errs, err)
}
// Plugin command doesn't support explicit namespace
release.Namespace = ""
flags, err := flagsForRelease(helm, state.BaseChartPath, release)
if err != nil {
errs = append(errs, err)
}
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
errs = append(errs, err)
}
if _, err := os.Stat(valfile); os.IsNotExist(err) {
errs = append(errs, err)
}
flags = append(flags, "--values", valfile)
}
if len(errs) == 0 {
if err := helm.DiffRelease(renderedName, normalizeChart(state.BaseChartPath, release.Chart), flags...); err != nil {
errs = append(errs, err)
}
}
for _, err := range errs {
errQueue <- err
}
wgRelease.Done()
}
}()
}
wgError.Add(1)
go func() {
for err := range errQueue {
errs = append(errs, err)
}
wgError.Done()
}()
for i := 0; i < len(state.Releases); i++ {
jobQueue <- &state.Releases[i]
}
close(jobQueue)
wgRelease.Wait()
close(errQueue)
wgError.Wait()
if len(errs) != 0 {
return errs
}
return nil
}
func (state *HelmState) DeleteReleases(helm helmexec.Interface) []error {
var wg sync.WaitGroup
errs := []error{}
for _, release := range state.Releases {
wg.Add(1)
go func(wg *sync.WaitGroup, release ReleaseSpec) {
if err := helm.DeleteRelease(release.Name); err != nil {
errs = append(errs, err)
}
wg.Done()
}(&wg, release)
}
wg.Wait()
if len(errs) != 0 {
return errs
}
return nil
}
// Clean will remove any generated secrets
func (state *HelmState) Clean() []error {
errs := []error{}
for _, release := range state.Releases {
for _, value := range release.generatedValues {
err := os.Remove(value)
if err != nil {
errs = append(errs, err)
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
func (state *HelmState) FilterReleases(labels []string) error {
var filteredReleases []ReleaseSpec
releaseSet := map[string]ReleaseSpec{}
filters := []ReleaseFilter{}
for _, label := range labels {
f, err := ParseLabels(label)
if err != nil {
return err
}
filters = append(filters, f)
}
for _, r := range state.Releases {
if r.Labels == nil {
r.Labels = map[string]string{}
}
// Let the release name be used as a tag
r.Labels["name"] = r.Name
for _, f := range filters {
if r.Labels == nil {
r.Labels = map[string]string{}
}
if f.Match(r) {
releaseSet[r.Name] = r
continue
}
}
}
for _, r := range releaseSet {
filteredReleases = append(filteredReleases, r)
}
state.Releases = filteredReleases
return nil
}
func (state *HelmState) UpdateDeps(helm helmexec.Interface) []error {
errs := []error{}
for _, release := range state.Releases {
if isLocalChart(release.Chart) {
if err := helm.UpdateDeps(normalizeChart(state.BaseChartPath, release.Chart)); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}
// normalizeChart allows for the distinction between a file path reference and repository references.
// - Any single (or double character) followed by a `/` will be considered a local file reference and
// be constructed relative to the `base path`.
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
func normalizeChart(basePath, chart string) string {
if !isLocalChart(chart) {
return chart
}
return filepath.Join(basePath, chart)
}
func isLocalChart(chart string) bool {
regex, _ := regexp.Compile("^[.]?./")
return regex.MatchString(chart)
}
func flagsForRelease(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Version != "" {
flags = append(flags, "--version", release.Version)
}
if release.Verify {
flags = append(flags, "--verify")
}
if release.Namespace != "" {
namespaceRendered, err := renderTemplateString(release.Namespace)
if err != nil {
return nil, err
}
flags = append(flags, "--namespace", namespaceRendered)
}
for _, value := range release.Values {
valfile := filepath.Join(basePath, value)
valfileRendered, err := renderTemplateString(valfile)
if err != nil {
return nil, err
}
if _, err := os.Stat(valfileRendered); os.IsNotExist(err) {
return nil, err
}
flags = append(flags, "--values", valfileRendered)
}
for _, value := range release.Secrets {
valfile := filepath.Join(basePath, value)
path, err := renderTemplateString(valfile)
if err != nil {
return nil, err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
valfileRendered, err := helm.DecryptSecret(path)
if err != nil {
return nil, err
}
release.generatedValues = append(release.generatedValues, valfileRendered)
flags = append(flags, "--values", valfileRendered)
}
if len(release.SetValues) > 0 {
val := []string{}
for _, set := range release.SetValues {
renderedValue, err := renderTemplateString(set.Value)
if err != nil {
return nil, err
}
val = append(val, fmt.Sprintf("%s=%s", set.Name, renderedValue))
}
flags = append(flags, "--set", strings.Join(val, ","))
}
/***********
* START 'env' section for backwards compatibility
***********/
// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
if len(release.EnvValues) > 0 {
val := []string{}
envValErrs := []string{}
for _, set := range release.EnvValues {
value, isSet := os.LookupEnv(set.Value)
if isSet {
val = append(val, fmt.Sprintf("%s=%s", set.Name, value))
} else {
errMsg := fmt.Sprintf("\t%s", set.Value)
envValErrs = append(envValErrs, errMsg)
}
}
if len(envValErrs) != 0 {
joinedEnvVals := strings.Join(envValErrs, "\n")
errMsg := fmt.Sprintf("Environment Variables not found. Please make sure they are set and try again:\n%s", joinedEnvVals)
return nil, errors.New(errMsg)
}
flags = append(flags, "--set", strings.Join(val, ","))
}
/**************
* END 'env' section for backwards compatibility
**************/
return flags, nil
}
-569
View File
@@ -1,569 +0,0 @@
package state
import (
"os"
"reflect"
"testing"
"errors"
"strings"
)
func TestReadFromYaml(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease
namespace: mynamespace
chart: mychart
`)
state, err := readFromYaml(yamlContent, yamlFile)
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_DeprecatedReleaseReferences(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`charts:
- name: myrelease
chart: mychart
`)
state, err := readFromYaml(yamlContent, yamlFile)
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 := readFromYaml(yamlContent, yamlFile)
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: map[string]string{"tier": "frontend"}},
[]bool{true, true, false}},
{LabelFilter{positiveLabels: map[string]string{"tier": "frontend", "foo": "bar"}},
[]bool{true, false, false}},
{LabelFilter{negativeLabels: map[string]string{"tier": "frontend"}},
[]bool{false, false, true}},
{LabelFilter{positiveLabels: map[string]string{"tier": "frontend"}, negativeLabels: map[string]string{"foo": "bar"}},
[]bool{false, true, false}},
}
state, err := readFromYaml(yamlContent, yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
for idx, c := range cases {
for idx2, expected := range c.results {
if f := c.filter.Match(state.Releases[idx2]); f != expected {
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
}
}
}
}
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
expectedFilter LabelFilter
errorExected bool
}{
{"foo=bar", LabelFilter{positiveLabels: map[string]string{"foo": "bar"}, negativeLabels: map[string]string{}}, false},
{"foo!=bar", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{"foo": "bar"}}, false},
{"foo!=bar,baz=bat", LabelFilter{positiveLabels: map[string]string{"baz": "bat"}, negativeLabels: map[string]string{"foo": "bar"}}, false},
{"foo", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
{"foo!=bar=baz", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
{"=bar", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
}
for idx, c := range cases {
filter, err := ParseLabels(c.labelString)
if err != nil && !c.errorExected {
t.Errorf("[%d] Didn't expect an error parsing labels: %s", idx, err)
} else if err == nil && c.errorExected {
t.Errorf("[%d] Expected %s to result in an error but got none", idx, c.labelString)
} else if !reflect.DeepEqual(filter, c.expectedFilter) {
t.Errorf("[%d] parsed label did not result in expected filter: %v", idx, filter)
}
}
}
func TestHelmState_applyDefaultsTo(t *testing.T) {
type fields struct {
BaseChartPath string
Context string
DeprecatedReleases []ReleaseSpec
Namespace string
Repositories []RepositorySpec
Releases []ReleaseSpec
}
type args struct {
spec ReleaseSpec
}
specWithNamespace := ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: false,
Name: "test-charts",
Namespace: "test-namespace",
Values: nil,
SetValues: nil,
EnvValues: nil,
}
specWithoutNamespace := specWithNamespace
specWithoutNamespace.Namespace = ""
specWithNamespaceFromFields := specWithNamespace
specWithNamespaceFromFields.Namespace = "test-namespace-field"
fieldsWithNamespace := fields{
BaseChartPath: ".",
Context: "test_context",
DeprecatedReleases: nil,
Namespace: specWithNamespaceFromFields.Namespace,
Repositories: nil,
Releases: []ReleaseSpec{
specWithNamespace,
},
}
fieldsWithoutNamespace := fieldsWithNamespace
fieldsWithoutNamespace.Namespace = ""
tests := []struct {
name string
fields fields
args args
want ReleaseSpec
}{
{
name: "Has a namespace from spec",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespace,
},
{
name: "Has a namespace from flags",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespace,
},
{
name: "Has a namespace from flags and from spec",
fields: fieldsWithNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespaceFromFields,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
BaseChartPath: tt.fields.BaseChartPath,
Context: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
Namespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
}
if state.applyDefaultsTo(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
t.Errorf("HelmState.applyDefaultsTo() = %v, want %v", tt.args.spec, tt.want)
}
})
}
}
func Test_renderTemplateString(t *testing.T) {
type args struct {
s string
envs map[string]string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "simple replacement",
args: args{
s: "{{ env \"HF_TEST_VAR\" }}",
envs: map[string]string{
"HF_TEST_VAR": "content",
},
},
want: "content",
wantErr: false,
},
{
name: "two replacements",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" }}{{ env \"HF_TEST_BETA\" }}",
envs: map[string]string{
"HF_TEST_ALPHA": "first",
"HF_TEST_BETA": "second",
},
},
want: "firstsecond",
wantErr: false,
},
{
name: "replacement and comment",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" }}{{/* comment */}}",
envs: map[string]string{
"HF_TEST_ALPHA": "first",
},
},
want: "first",
wantErr: false,
},
{
name: "global template function",
args: args{
s: "{{ env \"HF_TEST_ALPHA\" | len }}",
envs: map[string]string{
"HF_TEST_ALPHA": "abcdefg",
},
},
want: "7",
wantErr: false,
},
{
name: "env var not set",
args: args{
s: "{{ env \"HF_TEST_NONE\" }}",
envs: map[string]string{
"HF_TEST_THIS": "first",
},
},
wantErr: true,
},
{
name: "undefined function",
args: args{
s: "{{ env foo }}",
envs: map[string]string{
"foo": "bar",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.args.envs {
err := os.Setenv(k, v)
if err != nil {
t.Error("renderTemplateString() could not set env var for testing")
}
}
got, err := renderTemplateString(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("renderTemplateString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("renderTemplateString() = %v, want %v", got, tt.want)
}
})
}
}
func Test_isLocalChart(t *testing.T) {
type args struct {
chart string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "local chart",
args: args{
chart: "./charts/nonstop",
},
want: true,
},
{
name: "repo chart",
args: args{
chart: "stable/genius",
},
want: false,
},
{
name: "empty",
args: args{
chart: "",
},
want: false,
},
{
name: "parent local path",
args: args{
chart: "../../dotty",
},
want: true,
},
{
name: "parent-parent local path",
args: args{
chart: "../../dotty",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isLocalChart(tt.args.chart); got != tt.want {
t.Errorf("isLocalChart() = %v, want %v", got, tt.want)
}
})
}
}
func Test_normalizeChart(t *testing.T) {
type args struct {
basePath string
chart string
}
tests := []struct {
name string
args args
want string
}{
{
name: "construct local chart path",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "./app",
},
want: "/Users/jane/code/deploy/charts/app",
},
{
name: "repo path",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "remote/app",
},
want: "remote/app",
},
{
name: "construct local chart path, parent dir",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "../app",
},
want: "/Users/jane/code/deploy/app",
},
{
name: "too much parent levels",
args: args{
basePath: "/src",
chart: "../../app",
},
want: "/app",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeChart(tt.args.basePath, tt.args.chart); got != tt.want {
t.Errorf("normalizeChart() = %v, want %v", got, tt.want)
}
})
}
}
// mocking helmexec.Interface
type mockHelmExec struct {
charts []string
repo []string
}
func (helm *mockHelmExec) UpdateDeps(chart string) error {
if strings.Contains(chart, "error") {
return errors.New("error")
}
helm.charts = append(helm.charts, chart)
return nil
}
func (helm *mockHelmExec) SetExtraArgs(args ...string) {
return
}
func (helm *mockHelmExec) AddRepo(name, repository, certfile, keyfile string) error {
helm.repo = []string{name, repository, certfile, keyfile}
return nil
}
func (helm *mockHelmExec) UpdateRepo() error {
return nil
}
func (helm *mockHelmExec) SyncRelease(name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DiffRelease(name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DeleteRelease(name string) error {
return nil
}
func (helm *mockHelmExec) DecryptSecret(name string) (string, error) {
return "", nil
}
func TestHelmState_SyncRepos(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
helm *mockHelmExec
envs map[string]string
want []string
}{
{
name: "normal repository",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
},
},
helm: &mockHelmExec{},
want: []string{"name", "http://example.com/", "", ""},
},
{
name: "repository with cert and key",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "certfile",
KeyFile: "keyfile",
},
},
helm: &mockHelmExec{},
want: []string{"name", "http://example.com/", "certfile", "keyfile"},
},
{
name: "repository with env var url",
repos: []RepositorySpec{
{
Name: "name",
URL: "https://{{ env \"HF_TEST_GITHUB_TOKEN\"}}@raw.githubusercontent.com/u/r/b/",
CertFile: "certfile",
KeyFile: "keyfile",
},
},
envs: map[string]string{
"HF_TEST_GITHUB_TOKEN": "token",
},
helm: &mockHelmExec{},
want: []string{"name", "https://token@raw.githubusercontent.com/u/r/b/", "certfile", "keyfile"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envs {
err := os.Setenv(k, v)
if err != nil {
t.Error("HelmState.SyncRepos() could not set env var for testing")
}
}
state := &HelmState{
Repositories: tt.repos,
}
if got := state.SyncRepos(tt.helm); !reflect.DeepEqual(tt.helm.repo, tt.want) {
t.Errorf("HelmState.SyncRepos() = %v, want %v", got, tt.want)
}
})
}
}
func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{
BaseChartPath: "/src",
Releases: []ReleaseSpec{
{
Chart: "./local",
},
{
Chart: "../local",
},
{
Chart: "../../local",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "./error",
},
},
}
want := []string{"/src/local", "/local", "/local"}
helm := &mockHelmExec{}
errs := state.UpdateDeps(helm)
if !reflect.DeepEqual(helm.charts, want) {
t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.charts, want)
}
if len(errs) != 1 {
t.Errorf("HelmState.UpdateDeps() - expected an error, but got: %v", len(errs))
}
}
+1
View File
@@ -0,0 +1 @@
mysecret: MYSECRET
+15 -1
View File
@@ -1,8 +1,16 @@
environments:
default:
values:
- environment.values.yaml
---
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com/
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
context: minikube
helmDefaults:
kubeContext: minikube
releases:
@@ -11,3 +19,9 @@ releases:
set:
- name: ingress.enabled
value: false
- name: raw
chart: incubator/raw
values:
- mysecret: {{ .Environment.Values.mysecret }}
- values.yaml
+74 -13
View File
@@ -20,37 +20,98 @@ kubectl="kubectl --context=minikube --namespace=${test_ns}"
# FUNCTIONS ----------------------------------------------------------------------------------------------------------
function wait_deploy_ready() {
$kubectl rollout status deployment ${1}
while [ "$($kubectl get deploy ${1} -o=jsonpath='{.status.readyReplicas}')" == "0" ]; do
${kubectl} rollout status deployment ${1}
while [ "$(${kubectl} get deploy ${1} -o=jsonpath='{.status.readyReplicas}')" == "0" ]; do
info "Waiting for deployment ${1} to be ready"
sleep 1
done
}
function retry() {
local -r max=${1}
local -r command=${2}
n=0
retry_result=0
until [ ${n} -ge ${max} ]; do
info "Executing: ${command} (attempt $((n+1)))"
${command} && break # substitute your command here
retry_result=$?
n=$[$n+1]
# approximated binary exponential backoff to reduce flakiness
sleep $((n ** 2))
done
}
# SETUP --------------------------------------------------------------------------------------------------------------
set -e
info "Using namespace: ${test_ns}"
info "Using Helm version: $(helm version --short --client | grep -o v.*$)"
$helm init --wait
$helmfile -v
$kubectl get namespace ${test_ns} &> /dev/null && warn "Namespace ${test_ns} exists, from a previous test run?"
# helm v2
if helm version --client 2>/dev/null | grep '"v2\.'; then
info "Using Helm version: $(helm version --short --client | grep -o v.*$)"
${helm} init --wait --override spec.template.spec.automountServiceAccountToken=true
# helm v3
else
info "Using Helm version: $(helm version --short | grep -o v.*$)"
fi
${helm} plugin install https://github.com/databus23/helm-diff --version v3.0.0-rc.7
${kubectl} get namespace ${test_ns} &> /dev/null && warn "Namespace ${test_ns} exists, from a previous test run?"
$kubectl create namespace ${test_ns} || fail "Could not create namespace ${test_ns}"
trap "{ $kubectl delete namespace ${test_ns}; }" EXIT # remove namespace whenever we exit this script
# TEST CASES----------------------------------------------------------------------------------------------------------
test_start "happypath - simple rollout of httpbin chart"
$helmfile -f ${dir}/happypath.yaml sync
info "Diffing ${dir}/happypath.yaml"
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
info "Diffing ${dir}/happypath.yaml without color"
bash -c "${helmfile} -f ${dir}/happypath.yaml --no-color diff --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
info "Diffing ${dir}/happypath.yaml with limited context"
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --context 3 --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
info "Templating ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml template
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}"
info "Syncing ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml sync
wait_deploy_ready httpbin-httpbin
curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200 \
|| fail "httpbin failed to return 200 OK"
$helmfile -f ${dir}/happypath.yaml delete
$helm status --namespace=${test_ns} httpbin &> /dev/null \
&& fail "release should not exist anymore after a delete"
retry 5 "curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200"
[ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK"
info "Applying ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml apply
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
info "Locking dependencies"
${helmfile} -f ${dir}/happypath.yaml deps
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile deps: ${code}"
info "Applying ${dir}/happypath.yaml with locked dependencies"
${helmfile} -f ${dir}/happypath.yaml apply
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
${helm} list --namespace=${test_ns} || fail "unable to list releases"
info "Deleting release"
${helmfile} -f ${dir}/happypath.yaml delete
${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release should not exist anymore after a delete"
info "Ensuring \"helmfile delete\" doesn't fail when no releases installed"
${helmfile} -f ${dir}/happypath.yaml delete || fail "\"helmfile delete\" shouldn't fail when there are no installed releases"
info "Ensuring \"helmfile template\" output does contain only YAML docs"
(${helmfile} -f ${dir}/happypath.yaml template | kubectl apply -f -) || fail "\"helmfile template | kubectl apply -f -\" shouldn't fail"
test_pass "happypath"
# ALL DONE -----------------------------------------------------------------------------------------------------------
all_tests_passed
all_tests_passed
+9
View File
@@ -0,0 +1,9 @@
resources: []
templates:
- |
apiVersion: v1
kind: Secret
metadata:
name: common-secret
stringData:
mykey: {{ .Values.mysecret }}
-323
View File
@@ -1,323 +0,0 @@
# Change Log
**ATTN**: This project uses [semantic versioning](http://semver.org/).
## [Unreleased]
### Added
- `./runtests` test runner with coverage tracking by default
- testing on OS X
- testing on Windows
- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code
### Changed
- Use spaces for alignment in help/usage output instead of tabs, making the
output alignment consistent regardless of tab width
### Fixed
- Printing of command aliases in help text
- Printing of visible flags for both struct and struct pointer flags
## [1.17.0] - 2016-05-09
### Added
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool`
- Support for hiding commands by setting `Hidden: true` -- this will hide the
commands in help output
### Changed
- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer
quoted in help text output.
- All flag types now include `(default: {value})` strings following usage when a
default value can be (reasonably) detected.
- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent
with non-slice flag types
- Apps now exit with a code of 3 if an unknown subcommand is specified
(previously they printed "No help topic for...", but still exited 0. This
makes it easier to script around apps built using `cli` since they can trust
that a 0 exit code indicated a successful execution.
- cleanups based on [Go Report Card
feedback](https://goreportcard.com/report/github.com/urfave/cli)
## [1.16.0] - 2016-05-02
### Added
- `Hidden` field on all flag struct types to omit from generated help text
### Changed
- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from
generated help text via the `Hidden` field
### Fixed
- handling of error values in `HandleAction` and `HandleExitCoder`
## [1.15.0] - 2016-04-30
### Added
- This file!
- Support for placeholders in flag usage strings
- `App.Metadata` map for arbitrary data/state management
- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after
parsing.
- Support for nested lookup of dot-delimited keys in structures loaded from
YAML.
### Changed
- The `App.Action` and `Command.Action` now prefer a return signature of
`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil
`error` is returned, there may be two outcomes:
- If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called
automatically
- Else the error is bubbled up and returned from `App.Run`
- Specifying an `Action` with the legacy return signature of
`func(*cli.Context)` will produce a deprecation message to stderr
- Specifying an `Action` that is not a `func` type will produce a non-zero exit
from `App.Run`
- Specifying an `Action` func that has an invalid (input) signature will
produce a non-zero exit from `App.Run`
### Deprecated
- <a name="deprecated-cli-app-runandexitonerror"></a>
`cli.App.RunAndExitOnError`, which should now be done by returning an error
that fulfills `cli.ExitCoder` to `cli.App.Run`.
- <a name="deprecated-cli-app-action-signature"></a> the legacy signature for
`cli.App.Action` of `func(*cli.Context)`, which should now have a return
signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
### Fixed
- Added missing `*cli.Context.GlobalFloat64` method
## [1.14.0] - 2016-04-03 (backfilled 2016-04-25)
### Added
- Codebeat badge
- Support for categorization via `CategorizedHelp` and `Categories` on app.
### Changed
- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`.
### Fixed
- Ensure version is not shown in help text when `HideVersion` set.
## [1.13.0] - 2016-03-06 (backfilled 2016-04-25)
### Added
- YAML file input support.
- `NArg` method on context.
## [1.12.0] - 2016-02-17 (backfilled 2016-04-25)
### Added
- Custom usage error handling.
- Custom text support in `USAGE` section of help output.
- Improved help messages for empty strings.
- AppVeyor CI configuration.
### Changed
- Removed `panic` from default help printer func.
- De-duping and optimizations.
### Fixed
- Correctly handle `Before`/`After` at command level when no subcommands.
- Case of literal `-` argument causing flag reordering.
- Environment variable hints on Windows.
- Docs updates.
## [1.11.1] - 2015-12-21 (backfilled 2016-04-25)
### Changed
- Use `path.Base` in `Name` and `HelpName`
- Export `GetName` on flag types.
### Fixed
- Flag parsing when skipping is enabled.
- Test output cleanup.
- Move completion check to account for empty input case.
## [1.11.0] - 2015-11-15 (backfilled 2016-04-25)
### Added
- Destination scan support for flags.
- Testing against `tip` in Travis CI config.
### Changed
- Go version in Travis CI config.
### Fixed
- Removed redundant tests.
- Use correct example naming in tests.
## [1.10.2] - 2015-10-29 (backfilled 2016-04-25)
### Fixed
- Remove unused var in bash completion.
## [1.10.1] - 2015-10-21 (backfilled 2016-04-25)
### Added
- Coverage and reference logos in README.
### Fixed
- Use specified values in help and version parsing.
- Only display app version and help message once.
## [1.10.0] - 2015-10-06 (backfilled 2016-04-25)
### Added
- More tests for existing functionality.
- `ArgsUsage` at app and command level for help text flexibility.
### Fixed
- Honor `HideHelp` and `HideVersion` in `App.Run`.
- Remove juvenile word from README.
## [1.9.0] - 2015-09-08 (backfilled 2016-04-25)
### Added
- `FullName` on command with accompanying help output update.
- Set default `$PROG` in bash completion.
### Changed
- Docs formatting.
### Fixed
- Removed self-referential imports in tests.
## [1.8.0] - 2015-06-30 (backfilled 2016-04-25)
### Added
- Support for `Copyright` at app level.
- `Parent` func at context level to walk up context lineage.
### Fixed
- Global flag processing at top level.
## [1.7.1] - 2015-06-11 (backfilled 2016-04-25)
### Added
- Aggregate errors from `Before`/`After` funcs.
- Doc comments on flag structs.
- Include non-global flags when checking version and help.
- Travis CI config updates.
### Fixed
- Ensure slice type flags have non-nil values.
- Collect global flags from the full command hierarchy.
- Docs prose.
## [1.7.0] - 2015-05-03 (backfilled 2016-04-25)
### Changed
- `HelpPrinter` signature includes output writer.
### Fixed
- Specify go 1.1+ in docs.
- Set `Writer` when running command as app.
## [1.6.0] - 2015-03-23 (backfilled 2016-04-25)
### Added
- Multiple author support.
- `NumFlags` at context level.
- `Aliases` at command level.
### Deprecated
- `ShortName` at command level.
### Fixed
- Subcommand help output.
- Backward compatible support for deprecated `Author` and `Email` fields.
- Docs regarding `Names`/`Aliases`.
## [1.5.0] - 2015-02-20 (backfilled 2016-04-25)
### Added
- `After` hook func support at app and command level.
### Fixed
- Use parsed context when running command as subcommand.
- Docs prose.
## [1.4.1] - 2015-01-09 (backfilled 2016-04-25)
### Added
- Support for hiding `-h / --help` flags, but not `help` subcommand.
- Stop flag parsing after `--`.
### Fixed
- Help text for generic flags to specify single value.
- Use double quotes in output for defaults.
- Use `ParseInt` instead of `ParseUint` for int environment var values.
- Use `0` as base when parsing int environment var values.
## [1.4.0] - 2014-12-12 (backfilled 2016-04-25)
### Added
- Support for environment variable lookup "cascade".
- Support for `Stdout` on app for output redirection.
### Fixed
- Print command help instead of app help in `ShowCommandHelp`.
## [1.3.1] - 2014-11-13 (backfilled 2016-04-25)
### Added
- Docs and example code updates.
### Changed
- Default `-v / --version` flag made optional.
## [1.3.0] - 2014-08-10 (backfilled 2016-04-25)
### Added
- `FlagNames` at context level.
- Exposed `VersionPrinter` var for more control over version output.
- Zsh completion hook.
- `AUTHOR` section in default app help template.
- Contribution guidelines.
- `DurationFlag` type.
## [1.2.0] - 2014-08-02
### Added
- Support for environment variable defaults on flags plus tests.
## [1.1.0] - 2014-07-15
### Added
- Bash completion.
- Optional hiding of built-in help command.
- Optional skipping of flag parsing at command level.
- `Author`, `Email`, and `Compiled` metadata on app.
- `Before` hook func support at app and command level.
- `CommandNotFound` func support at app level.
- Command reference available on context.
- `GenericFlag` type.
- `Float64Flag` type.
- `BoolTFlag` type.
- `IsSet` flag helper on context.
- More flag lookup funcs at context level.
- More tests &amp; docs.
### Changed
- Help template updates to account for presence/absence of flags.
- Separated subcommand help template.
- Exposed `HelpPrinter` var for more control over help output.
## [1.0.0] - 2013-11-01
### Added
- `help` flag in default app flag set and each command flag set.
- Custom handling of argument parsing errors.
- Command lookup by name at app level.
- `StringSliceFlag` type and supporting `StringSlice` type.
- `IntSliceFlag` type and supporting `IntSlice` type.
- Slice type flag lookups by name at context level.
- Export of app and command help functions.
- More tests &amp; docs.
## 0.1.0 - 2013-07-22
### Added
- Initial implementation.
[Unreleased]: https://github.com/urfave/cli/compare/v1.17.0...HEAD
[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0
[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0
[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0
[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0
[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0
[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0
[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2
[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0
-21
View File
@@ -1,21 +0,0 @@
Copyright (C) 2013 Jeremy Saenz
All Rights Reserved.
MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-1309
View File
File diff suppressed because it is too large Load Diff
-501
View File
@@ -1,501 +0,0 @@
package cli
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
)
var (
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL)
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
errNonFuncAction = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+
fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
)
// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
// The name of the program. Defaults to path.Base(os.Args[0])
Name string
// Full name of command for help, defaults to Name
HelpName string
// Description of the program.
Usage string
// Text to override the USAGE section of help
UsageText string
// Description of the program argument format.
ArgsUsage string
// Version of the program
Version string
// List of commands to execute
Commands []Command
// List of flags to parse
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide built-in version flag and the VERSION section of help
HideVersion bool
// Populate on app startup, only gettable through method Categories()
categories CommandCategories
// An action to execute when the bash-completion flag is set
BashComplete BashCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The action to execute when no subcommands are specified
Action interface{}
// TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
// of deprecation period has passed, maybe?
// Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc
// Execute this function if an usage error occurs
OnUsageError OnUsageErrorFunc
// Compilation date
Compiled time.Time
// List of all authors who contributed
Authors []Author
// Copyright of the binary if any
Copyright string
// Name of Author (Note: Use App.Authors, this is deprecated)
Author string
// Email of Author (Note: Use App.Authors, this is deprecated)
Email string
// Writer writer to write output to
Writer io.Writer
// ErrWriter writes error output
ErrWriter io.Writer
// Other custom info
Metadata map[string]interface{}
didSetup bool
}
// Tries to find out when this binary was compiled.
// Returns the current time if it fails to find it.
func compileTime() time.Time {
info, err := os.Stat(os.Args[0])
if err != nil {
return time.Now()
}
return info.ModTime()
}
// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
HelpName: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
Version: "0.0.0",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Writer: os.Stdout,
}
}
// Setup runs initialization code to ensure all data structures are ready for
// `Run` or inspection prior to `Run`. It is internally called by `Run`, but
// will return early if setup has already happened.
func (a *App) Setup() {
if a.didSetup {
return
}
a.didSetup = true
if a.Author != "" || a.Email != "" {
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
}
newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
a.categories = CommandCategories{}
for _, command := range a.Commands {
a.categories = a.categories.AddCommand(command.Category, command)
}
sort.Sort(a.categories)
// append help to commands
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
//append version/help flags
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
if !a.HideVersion {
a.appendFlag(VersionFlag)
}
}
// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (a *App) Run(arguments []string) (err error) {
a.Setup()
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, nil)
if nerr != nil {
fmt.Fprintln(a.Writer, nerr)
ShowAppHelp(context)
return nerr
}
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.")
ShowAppHelp(context)
return err
}
if !a.HideHelp && checkHelp(context) {
ShowAppHelp(context)
return nil
}
if !a.HideVersion && checkVersion(context) {
ShowVersion(context)
return nil
}
if a.After != nil {
defer func() {
if afterErr := a.After(context); afterErr != nil {
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
ShowAppHelp(context)
HandleExitCoder(beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
err = HandleAction(a.Action, context)
HandleExitCoder(err)
return err
}
// DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling
func (a *App) RunAndExitOnError() {
fmt.Fprintf(a.errWriter(),
"DEPRECATED cli.App.RunAndExitOnError. %s See %s\n",
contactSysadmin, runAndExitOnErrorDeprecationURL)
if err := a.Run(os.Args); err != nil {
fmt.Fprintln(a.errWriter(), err)
OsExiter(1)
}
}
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
// generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
}
newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
// append flags
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard)
err = set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx)
if nerr != nil {
fmt.Fprintln(a.Writer, nerr)
fmt.Fprintln(a.Writer)
if len(a.Commands) > 0 {
ShowSubcommandHelp(context)
} else {
ShowCommandHelp(ctx, context.Args().First())
}
return nerr
}
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true)
HandleExitCoder(err)
return err
}
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.")
ShowSubcommandHelp(context)
return err
}
if len(a.Commands) > 0 {
if checkSubcommandHelp(context) {
return nil
}
} else {
if checkCommandHelp(ctx, context.Args().First()) {
return nil
}
}
if a.After != nil {
defer func() {
afterErr := a.After(context)
if afterErr != nil {
HandleExitCoder(err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil {
beforeErr := a.Before(context)
if beforeErr != nil {
HandleExitCoder(beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
err = HandleAction(a.Action, context)
HandleExitCoder(err)
return err
}
// Command returns the named command on App. Returns nil if the command does not exist
func (a *App) Command(name string) *Command {
for _, c := range a.Commands {
if c.HasName(name) {
return &c
}
}
return nil
}
// Categories returns a slice containing all the categories with the commands they contain
func (a *App) Categories() CommandCategories {
return a.categories
}
// VisibleCategories returns a slice of categories and commands that are
// Hidden=false
func (a *App) VisibleCategories() []*CommandCategory {
ret := []*CommandCategory{}
for _, category := range a.categories {
if visible := func() *CommandCategory {
for _, command := range category.Commands {
if !command.Hidden {
return category
}
}
return nil
}(); visible != nil {
ret = append(ret, visible)
}
}
return ret
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (a *App) VisibleCommands() []Command {
ret := []Command{}
for _, command := range a.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags)
}
func (a *App) hasFlag(flag Flag) bool {
for _, f := range a.Flags {
if flag == f {
return true
}
}
return false
}
func (a *App) errWriter() io.Writer {
// When the app ErrWriter is nil use the package level one.
if a.ErrWriter == nil {
return ErrWriter
}
return a.ErrWriter
}
func (a *App) appendFlag(flag Flag) {
if !a.hasFlag(flag) {
a.Flags = append(a.Flags, flag)
}
}
// Author represents someone who has contributed to a cli project.
type Author struct {
Name string // The Authors name
Email string // The Authors email
}
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
func (a Author) String() string {
e := ""
if a.Email != "" {
e = "<" + a.Email + "> "
}
return fmt.Sprintf("%v %v", a.Name, e)
}
// HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an
// ActionFunc, a func with the legacy signature for Action, or some other
// invalid thing. If it's an ActionFunc or a func with the legacy signature for
// Action, the func is run!
func HandleAction(action interface{}, context *Context) (err error) {
defer func() {
if r := recover(); r != nil {
// Try to detect a known reflection error from *this scope*, rather than
// swallowing all panics that may happen when calling an Action func.
s := fmt.Sprintf("%v", r)
if strings.HasPrefix(s, "reflect: ") && strings.Contains(s, "too many input arguments") {
err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2)
} else {
panic(r)
}
}
}()
if reflect.TypeOf(action).Kind() != reflect.Func {
return errNonFuncAction
}
vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)})
if len(vals) == 0 {
fmt.Fprintf(ErrWriter,
"DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n",
contactSysadmin, appActionDeprecationURL)
return nil
}
if len(vals) > 1 {
return errInvalidActionSignature
}
if retErr, ok := vals[0].Interface().(error); vals[0].IsValid() && ok {
return retErr
}
return err
}
-25
View File
@@ -1,25 +0,0 @@
version: "{build}"
os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\urfave\cli
environment:
GOPATH: C:\gopath
GOVERSION: 1.6
PYTHON: C:\Python27-x64
PYTHON_VERSION: 2.7.x
PYTHON_ARCH: 64
GFMXR_DEBUG: 1
install:
- set PATH=%GOPATH%\bin;C:\go\bin;%PATH%
- go version
- go env
- go get github.com/urfave/gfmxr/...
- go get -v -t ./...
build_script:
- python runtests vet
- python runtests test
- python runtests gfmxr
-44
View File
@@ -1,44 +0,0 @@
package cli
// CommandCategories is a slice of *CommandCategory.
type CommandCategories []*CommandCategory
// CommandCategory is a category containing commands.
type CommandCategory struct {
Name string
Commands Commands
}
func (c CommandCategories) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
func (c CommandCategories) Len() int {
return len(c)
}
func (c CommandCategories) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// AddCommand adds a command to a category.
func (c CommandCategories) AddCommand(category string, command Command) CommandCategories {
for _, commandCategory := range c {
if commandCategory.Name == category {
commandCategory.Commands = append(commandCategory.Commands, command)
return c
}
}
return append(c, &CommandCategory{Name: category, Commands: []Command{command}})
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (c *CommandCategory) VisibleCommands() []Command {
ret := []Command{}
for _, command := range c.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
-19
View File
@@ -1,19 +0,0 @@
// Package cli provides a minimal framework for creating and organizing command line
// Go applications. cli is designed to be easy to understand and write, the most simple
// cli application can be written as follows:
// func main() {
// cli.NewApp().Run(os.Args)
// }
//
// Of course this application does not do much, so let's make this an actual application:
// func main() {
// app := cli.NewApp()
// app.Name = "greet"
// app.Usage = "say a greeting"
// app.Action = func(c *cli.Context) error {
// println("Greetings")
// }
//
// app.Run(os.Args)
// }
package cli
-279
View File
@@ -1,279 +0,0 @@
package cli
import (
"fmt"
"io/ioutil"
"sort"
"strings"
)
// Command is a subcommand for a cli.App.
type Command struct {
// The name of the command
Name string
// short name of the command. Typically one character (deprecated, use `Aliases`)
ShortName string
// A list of aliases for the command
Aliases []string
// A short description of the usage of this command
Usage string
// Custom text to show on USAGE section of help
UsageText string
// A longer explanation of how the command works
Description string
// A short description of the arguments of this command
ArgsUsage string
// The category the command is part of
Category string
// The function to call when checking for bash command completions
BashComplete BashCompleteFunc
// An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The function to call when this command is invoked
Action interface{}
// TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
// of deprecation period has passed, maybe?
// Execute this function if a usage error occurs.
OnUsageError OnUsageErrorFunc
// List of child commands
Subcommands Commands
// List of flags to parse
Flags []Flag
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide this command from help or completion
Hidden bool
// Full name of command for help, defaults to full command name, including parent commands.
HelpName string
commandNamePath []string
}
// FullName returns the full name of the command.
// For subcommands this ensures that parent commands are part of the command path
func (c Command) FullName() string {
if c.commandNamePath == nil {
return c.Name
}
return strings.Join(c.commandNamePath, " ")
}
// Commands is a slice of Command
type Commands []Command
// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) (err error) {
if len(c.Subcommands) > 0 {
return c.startApp(ctx)
}
if !c.HideHelp && (HelpFlag != BoolFlag{}) {
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
}
if ctx.App.EnableBashCompletion {
c.Flags = append(c.Flags, BashCompletionFlag)
}
set := flagSet(c.Name, c.Flags)
set.SetOutput(ioutil.Discard)
if !c.SkipFlagParsing {
firstFlagIndex := -1
terminatorIndex := -1
for index, arg := range ctx.Args() {
if arg == "--" {
terminatorIndex = index
break
} else if arg == "-" {
// Do nothing. A dash alone is not really a flag.
continue
} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
firstFlagIndex = index
}
}
if firstFlagIndex > -1 {
args := ctx.Args()
regularArgs := make([]string, len(args[1:firstFlagIndex]))
copy(regularArgs, args[1:firstFlagIndex])
var flagArgs []string
if terminatorIndex > -1 {
flagArgs = args[firstFlagIndex:terminatorIndex]
regularArgs = append(regularArgs, args[terminatorIndex:]...)
} else {
flagArgs = args[firstFlagIndex:]
}
err = set.Parse(append(flagArgs, regularArgs...))
} else {
err = set.Parse(ctx.Args().Tail())
}
} else {
if c.SkipFlagParsing {
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
}
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(ctx, err, false)
HandleExitCoder(err)
return err
}
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.")
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return err
}
nerr := normalizeFlags(c.Flags, set)
if nerr != nil {
fmt.Fprintln(ctx.App.Writer, nerr)
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return nerr
}
context := NewContext(ctx.App, set, ctx)
if checkCommandCompletions(context, c.Name) {
return nil
}
if checkCommandHelp(context, c.Name) {
return nil
}
if c.After != nil {
defer func() {
afterErr := c.After(context)
if afterErr != nil {
HandleExitCoder(err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if c.Before != nil {
err = c.Before(context)
if err != nil {
fmt.Fprintln(ctx.App.Writer, err)
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
HandleExitCoder(err)
return err
}
}
context.Command = c
err = HandleAction(c.Action, context)
if err != nil {
HandleExitCoder(err)
}
return err
}
// Names returns the names including short names and aliases.
func (c Command) Names() []string {
names := []string{c.Name}
if c.ShortName != "" {
names = append(names, c.ShortName)
}
return append(names, c.Aliases...)
}
// HasName returns true if Command.Name or Command.ShortName matches given name
func (c Command) HasName(name string) bool {
for _, n := range c.Names() {
if n == name {
return true
}
}
return false
}
func (c Command) startApp(ctx *Context) error {
app := NewApp()
app.Metadata = ctx.App.Metadata
// set the name and usage
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
if c.HelpName == "" {
app.HelpName = c.HelpName
} else {
app.HelpName = app.Name
}
if c.Description != "" {
app.Usage = c.Description
} else {
app.Usage = c.Usage
}
// set CommandNotFound
app.CommandNotFound = ctx.App.CommandNotFound
// set the flags and commands
app.Commands = c.Subcommands
app.Flags = c.Flags
app.HideHelp = c.HideHelp
app.Version = ctx.App.Version
app.HideVersion = ctx.App.HideVersion
app.Compiled = ctx.App.Compiled
app.Author = ctx.App.Author
app.Email = ctx.App.Email
app.Writer = ctx.App.Writer
app.categories = CommandCategories{}
for _, command := range c.Subcommands {
app.categories = app.categories.AddCommand(command.Category, command)
}
sort.Sort(app.categories)
// bash completion
app.EnableBashCompletion = ctx.App.EnableBashCompletion
if c.BashComplete != nil {
app.BashComplete = c.BashComplete
}
// set the actions
app.Before = c.Before
app.After = c.After
if c.Action != nil {
app.Action = c.Action
} else {
app.Action = helpSubcommand.Action
}
for index, cc := range app.Commands {
app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
}
return app.RunAsSubcommand(ctx)
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (c Command) VisibleFlags() []Flag {
return visibleFlags(c.Flags)
}
-549
View File
@@ -1,549 +0,0 @@
package cli
import (
"errors"
"flag"
"strconv"
"strings"
"time"
)
// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific Args and
// parsed command-line options.
type Context struct {
App *App
Command Command
flagSet *flag.FlagSet
setFlags map[string]bool
globalSetFlags map[string]bool
parentContext *Context
}
// NewContext creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
return &Context{App: app, flagSet: set, parentContext: parentCtx}
}
// Int looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Int(name string) int {
return lookupInt(name, c.flagSet)
}
// Int64 looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Int64(name string) int64 {
return lookupInt64(name, c.flagSet)
}
// Uint looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Uint(name string) uint {
return lookupUint(name, c.flagSet)
}
// Uint64 looks up the value of a local int flag, returns 0 if no int flag exists
func (c *Context) Uint64(name string) uint64 {
return lookupUint64(name, c.flagSet)
}
// Duration looks up the value of a local time.Duration flag, returns 0 if no
// time.Duration flag exists
func (c *Context) Duration(name string) time.Duration {
return lookupDuration(name, c.flagSet)
}
// Float64 looks up the value of a local float64 flag, returns 0 if no float64
// flag exists
func (c *Context) Float64(name string) float64 {
return lookupFloat64(name, c.flagSet)
}
// Bool looks up the value of a local bool flag, returns false if no bool flag exists
func (c *Context) Bool(name string) bool {
return lookupBool(name, c.flagSet)
}
// BoolT looks up the value of a local boolT flag, returns false if no bool flag exists
func (c *Context) BoolT(name string) bool {
return lookupBoolT(name, c.flagSet)
}
// String looks up the value of a local string flag, returns "" if no string flag exists
func (c *Context) String(name string) string {
return lookupString(name, c.flagSet)
}
// StringSlice looks up the value of a local string slice flag, returns nil if no
// string slice flag exists
func (c *Context) StringSlice(name string) []string {
return lookupStringSlice(name, c.flagSet)
}
// IntSlice looks up the value of a local int slice flag, returns nil if no int
// slice flag exists
func (c *Context) IntSlice(name string) []int {
return lookupIntSlice(name, c.flagSet)
}
// Int64Slice looks up the value of a local int slice flag, returns nil if no int
// slice flag exists
func (c *Context) Int64Slice(name string) []int64 {
return lookupInt64Slice(name, c.flagSet)
}
// Generic looks up the value of a local generic flag, returns nil if no generic
// flag exists
func (c *Context) Generic(name string) interface{} {
return lookupGeneric(name, c.flagSet)
}
// GlobalInt looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalInt(name string) int {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt(name, fs)
}
return 0
}
// GlobalInt64 looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalInt64(name string) int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64(name, fs)
}
return 0
}
// GlobalUint looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalUint(name string) uint {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint(name, fs)
}
return 0
}
// GlobalUint64 looks up the value of a global int flag, returns 0 if no int flag exists
func (c *Context) GlobalUint64(name string) uint64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint64(name, fs)
}
return 0
}
// GlobalFloat64 looks up the value of a global float64 flag, returns float64(0)
// if no float64 flag exists
func (c *Context) GlobalFloat64(name string) float64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupFloat64(name, fs)
}
return float64(0)
}
// GlobalDuration looks up the value of a global time.Duration flag, returns 0
// if no time.Duration flag exists
func (c *Context) GlobalDuration(name string) time.Duration {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupDuration(name, fs)
}
return 0
}
// GlobalBool looks up the value of a global bool flag, returns false if no bool
// flag exists
func (c *Context) GlobalBool(name string) bool {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupBool(name, fs)
}
return false
}
// GlobalBoolT looks up the value of a global bool flag, returns true if no bool
// flag exists
func (c *Context) GlobalBoolT(name string) bool {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupBoolT(name, fs)
}
return false
}
// GlobalString looks up the value of a global string flag, returns "" if no
// string flag exists
func (c *Context) GlobalString(name string) string {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupString(name, fs)
}
return ""
}
// GlobalStringSlice looks up the value of a global string slice flag, returns
// nil if no string slice flag exists
func (c *Context) GlobalStringSlice(name string) []string {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupStringSlice(name, fs)
}
return nil
}
// GlobalIntSlice looks up the value of a global int slice flag, returns nil if
// no int slice flag exists
func (c *Context) GlobalIntSlice(name string) []int {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupIntSlice(name, fs)
}
return nil
}
// GlobalInt64Slice looks up the value of a global int slice flag, returns nil if
// no int slice flag exists
func (c *Context) GlobalInt64Slice(name string) []int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64Slice(name, fs)
}
return nil
}
// GlobalGeneric looks up the value of a global generic flag, returns nil if no
// generic flag exists
func (c *Context) GlobalGeneric(name string) interface{} {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupGeneric(name, fs)
}
return nil
}
// NumFlags returns the number of flags set
func (c *Context) NumFlags() int {
return c.flagSet.NFlag()
}
// Set sets a context flag to a value.
func (c *Context) Set(name, value string) error {
return c.flagSet.Set(name, value)
}
// GlobalSet sets a context flag to a value on the global flagset
func (c *Context) GlobalSet(name, value string) error {
return globalContext(c).flagSet.Set(name, value)
}
// IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool {
if c.setFlags == nil {
c.setFlags = make(map[string]bool)
c.flagSet.Visit(func(f *flag.Flag) {
c.setFlags[f.Name] = true
})
}
return c.setFlags[name] == true
}
// GlobalIsSet determines if the global flag was actually set
func (c *Context) GlobalIsSet(name string) bool {
if c.globalSetFlags == nil {
c.globalSetFlags = make(map[string]bool)
ctx := c
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext {
ctx.flagSet.Visit(func(f *flag.Flag) {
c.globalSetFlags[f.Name] = true
})
}
}
return c.globalSetFlags[name]
}
// FlagNames returns a slice of flag names used in this context.
func (c *Context) FlagNames() (names []string) {
for _, flag := range c.Command.Flags {
name := strings.Split(flag.GetName(), ",")[0]
if name == "help" {
continue
}
names = append(names, name)
}
return
}
// GlobalFlagNames returns a slice of global flag names used by the app.
func (c *Context) GlobalFlagNames() (names []string) {
for _, flag := range c.App.Flags {
name := strings.Split(flag.GetName(), ",")[0]
if name == "help" || name == "version" {
continue
}
names = append(names, name)
}
return
}
// Parent returns the parent context, if any
func (c *Context) Parent() *Context {
return c.parentContext
}
// Args contains apps console arguments
type Args []string
// Args returns the command line arguments associated with the context.
func (c *Context) Args() Args {
args := Args(c.flagSet.Args())
return args
}
// NArg returns the number of the command line arguments.
func (c *Context) NArg() int {
return len(c.Args())
}
// Get returns the nth argument, or else a blank string
func (a Args) Get(n int) string {
if len(a) > n {
return a[n]
}
return ""
}
// First returns the first argument, or else a blank string
func (a Args) First() string {
return a.Get(0)
}
// Tail returns the rest of the arguments (not the first one)
// or else an empty string slice
func (a Args) Tail() []string {
if len(a) >= 2 {
return []string(a)[1:]
}
return []string{}
}
// Present checks if there are any arguments present
func (a Args) Present() bool {
return len(a) != 0
}
// Swap swaps arguments at the given indexes
func (a Args) Swap(from, to int) error {
if from >= len(a) || to >= len(a) {
return errors.New("index out of range")
}
a[from], a[to] = a[to], a[from]
return nil
}
func globalContext(ctx *Context) *Context {
if ctx == nil {
return nil
}
for {
if ctx.parentContext == nil {
return ctx
}
ctx = ctx.parentContext
}
}
func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil; ctx = ctx.parentContext {
if f := ctx.flagSet.Lookup(name); f != nil {
return ctx.flagSet
}
}
return nil
}
func lookupInt(name string, set *flag.FlagSet) int {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return int(val)
}
return 0
}
func lookupInt64(name string, set *flag.FlagSet) int64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return val
}
return 0
}
func lookupUint(name string, set *flag.FlagSet) uint {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseUint(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return uint(val)
}
return 0
}
func lookupUint64(name string, set *flag.FlagSet) uint64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseUint(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return val
}
return 0
}
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
f := set.Lookup(name)
if f != nil {
val, err := time.ParseDuration(f.Value.String())
if err == nil {
return val
}
}
return 0
}
func lookupFloat64(name string, set *flag.FlagSet) float64 {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseFloat(f.Value.String(), 64)
if err != nil {
return 0
}
return val
}
return 0
}
func lookupString(name string, set *flag.FlagSet) string {
f := set.Lookup(name)
if f != nil {
return f.Value.String()
}
return ""
}
func lookupStringSlice(name string, set *flag.FlagSet) []string {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*StringSlice)).Value()
}
return nil
}
func lookupIntSlice(name string, set *flag.FlagSet) []int {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*IntSlice)).Value()
}
return nil
}
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
f := set.Lookup(name)
if f != nil {
return (f.Value.(*Int64Slice)).Value()
}
return nil
}
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
f := set.Lookup(name)
if f != nil {
return f.Value
}
return nil
}
func lookupBool(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseBool(f.Value.String())
if err != nil {
return false
}
return val
}
return false
}
func lookupBoolT(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
val, err := strconv.ParseBool(f.Value.String())
if err != nil {
return true
}
return val
}
return false
}
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *StringSlice:
default:
set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := strings.Split(f.GetName(), ",")
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}
-92
View File
@@ -1,92 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
)
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
var OsExiter = os.Exit
// ErrWriter is used to write errors to the user. This can be anything
// implementing the io.Writer interface and defaults to os.Stderr.
var ErrWriter io.Writer = os.Stderr
// MultiError is an error that wraps multiple errors.
type MultiError struct {
Errors []error
}
// NewMultiError creates a new MultiError. Pass in one or more errors.
func NewMultiError(err ...error) MultiError {
return MultiError{Errors: err}
}
// Error implents the error interface.
func (m MultiError) Error() string {
errs := make([]string, len(m.Errors))
for i, err := range m.Errors {
errs[i] = err.Error()
}
return strings.Join(errs, "\n")
}
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
// code
type ExitCoder interface {
error
ExitCode() int
}
// ExitError fulfills both the builtin `error` interface and `ExitCoder`
type ExitError struct {
exitCode int
message string
}
// NewExitError makes a new *ExitError
func NewExitError(message string, exitCode int) *ExitError {
return &ExitError{
exitCode: exitCode,
message: message,
}
}
// Error returns the string message, fulfilling the interface required by
// `error`
func (ee *ExitError) Error() string {
return ee.message
}
// ExitCode returns the exit code, fulfilling the interface required by
// `ExitCoder`
func (ee *ExitError) ExitCode() int {
return ee.exitCode
}
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
// given exit code. If the given error is a MultiError, then this func is
// called on all members of the Errors slice.
func HandleExitCoder(err error) {
if err == nil {
return
}
if exitErr, ok := err.(ExitCoder); ok {
if err.Error() != "" {
fmt.Fprintln(ErrWriter, err)
}
OsExiter(exitErr.ExitCode())
return
}
if multiErr, ok := err.(MultiError); ok {
for _, merr := range multiErr.Errors {
HandleExitCoder(merr)
}
}
}
-882
View File
@@ -1,882 +0,0 @@
package cli
import (
"flag"
"fmt"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"time"
)
const defaultPlaceholder = "value"
// BashCompletionFlag enables bash-completion for all commands and subcommands
var BashCompletionFlag = BoolFlag{
Name: "generate-bash-completion",
Hidden: true,
}
// VersionFlag prints the version for the application
var VersionFlag = BoolFlag{
Name: "version, v",
Usage: "print the version",
}
// HelpFlag prints the help for all commands and subcommands
// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand
// unless HideHelp is set to true)
var HelpFlag = BoolFlag{
Name: "help, h",
Usage: "show help",
}
// FlagStringer converts a flag definition to a string. This is used by help
// to display a flag.
var FlagStringer FlagStringFunc = stringifyFlag
// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that
// this interface be implemented.
type Flag interface {
fmt.Stringer
// Apply Flag settings to the given flag set
Apply(*flag.FlagSet)
GetName() string
}
func flagSet(name string, flags []Flag) *flag.FlagSet {
set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags {
f.Apply(set)
}
return set
}
func eachName(longName string, fn func(string)) {
parts := strings.Split(longName, ",")
for _, name := range parts {
name = strings.Trim(name, " ")
fn(name)
}
}
// Generic is a generic parseable type identified by a specific flag
type Generic interface {
Set(value string) error
String() string
}
// GenericFlag is the flag type for types implementing Generic
type GenericFlag struct {
Name string
Value Generic
Usage string
EnvVar string
Hidden bool
}
// String returns the string representation of the generic flag to display the
// help text to the user (uses the String() method of the generic flag to show
// the value)
func (f GenericFlag) String() string {
return FlagStringer(f)
}
// Apply takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag
func (f GenericFlag) Apply(set *flag.FlagSet) {
val := f.Value
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
val.Set(envVal)
break
}
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
}
// GetName returns the name of a flag.
func (f GenericFlag) GetName() string {
return f.Name
}
// StringSlice is an opaque type for []string to satisfy flag.Value
type StringSlice []string
// Set appends the string value to the list of values
func (f *StringSlice) Set(value string) error {
*f = append(*f, value)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *StringSlice) String() string {
return fmt.Sprintf("%s", *f)
}
// Value returns the slice of strings set by this flag
func (f *StringSlice) Value() []string {
return *f
}
// StringSliceFlag is a string flag that can be specified multiple times on the
// command-line
type StringSliceFlag struct {
Name string
Value *StringSlice
Usage string
EnvVar string
Hidden bool
}
// String returns the usage
func (f StringSliceFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &StringSlice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
newVal.Set(s)
}
f.Value = newVal
break
}
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &StringSlice{}
}
set.Var(f.Value, name, f.Usage)
})
}
// GetName returns the name of a flag.
func (f StringSliceFlag) GetName() string {
return f.Name
}
// IntSlice is an opaque type for []int to satisfy flag.Value
type IntSlice []int
// Set parses the value into an integer and appends it to the list of values
func (f *IntSlice) Set(value string) error {
tmp, err := strconv.Atoi(value)
if err != nil {
return err
}
*f = append(*f, tmp)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *IntSlice) String() string {
return fmt.Sprintf("%#v", *f)
}
// Value returns the slice of ints set by this flag
func (f *IntSlice) Value() []int {
return *f
}
// IntSliceFlag is an int flag that can be specified multiple times on the
// command-line
type IntSliceFlag struct {
Name string
Value *IntSlice
Usage string
EnvVar string
Hidden bool
}
// String returns the usage
func (f IntSliceFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &IntSlice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(ErrWriter, err.Error())
}
}
f.Value = newVal
break
}
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &IntSlice{}
}
set.Var(f.Value, name, f.Usage)
})
}
// GetName returns the name of the flag.
func (f IntSliceFlag) GetName() string {
return f.Name
}
// Int64Slice is an opaque type for []int to satisfy flag.Value
type Int64Slice []int64
// Set parses the value into an integer and appends it to the list of values
func (f *Int64Slice) Set(value string) error {
tmp, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
*f = append(*f, tmp)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *Int64Slice) String() string {
return fmt.Sprintf("%#v", *f)
}
// Value returns the slice of ints set by this flag
func (f *Int64Slice) Value() []int64 {
return *f
}
// Int64SliceFlag is an int flag that can be specified multiple times on the
// command-line
type Int64SliceFlag struct {
Name string
Value *Int64Slice
Usage string
EnvVar string
Hidden bool
}
// String returns the usage
func (f Int64SliceFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &Int64Slice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(ErrWriter, err.Error())
}
}
f.Value = newVal
break
}
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &Int64Slice{}
}
set.Var(f.Value, name, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Int64SliceFlag) GetName() string {
return f.Name
}
// BoolFlag is a switch that defaults to false
type BoolFlag struct {
Name string
Usage string
EnvVar string
Destination *bool
Hidden bool
}
// String returns a readable representation of this value (for usage defaults)
func (f BoolFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f BoolFlag) Apply(set *flag.FlagSet) {
val := false
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
}
break
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.BoolVar(f.Destination, name, val, f.Usage)
return
}
set.Bool(name, val, f.Usage)
})
}
// GetName returns the name of the flag.
func (f BoolFlag) GetName() string {
return f.Name
}
// BoolTFlag this represents a boolean flag that is true by default, but can
// still be set to false by --some-flag=false
type BoolTFlag struct {
Name string
Usage string
EnvVar string
Destination *bool
Hidden bool
}
// String returns a readable representation of this value (for usage defaults)
func (f BoolTFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f BoolTFlag) Apply(set *flag.FlagSet) {
val := true
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.BoolVar(f.Destination, name, val, f.Usage)
return
}
set.Bool(name, val, f.Usage)
})
}
// GetName returns the name of the flag.
func (f BoolTFlag) GetName() string {
return f.Name
}
// StringFlag represents a flag that takes as string value
type StringFlag struct {
Name string
Value string
Usage string
EnvVar string
Destination *string
Hidden bool
}
// String returns the usage
func (f StringFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f StringFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
f.Value = envVal
break
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.StringVar(f.Destination, name, f.Value, f.Usage)
return
}
set.String(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f StringFlag) GetName() string {
return f.Name
}
// IntFlag is a flag that takes an integer
type IntFlag struct {
Name string
Value int
Usage string
EnvVar string
Destination *int
Hidden bool
}
// String returns the usage
func (f IntFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f IntFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil {
f.Value = int(envValInt)
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.IntVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Int(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f IntFlag) GetName() string {
return f.Name
}
// Int64Flag is a flag that takes a 64-bit integer
type Int64Flag struct {
Name string
Value int64
Usage string
EnvVar string
Destination *int64
Hidden bool
}
// String returns the usage
func (f Int64Flag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Int64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err == nil {
f.Value = envValInt
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Int64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Int64(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Int64Flag) GetName() string {
return f.Name
}
// UintFlag is a flag that takes an unsigned integer
type UintFlag struct {
Name string
Value uint
Usage string
EnvVar string
Destination *uint
Hidden bool
}
// String returns the usage
func (f UintFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f UintFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint(envValInt)
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.UintVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f UintFlag) GetName() string {
return f.Name
}
// Uint64Flag is a flag that takes an unsigned 64-bit integer
type Uint64Flag struct {
Name string
Value uint64
Usage string
EnvVar string
Destination *uint64
Hidden bool
}
// String returns the usage
func (f Uint64Flag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Uint64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err == nil {
f.Value = uint64(envValInt)
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint64(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Uint64Flag) GetName() string {
return f.Name
}
// DurationFlag is a flag that takes a duration specified in Go's duration
// format: https://golang.org/pkg/time/#ParseDuration
type DurationFlag struct {
Name string
Value time.Duration
Usage string
EnvVar string
Destination *time.Duration
Hidden bool
}
// String returns a readable representation of this value (for usage defaults)
func (f DurationFlag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f DurationFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValDuration, err := time.ParseDuration(envVal)
if err == nil {
f.Value = envValDuration
break
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.DurationVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Duration(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f DurationFlag) GetName() string {
return f.Name
}
// Float64Flag is a flag that takes an float value
type Float64Flag struct {
Name string
Value float64
Usage string
EnvVar string
Destination *float64
Hidden bool
}
// String returns the usage
func (f Float64Flag) String() string {
return FlagStringer(f)
}
// Apply populates the flag given the flag set and environment
func (f Float64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
envValFloat, err := strconv.ParseFloat(envVal, 10)
if err == nil {
f.Value = float64(envValFloat)
}
}
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Float64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Float64(name, f.Value, f.Usage)
})
}
// GetName returns the name of the flag.
func (f Float64Flag) GetName() string {
return f.Name
}
func visibleFlags(fl []Flag) []Flag {
visible := []Flag{}
for _, flag := range fl {
if !flagValue(flag).FieldByName("Hidden").Bool() {
visible = append(visible, flag)
}
}
return visible
}
func prefixFor(name string) (prefix string) {
if len(name) == 1 {
prefix = "-"
} else {
prefix = "--"
}
return
}
// Returns the placeholder, if any, and the unquoted usage string.
func unquoteUsage(usage string) (string, string) {
for i := 0; i < len(usage); i++ {
if usage[i] == '`' {
for j := i + 1; j < len(usage); j++ {
if usage[j] == '`' {
name := usage[i+1 : j]
usage = usage[:i] + name + usage[j+1:]
return name, usage
}
}
break
}
}
return "", usage
}
func prefixedNames(fullName, placeholder string) string {
var prefixed string
parts := strings.Split(fullName, ",")
for i, name := range parts {
name = strings.Trim(name, " ")
prefixed += prefixFor(name) + name
if placeholder != "" {
prefixed += " " + placeholder
}
if i < len(parts)-1 {
prefixed += ", "
}
}
return prefixed
}
func withEnvHint(envVar, str string) string {
envText := ""
if envVar != "" {
prefix := "$"
suffix := ""
sep := ", $"
if runtime.GOOS == "windows" {
prefix = "%"
suffix = "%"
sep = "%, %"
}
envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix)
}
return str + envText
}
func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr {
fv = reflect.Indirect(fv)
}
return fv
}
func stringifyFlag(f Flag) string {
fv := flagValue(f)
switch f.(type) {
case IntSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyIntSliceFlag(f.(IntSliceFlag)))
case Int64SliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyInt64SliceFlag(f.(Int64SliceFlag)))
case StringSliceFlag:
return withEnvHint(fv.FieldByName("EnvVar").String(),
stringifyStringSliceFlag(f.(StringSliceFlag)))
}
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
needsPlaceholder := false
defaultValueString := ""
val := fv.FieldByName("Value")
if val.IsValid() {
needsPlaceholder = true
defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface())
if val.Kind() == reflect.String && val.String() != "" {
defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
}
}
if defaultValueString == " (default: )" {
defaultValueString = ""
}
if needsPlaceholder && placeholder == "" {
placeholder = defaultPlaceholder
}
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString))
return withEnvHint(fv.FieldByName("EnvVar").String(),
fmt.Sprintf("%s\t%s", prefixedNames(fv.FieldByName("Name").String(), placeholder), usageWithDefault))
}
func stringifyIntSliceFlag(f IntSliceFlag) string {
defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() {
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyInt64SliceFlag(f Int64SliceFlag) string {
defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() {
defaultVals = append(defaultVals, fmt.Sprintf("%d", i))
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyStringSliceFlag(f StringSliceFlag) string {
defaultVals := []string{}
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, s := range f.Value.Value() {
if len(s) > 0 {
defaultVals = append(defaultVals, fmt.Sprintf("%q", s))
}
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifySliceFlag(usage, name string, defaultVals []string) string {
placeholder, usage := unquoteUsage(usage)
if placeholder == "" {
placeholder = defaultPlaceholder
}
defaultVal := ""
if len(defaultVals) > 0 {
defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
}
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
return fmt.Sprintf("%s\t%s", prefixedNames(name, placeholder), usageWithDefault)
}
-28
View File
@@ -1,28 +0,0 @@
package cli
// BashCompleteFunc is an action to execute when the bash-completion flag is set
type BashCompleteFunc func(*Context)
// BeforeFunc is an action to execute before any subcommands are run, but after
// the context is ready if a non-nil error is returned, no subcommands are run
type BeforeFunc func(*Context) error
// AfterFunc is an action to execute after any subcommands are run, but after the
// subcommand has finished it is run even if Action() panics
type AfterFunc func(*Context) error
// ActionFunc is the action to execute when no subcommands are specified
type ActionFunc func(*Context) error
// CommandNotFoundFunc is executed if the proper command cannot be found
type CommandNotFoundFunc func(*Context, string)
// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying
// customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error
// FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line.
type FlagStringFunc func(Flag) string
-267
View File
@@ -1,267 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"text/template"
)
// AppHelpTemplate is the text template for the Default help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
{{if .Version}}{{if not .HideVersion}}
VERSION:
{{.Version}}
{{end}}{{end}}{{if len .Authors}}
AUTHOR(S):
{{range .Authors}}{{.}}{{end}}
{{end}}{{if .VisibleCommands}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}
{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}{{if .Copyright}}
COPYRIGHT:
{{.Copyright}}
{{end}}
`
// CommandHelpTemplate is the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
// SubcommandHelpTemplate is the text template for the subcommand help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{end}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}
{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
var helpCommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
}
ShowAppHelp(c)
return nil
},
}
var helpSubcommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
}
return ShowSubcommandHelp(c)
},
}
// Prints help for the App or Command
type helpPrinter func(w io.Writer, templ string, data interface{})
// HelpPrinter is a function that writes the help output. If not set a default
// is used. The function signature is:
// func(w io.Writer, templ string, data interface{})
var HelpPrinter helpPrinter = printHelp
// VersionPrinter prints the version for the App
var VersionPrinter = printVersion
// ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error {
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
return nil
}
// DefaultAppComplete prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
for _, command := range c.App.Commands {
if command.Hidden {
continue
}
for _, name := range command.Names() {
fmt.Fprintln(c.App.Writer, name)
}
}
}
// ShowCommandHelp prints help for the given command
func ShowCommandHelp(ctx *Context, command string) error {
// show the subcommand help for a command with subcommands
if command == "" {
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
return nil
}
for _, c := range ctx.App.Commands {
if c.HasName(command) {
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
return nil
}
}
if ctx.App.CommandNotFound == nil {
return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3)
}
ctx.App.CommandNotFound(ctx, command)
return nil
}
// ShowSubcommandHelp prints help for the given subcommand
func ShowSubcommandHelp(c *Context) error {
return ShowCommandHelp(c, c.Command.Name)
}
// ShowVersion prints the version number of the App
func ShowVersion(c *Context) {
VersionPrinter(c)
}
func printVersion(c *Context) {
fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
}
// ShowCompletions prints the lists of commands within a given context
func ShowCompletions(c *Context) {
a := c.App
if a != nil && a.BashComplete != nil {
a.BashComplete(c)
}
}
// ShowCommandCompletions prints the custom completions for a given command
func ShowCommandCompletions(ctx *Context, command string) {
c := ctx.App.Command(command)
if c != nil && c.BashComplete != nil {
c.BashComplete(ctx)
}
}
func printHelp(out io.Writer, templ string, data interface{}) {
funcMap := template.FuncMap{
"join": strings.Join,
}
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data)
if err != nil {
// If the writer is closed, t.Execute will fail, and there's nothing
// we can do to recover.
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
}
return
}
w.Flush()
}
func checkVersion(c *Context) bool {
found := false
if VersionFlag.Name != "" {
eachName(VersionFlag.Name, func(name string) {
if c.GlobalBool(name) || c.Bool(name) {
found = true
}
})
}
return found
}
func checkHelp(c *Context) bool {
found := false
if HelpFlag.Name != "" {
eachName(HelpFlag.Name, func(name string) {
if c.GlobalBool(name) || c.Bool(name) {
found = true
}
})
}
return found
}
func checkCommandHelp(c *Context, name string) bool {
if c.Bool("h") || c.Bool("help") {
ShowCommandHelp(c, name)
return true
}
return false
}
func checkSubcommandHelp(c *Context) bool {
if c.GlobalBool("h") || c.GlobalBool("help") {
ShowSubcommandHelp(c)
return true
}
return false
}
func checkCompletions(c *Context) bool {
if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion {
ShowCompletions(c)
return true
}
return false
}
func checkCommandCompletions(c *Context, name string) bool {
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
ShowCommandCompletions(c, name)
return true
}
return false
}

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