Compare commits

...
79 Commits
Author SHA1 Message Date
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
37 changed files with 4771 additions and 786 deletions
+88 -10
View File
@@ -4,7 +4,7 @@ jobs:
build:
docker:
- image: circleci/golang:1.12.4
- image: circleci/golang:1.13.4
environment:
GOPATH: /go
working_directory: /home/circleci/workspace/helmfile
@@ -24,12 +24,45 @@ jobs:
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.12.4
- image: circleci/golang:1.13.4
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
@@ -58,7 +91,7 @@ jobs:
- run:
name: Install helm
environment:
HELM_VERSION: v2.13.0
HELM_VERSION: v2.15.2
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM_FILENAME}"
@@ -87,11 +120,56 @@ 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.12.4
- image: circleci/golang:1.13.4
working_directory: /home/circleci/workspace/helmfile
steps:
- checkout
@@ -105,22 +183,22 @@ jobs:
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io
make tools
go mod vendor
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make push release
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:
+8 -6
View File
@@ -1,4 +1,4 @@
FROM golang:1.12.4-alpine3.9 as builder
FROM golang:1.13.4-alpine3.10 as builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
@@ -7,17 +7,19 @@ RUN make static-linux
# -----------------------------------------------------------------------------
FROM alpine:3.8
FROM alpine:3.10
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION=v2.13.0
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="15eca6ad225a8279de80c7ced42305e24bc5ac60bb7d96f2d2fa4af86e02c794"
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
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
@@ -30,7 +32,7 @@ RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECT
chmod +x kubectl && \
mv kubectl /usr/local/bin/kubectl
RUN mkdir -p "$(helm home)/plugins"
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 && \
+44
View File
@@ -0,0 +1,44 @@
FROM golang:1.13.4-alpine3.10 as builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
COPY . /workspace/helmfile
RUN make static-linux
# -----------------------------------------------------------------------------
FROM alpine:3.10
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION=v3.0.0
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="10e1fdcca263062b1d7b2cb93a924be1ef3dd6c381263d8151dd1a20a3d8c0dc"
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"]
+7
View File
@@ -59,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
+98 -15
View File
@@ -35,7 +35,7 @@ To avoid upgrades for each iteration of `helm`, the `helmfile` executable delega
## Configuration
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.79.1](https://github.com/roboll/helmfile/tree/v0.79.1).
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.92.1](https://github.com/roboll/helmfile/tree/v0.92.1).
The default helmfile is `helmfile.yaml`:
@@ -61,6 +61,11 @@ repositories:
keyFile: optional_client_key
username: optional_username
password: optional_password
# Advanced configuration: You can use a ca bundle to use an https repo
# with a self-signed certificate
- name: insecure
url: https://charts.my-insecure-domain.com
caFile: optional_ca_crt
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
@@ -69,6 +74,7 @@ helmDefaults:
tillerNamespace: tiller-namespace #dedicated default key for tiller-namespace
tillerless: false #dedicated default key for tillerless
kubeContext: kube-context #dedicated default key for kube-context (--kube-context)
cleanupOnFail: false #dedicated default key for helm flag --cleanup-on-fail
# additional and global args passed to helm
args:
- "--set k=v"
@@ -152,6 +158,8 @@ releases:
installed: true
# restores previous state in case of failed release
atomic: true
# when true, cleans up any new resources created during a failed release
cleanupOnFail: false
# name of the tiller namespace
tillerNamespace: vault
# if true, will use the helm-tiller plugin
@@ -192,7 +200,9 @@ helmfiles:
# Override state values
values:
# Values files merged into the nested state's values
- additiona.values.yaml
- additional.values.yaml
# One important aspect of using values here is that they first need to be defined in the values section
# of the origin helmfile, so in this example key1 needs to be in the values or environments.NAME.values of path/to/subhelmfile.yaml
# Inline state values merged into the nested state's values
- key1: val1
- # All the nested state files under `helmfiles:` is processed in the order of definition.
@@ -212,17 +222,17 @@ helmfiles:
# The default is `environments: {"default": {}}` which implies:
#
# - `{{ .Environment.Name }}` evaluates to "default"
# - `{{ .Environment.Values }}` being empty
# - `{{ .Values }}` being empty
environments:
# The "default" environment is available and used when `helmfile` is run without `--environment NAME`.
default:
# Everything from the values.yaml is available via `{{ .Environment.Values.KEY }}`.
# Everything from the values.yaml is available via `{{ .Values.KEY }}`.
# Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below,
# `{{ .Environment.Values.foo.bar }}` is evaluated to `1`.
# `{{ .Values.foo.bar }}` is evaluated to `1`.
values:
- environments/default/values.yaml
# Each entry in values can be either a file path or inline values.
# The below is an example of inline values, which is merged to the `.Environment.Values`
# The below is an example of inline values, which is merged to the `.Values`
- myChartVer: 1.0.0-dev
# Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`.
# That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`.
@@ -332,13 +342,13 @@ Iterate on the `helmfile.yaml` by referencing:
```
NAME:
helmfile -
helmfile
USAGE:
helmfile [global options] command [command options] [arguments...]
VERSION:
v0.70.0
v0.92.1
COMMANDS:
deps update charts based on the contents of requirements.yaml
@@ -353,15 +363,19 @@ COMMANDS:
delete DEPRECATED: delete releases from state file (helm delete)
destroy deletes and then purges releases
test test releases from state file (helm test)
build output compiled helmfile state(s) as YAML
list list releases defined in state file
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--helm-binary value, -b value path to helm binary
--helm-binary value, -b value path to helm binary (default: "helm")
--file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference
--environment default, -e default specify the environment name. defaults to default
--state-values-set value set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--state-values-file value specify state values in a YAML file
--quiet, -q Silence output. Equivalent to log-level warn
--kube-context value Set kubectl context. Uses current context by default
--no-color Output without color
--log-level value Set log level, default info
--namespace value, -n value Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
--selector value, -l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
@@ -552,10 +566,10 @@ releases:
namespace: {{ requiredEnv "NAME" }}
chart: roboll/vault-secret-manager
values:
- values.yaml.tmpl
- values.yaml.gotmpl
```
`values.yaml.tmpl`:
`values.yaml.gotmpl`:
```yaml
db:
@@ -625,7 +639,7 @@ releaseName: prod
`values.yaml.gotmpl`
```yaml
domain: {{ .Environment.Values | getOrNil "my.domain" | default "dev.example.com" }}
domain: {{ .Values | getOrNil "my.domain" | default "dev.example.com" }}
```
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
@@ -652,17 +666,26 @@ environments:
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
releases:
- name: myapp-{{ .Environment.Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
values:
- values.yaml.gotmpl
{{ if eq (.Environment.Values.releaseName "prod" ) }}
{{ if eq (.Values.releaseName "prod" ) }}
# this release would be installed only if selected environment is `production`
- name: production-specific-release
...
{{ end }}
```
### Note
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
Prior to this [pull request](https://github.com/roboll/helmfile/pull/647), environment values were made available through the `{{ .Environment.Values.foo }}` syntax.
This is still working but is **deprecated** and the new `{{ .Values.foo }}` syntax should be used instead.
You can read more infos about the feature proposal [here](https://github.com/roboll/helmfile/issues/640).
## Environment Secrets
Environment Secrets (not to be confused with Kubernetes Secrets) are encrypted versions of `Environment Values`.
@@ -697,7 +720,7 @@ releases:
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
```yaml
{{ .Environment.Values.foo.bar }}
{{ .Values.foo.bar }}
```
## Tillerless
@@ -707,6 +730,50 @@ With the [helm-tiller](https://github.com/rimusz/helm-tiller) plugin installed,
To enable this mode, you need to define `tillerless: true` and set the `tillerNamespace` in the `helmDefaults` section
or in the `releases` entries.
## DAG-aware installation/deletion ordering
`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 `helmfile [delete|destroy]`, deletions happen in the reverse order.
That is, `myapp1` and `myapp2` are deleted first, then `servicemesh`, and finally `logging`.
## Separating helmfile.yaml into multiple independent files
Once your `helmfile.yaml` got to contain too many releases,
@@ -978,6 +1045,22 @@ Those features are set using the environment variable `HELMFILE_EXPERIMENTAL`. H
If you want to enable all experimental features set the env var to `HELMFILE_EXPERIMENTAL=true`
## Azure ACR integration
Azure offers helm repository [support for Azure Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-helm-repos) as a preview feature.
To use this you must first `az login` and then `az acr helm repo add -n <MyRegistry>`. This will extract a token for the given ACR and configure `helm` to use it, e.g. `helm repo update` should work straight away.
To use `helmfile` with ACR, on the other hand, you must either include a username/password in the repository definition for the ACR in your `helmfile.yaml` or use the `--skip-deps` switch, e.g. `helmfile template --skip-deps`.
An ACR repository definition in `helmfile.yaml` looks like this:
```
repositories:
- name: <MyRegistry>
url: https://<MyRegistry>.azurecr.io/helm/v1/repo
```
## Examples
For more examples, see the [examples/README.md](https://github.com/roboll/helmfile/blob/master/examples/README.md) or the [`helmfile`](https://github.com/cloudposse/helmfiles/tree/master/releases) distribution by [Cloud Posse](https://github.com/cloudposse/).
+2 -1
View File
@@ -16,5 +16,6 @@ information to this file.
| [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) | proof-of-concept, stage | Used with [helmfile-deploy](https://github.com/bitsofinfo/helmfile-deploy) to manage releases for dozens of apps | USA | July 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 -->
+11 -11
View File
@@ -11,13 +11,13 @@ One example of how helmfile achieves it is that, `helmfile` fails when you tried
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
```
{{ .Environment.Values.eventApi.replicas | default 1 }}
{{ .Values.eventApi.replicas | default 1 }}
```
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" }}
{{ .Values | getOrNil "eventApi.replicas" }}
```
This result in printing `<no value` in your template, that may or may not result in a failure.
@@ -25,7 +25,7 @@ This result in printing `<no value` in your template, that may or may not result
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
```
{{ .Environment.Values | getOrNil "eventApi.replicas" | default 1 }}
{{ .Values | getOrNil "eventApi.replicas" | default 1 }}
```
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
@@ -57,7 +57,7 @@ releases:
values:
- "./config/kubernetes-dashboard/values.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
values:
secrets:
- "./config/kubernetes-dashboard/secrets.yaml"
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
```
@@ -267,7 +267,7 @@ bases:
# Part 3: Dynamic Releases
releases:
- name: test1
chart: mychart-{{ .Environment.Values.myname }}
chart: mychart-{{ .Values.myname }}
values:
replicaCount: 1
image:
@@ -302,9 +302,9 @@ Where the gotmpl file loaded in the second part looks like:
```yaml
helmDefaults:
tillerNamespace: kube-system
kubeContext: {{ .Environment.Values.kubeContext }}
kubeContext: {{ .Values.kubeContext }}
verify: false
{{ if .Environment.Values.wait }}
{{ if .Values.wait }}
wait: true
{{ else }}
wait: false
@@ -314,9 +314,9 @@ helmDefaults:
force: true
```
Each go template is rendered in the context where `.Environment.Values` is inherited from the previous part.
Each go template is rendered in the context where `.Values` is inherited from the previous part.
So in `mydefaults.yaml.gotmpl`, both `.Environment.Values.kubeContext` and `.Environment.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:
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:
@@ -329,13 +329,13 @@ helmDefaults:
force: true
```
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Environment.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
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-{{ .Environment.Values.myname }}
chart: mychart-{{ .Values.myname }}
values:
replicaCount: 1
image:
+33 -12
View File
@@ -1,26 +1,47 @@
module github.com/roboll/helmfile
go 1.12
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.1
github.com/Masterminds/sprig v2.20.0+incompatible
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/google/go-cmp v0.3.0
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
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/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/pkg/errors v0.8.1 // indirect
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 v0.0.0-20160620154522-6011f165dc28
go.uber.org/atomic v1.3.2 // indirect
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.8.0
gopkg.in/yaml.v2 v2.2.1
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
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0
+845 -2
View File
File diff suppressed because it is too large Load Diff
+61 -2
View File
@@ -50,6 +50,7 @@ func main() {
cli.StringFlag{
Name: "helm-binary, b",
Usage: "path to helm binary",
Value: "helm",
},
cli.StringFlag{
Name: "file, f",
@@ -75,6 +76,10 @@ func main() {
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",
@@ -111,6 +116,10 @@ func main() {
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)
@@ -139,6 +148,10 @@ func main() {
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",
@@ -162,6 +175,10 @@ func main() {
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",
@@ -183,6 +200,11 @@ func main() {
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: action(func(run *app.App, c configImpl) error {
return run.Diff(c)
@@ -197,6 +219,10 @@ func main() {
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",
@@ -228,6 +254,10 @@ func main() {
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",
@@ -250,6 +280,10 @@ func main() {
Name: "sync",
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",
@@ -277,6 +311,10 @@ func main() {
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",
@@ -286,6 +324,11 @@ func main() {
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: "",
@@ -453,6 +496,14 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
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")
}
@@ -525,11 +576,11 @@ func (c configImpl) Selectors() []string {
return c.c.GlobalStringSlice("selector")
}
func (c configImpl) Set() map[string]interface{} {
func (c configImpl) StateValuesSet() map[string]interface{} {
return c.set
}
func (c configImpl) ValuesFiles() []string {
func (c configImpl) StateValuesFiles() []string {
return c.c.GlobalStringSlice("state-values-file")
}
@@ -537,6 +588,14 @@ 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)
}
+586 -34
View File
@@ -1,23 +1,30 @@
package app
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"text/tabwriter"
"github.com/gosuri/uitable"
"github.com/roboll/helmfile/pkg/argparser"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/state"
"github.com/variantdev/vals"
"go.uber.org/zap"
)
"path/filepath"
"sort"
const (
// cache size for improving performance of ref+.* secrets rendering
valsCacheSize = 512
)
type App struct {
@@ -27,7 +34,6 @@ type App struct {
Env string
Namespace string
Selectors []string
HelmBinary string
Args string
ValuesFiles []string
Set map[string]interface{}
@@ -49,6 +55,8 @@ type App struct {
remote *remote.Remote
helmExecer helmexec.Interface
valsRuntime vals.Evaluator
}
func New(conf ConfigProvider) *App {
@@ -58,12 +66,11 @@ func New(conf ConfigProvider) *App {
Env: conf.Env(),
Namespace: conf.Namespace(),
Selectors: conf.Selectors(),
HelmBinary: conf.HelmBinary(),
Args: conf.Args(),
FileOrDir: conf.FileOrDir(),
ValuesFiles: conf.ValuesFiles(),
Set: conf.Set(),
helmExecer: helmexec.New(conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
ValuesFiles: conf.StateValuesFiles(),
Set: conf.StateValuesSet(),
helmExecer: helmexec.New(conf.HelmBinary(), conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
Logger: conf.Logger(),
}),
})
@@ -78,17 +85,24 @@ func Init(app *App) *App {
app.fileExistsAt = fileExistsAt
app.fileExists = fileExists
app.directoryExistsAt = directoryExistsAt
var err error
app.valsRuntime, err = vals.New(vals.Options{CacheSize: valsCacheSize})
if err != nil {
panic(fmt.Sprintf("Failed to initialize vals runtime: %v", err))
}
return app
}
func (a *App) Deps(c DepsConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Deps(c)
})
}
func (a *App) Repos(c ReposConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Repos(c)
})
}
@@ -100,68 +114,68 @@ func (a *App) reverse() *App {
}
func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.DeprecatedSyncCharts(c)
})
}
func (a *App) Diff(c DiffConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Diff(c)
})
}
func (a *App) Template(c TemplateConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Template(c)
return a.ForEachState(func(run *Run) (bool, []error) {
return a.template(run, c)
})
}
func (a *App) Lint(c LintConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Lint(c)
})
}
func (a *App) Sync(c SyncConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Sync(c)
return a.ForEachState(func(run *Run) (bool, []error) {
return a.sync(run, c)
})
}
func (a *App) Apply(c ApplyConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Apply(c)
return a.ForEachState(func(run *Run) (bool, []error) {
return a.apply(run, c)
})
}
func (a *App) Status(c StatusesConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Status(c)
})
}
func (a *App) Delete(c DeleteConfigProvider) error {
return a.reverse().ForEachState(func(run *Run) []error {
return run.Delete(c)
return a.reverse().ForEachState(func(run *Run) (bool, []error) {
return a.delete(run, c.Purge(), c)
})
}
func (a *App) Destroy(c DestroyConfigProvider) error {
return a.reverse().ForEachState(func(run *Run) []error {
return run.Destroy(c)
return a.reverse().ForEachState(func(run *Run) (bool, []error) {
return a.delete(run, true, c)
})
}
func (a *App) Test(c TestConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
return run.Test(c)
})
}
func (a *App) PrintState(c StateConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return a.ForEachStateFiltered(func(run *Run) []error {
state, err := run.state.ToYaml()
if err != nil {
return []error{err}
@@ -175,7 +189,7 @@ func (a *App) ListReleases(c StateConfigProvider) error {
table := uitable.New()
table.AddRow("NAME", "NAMESPACE", "INSTALLED", "LABELS")
err := a.ForEachState(func(run *Run) []error {
err := a.ForEachStateFiltered(func(run *Run) []error {
//var releases m
for _, r := range run.state.Releases {
labels := ""
@@ -274,6 +288,7 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He
KubeContext: a.KubeContext,
glob: a.glob,
helm: a.helmExecer,
valsRuntime: a.valsRuntime,
}
var op LoadOpts
@@ -364,8 +379,10 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
if tmplErr != nil {
return appError(fmt.Sprintf("failed executing release templates in \"%s\"", f), tmplErr)
}
processed, errs := converge(templated, helm)
noMatchInHelmfiles = noMatchInHelmfiles && !processed
return context{a, templated}.clean(errs)
})
@@ -380,7 +397,7 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
return nil
}
func (a *App) ForEachState(do func(*Run) []error) error {
func (a *App) ForEachStateFiltered(do func(*Run) []error) error {
ctx := NewContext()
err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error {
run := NewRun(st, helm, ctx)
@@ -395,7 +412,88 @@ func (a *App) ForEachState(do func(*Run) []error) error {
return err
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
func (a *App) ForEachState(do func(*Run) (bool, []error)) error {
ctx := NewContext()
err := a.visitStatesWithSelectorsAndRemoteSupport(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
run := NewRun(st, helm, ctx)
return do(run)
})
if err != nil && a.ErrorHandler != nil {
return a.ErrorHandler(err)
}
return err
}
func printBatches(batches [][]state.Release) string {
buf := &bytes.Buffer{}
w := new(tabwriter.Writer)
w.Init(buf, 0, 1, 1, ' ', 0)
fmt.Fprintln(w, "GROUP\tRELEASES")
for i, batch := range batches {
ids := []string{}
for _, r := range batch {
ids = append(ids, state.ReleaseToID(&r.ReleaseSpec))
}
fmt.Fprintf(w, "%d\t%s\n", i+1, strings.Join(ids, ", "))
}
w.Flush()
return buf.String()
}
func withDAG(templated *state.HelmState, helm helmexec.Interface, logger *zap.SugaredLogger, reverse bool, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) (bool, []error) {
batches, err := templated.PlanReleases(reverse)
if err != nil {
return false, []error{err}
}
numBatches := len(batches)
logger.Debugf("processing %d groups of releases in this order:\n%s", numBatches, printBatches(batches))
any := false
for i, batch := range batches {
var targets []state.ReleaseSpec
for _, marked := range batch {
targets = append(targets, marked.ReleaseSpec)
}
var releaseIds []string
for _, r := range targets {
releaseIds = append(releaseIds, state.ReleaseToID(&r))
}
logger.Debugf("processing releases in group %d/%d: %s", i+1, numBatches, strings.Join(releaseIds, ", "))
batchSt := *templated
batchSt.Releases = targets
processed, errs := converge(&batchSt, helm)
if errs != nil && len(errs) > 0 {
return false, errs
}
any = any || processed
}
return any, nil
}
type Opts struct {
DAGEnabled bool
}
func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
opts := LoadOpts{
Selectors: a.Selectors,
}
@@ -434,7 +532,11 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
a.remote = remote
return a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
return a.visitStates(fileOrDir, opts, converge)
}
func (a *App) Wrap(converge func(*state.HelmState, helmexec.Interface) []error) func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
return func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if len(st.Selectors) > 0 {
err := st.FilterReleases()
if err != nil {
@@ -442,10 +544,6 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
}
}
if a.HelmBinary != "" {
helm.SetHelmBinary(a.HelmBinary)
}
type Key struct {
TillerNamespace, Name string
}
@@ -469,6 +567,14 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
processed := len(st.Releases) != 0 && len(errs) == 0
return processed, errs
}
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
f := a.Wrap(converge)
return a.visitStatesWithSelectorsAndRemoteSupport(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
return f(st, helm)
})
}
@@ -544,6 +650,452 @@ func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
return files, nil
}
func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) {
st := r.state
helm := r.helm
ctx := r.ctx
allReleases := st.GetReleasesWithOverrides()
toApply, err := st.GetSelectedReleasesWithOverrides()
if err != nil {
return false, []error{err}
}
if len(toApply) == 0 {
return false, nil
}
// Do build deps and prepare only on selected releases so that we won't waste time
// on running various helm commands on unnecessary releases
st.Releases = toApply
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return false, errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return false, errs
}
}
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
return false, errs
}
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
detailedExitCode := true
diffOpts := &state.DiffOpts{
NoColor: c.NoColor(),
Context: c.Context(),
Set: c.Set(),
}
var changedReleases []state.ReleaseSpec
var deletingReleases []state.ReleaseSpec
var planningErrs []error
// TODO Better way to detect diff on only filtered releases
{
changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false, diffOpts)
var err error
deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases)
if err != nil {
planningErrs = append(planningErrs, err)
}
}
fatalErrs := []error{}
for _, e := range planningErrs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
fatalErrs = append(fatalErrs, e)
}
default:
fatalErrs = append(fatalErrs, e)
}
}
if len(fatalErrs) > 0 {
return false, fatalErrs
}
releasesToBeDeleted := map[string]state.ReleaseSpec{}
for _, r := range deletingReleases {
id := state.ReleaseToID(&r)
releasesToBeDeleted[id] = r
}
releasesToBeUpdated := map[string]state.ReleaseSpec{}
for _, r := range changedReleases {
id := state.ReleaseToID(&r)
// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
if _, ok := releasesToBeDeleted[id]; !ok {
releasesToBeUpdated[id] = r
}
}
// sync only when there are changes
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
// TODO better way to get the logger
logger := c.Logger()
logger.Infof("")
logger.Infof("No affected releases")
return true, nil
}
names := []string{}
for _, r := range releasesToBeUpdated {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
// Make the output deterministic for testing purpose
sort.Strings(names)
infoMsg := fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
confMsg := fmt.Sprintf(`%s
Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`, infoMsg)
interactive := c.Interactive()
if !interactive {
a.Logger.Debug(infoMsg)
}
syncErrs := []error{}
affectedReleases := state.AffectedReleases{}
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = allReleases
if !interactive || interactive && r.askForConfirmation(confMsg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
// We deleted releases by traversing the DAG in reverse order
if len(releasesToBeDeleted) > 0 {
_, deletionErrs := withDAG(st, helm, a.Logger, true, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if r2, ok := releasesToBeDeleted[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency())
}))
if deletionErrs != nil && len(deletionErrs) > 0 {
syncErrs = append(syncErrs, deletionErrs...)
}
}
// We upgrade releases by traversing the DAG
if len(releasesToBeUpdated) > 0 {
_, updateErrs := withDAG(st, helm, a.Logger, false, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if r2, ok := releasesToBeUpdated[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
syncOpts := state.SyncOpts{
Set: c.Set(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), &syncOpts)
}))
if updateErrs != nil && len(updateErrs) > 0 {
syncErrs = append(syncErrs, updateErrs...)
}
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return true, syncErrs
}
func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) {
st := r.state
helm := r.helm
affectedReleases := state.AffectedReleases{}
toSync, err := st.GetSelectedReleasesWithOverrides()
if err != nil {
return false, []error{err}
}
if len(toSync) == 0 {
return false, nil
}
toDelete, err := st.DetectReleasesToBeDeleted(helm, toSync)
if err != nil {
return false, []error{err}
}
releasesToDelete := map[string]state.ReleaseSpec{}
for _, r := range toDelete {
id := state.ReleaseToID(&r)
releasesToDelete[id] = r
}
names := make([]string, len(toSync))
for i, r := range toSync {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
var errs []error
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
if len(releasesToDelete) > 0 {
_, deletionErrs := withDAG(st, helm, a.Logger, true, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if _, ok := releasesToDelete[state.ReleaseToID(&r)]; ok {
rs = append(rs, r)
}
}
subst.Releases = rs
return subst.DeleteReleases(&affectedReleases, helm, c.Concurrency(), purge)
}))
if deletionErrs != nil && len(deletionErrs) > 0 {
errs = append(errs, deletionErrs...)
}
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return true, errs
}
func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
st := r.state
helm := r.helm
ctx := r.ctx
allReleases := st.GetReleasesWithOverrides()
toSync, err := st.GetSelectedReleasesWithOverrides()
if err != nil {
return false, []error{err}
}
if len(toSync) == 0 {
return false, nil
}
// Do build deps and prepare only on selected releases so that we won't waste time
// on running various helm commands on unnecessary releases
st.Releases = toSync
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return false, errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return false, errs
}
}
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
return false, errs
}
toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, toSync)
if err != nil {
return false, []error{err}
}
releasesToDelete := map[string]state.ReleaseSpec{}
for _, r := range toDelete {
id := state.ReleaseToID(&r)
releasesToDelete[id] = r
}
var toUpdate []state.ReleaseSpec
for _, r := range toSync {
if _, deleted := releasesToDelete[state.ReleaseToID(&r)]; !deleted {
toUpdate = append(toUpdate, r)
}
}
releasesToUpdate := map[string]state.ReleaseSpec{}
for _, r := range toUpdate {
id := state.ReleaseToID(&r)
releasesToUpdate[id] = r
}
names := []string{}
for _, r := range releasesToUpdate {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToDelete {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
// Make the output deterministic for testing purpose
sort.Strings(names)
infoMsg := fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
a.Logger.Info(infoMsg)
var errs []error
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = allReleases
affectedReleases := state.AffectedReleases{}
if len(releasesToDelete) > 0 {
_, deletionErrs := withDAG(st, helm, a.Logger, true, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if r2, ok := releasesToDelete[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
return subst.DeleteReleasesForSync(&affectedReleases, helm, c.Concurrency())
}))
if deletionErrs != nil && len(deletionErrs) > 0 {
errs = append(errs, deletionErrs...)
}
}
if len(releasesToUpdate) > 0 {
_, syncErrs := withDAG(st, helm, a.Logger, false, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if r2, ok := releasesToUpdate[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
opts := &state.SyncOpts{
Set: c.Set(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
}))
if syncErrs != nil && len(syncErrs) > 0 {
errs = append(errs, syncErrs...)
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return true, errs
}
func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) {
st := r.state
helm := r.helm
ctx := r.ctx
allReleases := st.GetReleasesWithOverrides()
toRender, err := st.GetSelectedReleasesWithOverrides()
if err != nil {
return false, []error{err}
}
if len(toRender) == 0 {
return false, nil
}
// Do build deps and prepare only on selected releases so that we won't waste time
// on running various helm commands on unnecessary releases
st.Releases = toRender
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return false, errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return false, errs
}
}
if errs := st.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
return false, errs
}
releasesToRender := map[string]state.ReleaseSpec{}
for _, r := range toRender {
id := state.ReleaseToID(&r)
releasesToRender[id] = r
}
names := make([]string, len(toRender))
for i, r := range toRender {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
var errs []error
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = allReleases
if len(releasesToRender) > 0 {
_, templateErrs := withDAG(st, helm, a.Logger, false, a.Wrap(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
if r2, ok := releasesToRender[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
args := argparser.GetArgs(c.Args(), st)
opts := &state.TemplateOpts{
Set: c.Set(),
}
return subst.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency(), opts)
}))
if templateErrs != nil && len(templateErrs) > 0 {
errs = append(errs, templateErrs...)
}
}
return true, errs
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
+1587 -37
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -10,8 +10,8 @@ type ConfigProvider interface {
KubeContext() string
Namespace() string
Selectors() []string
Set() map[string]interface{}
ValuesFiles() []string
StateValuesSet() map[string]interface{}
StateValuesFiles() []string
Env() string
loggingConfig
@@ -26,6 +26,7 @@ type DeprecatedChartsConfigProvider interface {
type DepsConfigProvider interface {
Args() string
SkipRepos() bool
}
type ReposConfigProvider interface {
@@ -36,10 +37,14 @@ type ApplyConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
SuppressSecrets() bool
NoColor() bool
Context() int
concurrencyConfig
interactive
loggingConfig
@@ -49,6 +54,7 @@ type SyncConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
concurrencyConfig
@@ -59,11 +65,14 @@ type DiffConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
SuppressSecrets() bool
DetailedExitcode() bool
NoColor() bool
Context() int
concurrencyConfig
}
@@ -99,6 +108,7 @@ type LintConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
concurrencyConfig
@@ -108,6 +118,7 @@ type TemplateConfigProvider interface {
Args() string
Values() []string
Set() []string
SkipDeps() bool
OutputDir() string
+21 -6
View File
@@ -11,6 +11,7 @@ import (
"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"
)
@@ -26,8 +27,9 @@ type desiredStateLoader struct {
abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger
helm helmexec.Interface
logger *zap.SugaredLogger
helm helmexec.Interface
valsRuntime vals.Evaluator
}
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
@@ -74,10 +76,10 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
}
if ld.namespace != "" {
if st.Namespace != "" {
if st.OverrideNamespace != "" {
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
}
st.Namespace = ld.namespace
st.OverrideNamespace = ld.namespace
}
return st, nil
@@ -124,11 +126,24 @@ func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *e
)
}
return self, err
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)
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.helm, a.valsRuntime)
c.LoadFile = a.loadFile
return c
}
+19 -201
View File
@@ -1,9 +1,6 @@
package app
import (
"fmt"
"strings"
"github.com/roboll/helmfile/pkg/argparser"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
@@ -31,8 +28,10 @@ func (r *Run) askForConfirmation(msg string) bool {
func (r *Run) Deps(c DepsConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
return errs
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)
@@ -62,149 +61,6 @@ func (r *Run) Status(c StatusesConfigProvider) []error {
return r.state.ReleaseStatuses(r.helm, workers)
}
func (r *Run) Delete(c DeleteConfigProvider) []error {
affectedReleases := state.AffectedReleases{}
purge := c.Purge()
errs := []error{}
names := make([]string, len(r.state.Releases))
for i, r := range r.state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), purge)
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Destroy(c DestroyConfigProvider) []error {
errs := []error{}
affectedReleases := state.AffectedReleases{}
names := make([]string, len(r.state.Releases))
for i, r := range r.state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), true)
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Apply(c ApplyConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
affectedReleases := state.AffectedReleases{}
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, "apply"); errs != nil && len(errs) > 0 {
return errs
}
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
detailedExitCode := true
releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false)
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
if err != nil {
errs = append(errs, err)
}
fatalErrs := []error{}
noError := true
for _, e := range errs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
noError = false
fatalErrs = append(fatalErrs, e)
}
default:
noError = false
fatalErrs = append(fatalErrs, e)
}
}
// sync only when there are changes
if noError {
if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
// TODO better way to get the logger
logger := c.Logger()
logger.Infof("")
logger.Infof("No affected releases")
} else {
names := []string{}
for _, r := range releases {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
rs := []state.ReleaseSpec{}
for _, r := range releases {
rs = append(rs, *r)
}
for _, r := range releasesToBeDeleted {
rs = append(rs, *r)
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
st.Releases = rs
return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
}
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return fatalErrs
}
func (r *Run) Diff(c DiffConfigProvider) []error {
st := r.state
helm := r.helm
@@ -224,56 +80,15 @@ func (r *Run) Diff(c DiffConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true)
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) Sync(c SyncConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
affectedReleases := state.AffectedReleases{}
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, "sync"); errs != nil && len(errs) > 0 {
return errs
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Template(c TemplateConfigProvider) []error {
state := r.state
helm := r.helm
ctx := r.ctx
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
return errs
}
args := argparser.GetArgs(c.Args(), state)
return state.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency())
}
func (r *Run) Test(c TestConfigProvider) []error {
cleanup := c.Cleanup()
timeout := c.Timeout()
@@ -285,23 +100,26 @@ func (r *Run) Test(c TestConfigProvider) []error {
}
func (r *Run) Lint(c LintConfigProvider) []error {
state := r.state
st := r.state
helm := r.helm
ctx := r.ctx
values := c.Values()
args := argparser.GetArgs(c.Args(), state)
args := argparser.GetArgs(c.Args(), st)
workers := c.Concurrency()
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
if errs := st.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
return state.LintReleases(helm, values, args, workers)
opts := &state.LintOpts{
Set: c.Set(),
}
return st.LintReleases(helm, values, args, workers, opts)
}
+5 -2
View File
@@ -31,12 +31,15 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
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
r.logger.Debugf("template syntax error: %v", err)
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>", "")
@@ -53,7 +56,7 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
r.logger.Debugf("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
}
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
}
+4 -3
View File
@@ -1,13 +1,14 @@
package app
import (
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/testhelper"
"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"
)
+5 -3
View File
@@ -20,7 +20,8 @@ type Hook struct {
}
type event struct {
Name string
Name string
Error error
}
type Bus struct {
@@ -37,7 +38,7 @@ type Bus struct {
Logger *zap.SugaredLogger
}
func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error) {
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,
@@ -68,7 +69,8 @@ func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error
"Environment": bus.Env,
"Namespace": bus.Namespace,
"Event": event{
Name: evt,
Name: evt,
Error: evtErr,
},
}
for k, v := range context {
+1 -1
View File
@@ -104,7 +104,7 @@ func TestTrigger(t *testing.T) {
"Release": "myrel",
"HelmfileCommand": "mycmd",
}
ok, err := bus.Trigger(c.triggeredEvt, data)
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)
+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()
}
+4 -4
View File
@@ -11,12 +11,12 @@ type HelmContext struct {
WorkerIndex int
}
func (context *HelmContext) GetTillerlessArgs(helmBinary string) []string {
if context.Tillerless {
func (context *HelmContext) GetTillerlessArgs(helm *execer) []string {
if context.Tillerless && !helm.IsHelm3() {
if context.TillerNamespace != "" {
return []string{"tiller", "run", context.TillerNamespace, "--", helmBinary}
return []string{"tiller", "run", context.TillerNamespace, "--", helm.helmBinary}
} else {
return []string{"tiller", "run", "--", helmBinary}
return []string{"tiller", "run", "--", helm.helmBinary}
}
} else {
return []string{}
+46 -19
View File
@@ -13,10 +13,6 @@ import (
"go.uber.org/zap/zapcore"
)
const (
command = "helm"
)
type decryptedSecret struct {
mutex sync.RWMutex
bytes []byte
@@ -24,6 +20,7 @@ type decryptedSecret struct {
type execer struct {
helmBinary string
isHelm3 bool
runner Runner
logger *zap.SugaredLogger
kubeContext string
@@ -49,15 +46,31 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
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(logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{
helmBinary: command,
helmBinary: helmBinary,
isHelm3: detectHelm3(helmBinary, logger, runner),
logger: logger,
kubeContext: kubeContext,
runner: runner,
decryptedSecrets: make(map[string]*decryptedSecret),
}
}
func (helm *execer) SetExtraArgs(args ...string) {
@@ -68,12 +81,15 @@ func (helm *execer) SetHelmBinary(bin string) {
helm.helmBinary = bin
}
func (helm *execer) AddRepo(name, repository, certfile, keyfile, username, password string) error {
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)
}
@@ -106,7 +122,7 @@ func (helm *execer) UpdateDeps(chart string) error {
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.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
helm.write(out)
@@ -115,7 +131,7 @@ func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags .
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Getting status %v", name)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
helm.write(out)
@@ -124,16 +140,27 @@ func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...str
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
helm.logger.Infof("Listing releases matching %v", filter)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
var args []string
if helm.isHelm3() {
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
}
@@ -160,7 +187,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
helm.decryptedSecretMutex.Unlock()
helm.logger.Infof("Decrypting secret %v", absPath)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
helm.info(out)
@@ -209,7 +236,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
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() {
if helm.IsHelm3() {
args = []string{"template", name, chart}
} else {
args = []string{"template", chart, "--name", name}
@@ -222,7 +249,7 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
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.helmBinary)
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
@@ -264,7 +291,7 @@ func (helm *execer) Fetch(chart string, flags ...string) error {
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Deleting %v", name)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
helm.write(out)
@@ -273,10 +300,10 @@ func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...str
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
helm.logger.Infof("Testing %v", name)
preArgs := context.GetTillerlessArgs(helm.helmBinary)
preArgs := context.GetTillerlessArgs(helm)
env := context.getTillerlessEnv()
var args []string
if helm.isHelm3() {
if helm.IsHelm3() {
args = []string{"test", "run", name}
} else {
args = []string{"test", name}
@@ -313,6 +340,6 @@ func (helm *execer) write(out []byte) {
}
}
func (helm *execer) isHelm3() bool {
return os.Getenv("HELMFILE_HELM3") != ""
func (helm *execer) IsHelm3() bool {
return helm.isHelm3
}
+33 -20
View File
@@ -20,11 +20,11 @@ type mockRunner struct {
}
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
return []byte{}, nil
return mock.output, mock.err
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
execer := New(logger, kubeContext, &mockRunner{})
execer := New("helm", logger, kubeContext, &mockRunner{})
return execer
}
@@ -32,10 +32,7 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(logger, "dev", &ShellRunner{
Logger: logger,
})
helm := MockExecer(NewLogger(buffer, "debug"), "dev")
if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext")
}
@@ -48,11 +45,7 @@ func TestNewHelmExec(t *testing.T) {
}
func Test_SetExtraArgs(t *testing.T) {
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
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")
@@ -68,11 +61,7 @@ func Test_SetExtraArgs(t *testing.T) {
}
func Test_SetHelmBinary(t *testing.T) {
buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug")
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not helm")
}
@@ -86,7 +75,7 @@ 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", "", "")
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:
@@ -96,7 +85,17 @@ exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
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:
@@ -106,7 +105,7 @@ exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
}
buffer.Reset()
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
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:
@@ -453,7 +452,7 @@ func Test_LogLevels(t *testing.T) {
buffer.Reset()
logger := NewLogger(&buffer, logLevel)
helm := MockExecer(logger, "")
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
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)
}
@@ -507,3 +506,17 @@ exec: helm template path/to/chart --name release --values file.yml --kube-contex
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")
}
}
+6 -6
View File
@@ -8,8 +8,8 @@ import (
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
return ExitError{
msg: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
exitStatus: exitStatus,
Message: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
Code: exitStatus,
}
}
@@ -23,14 +23,14 @@ func indent(text string) string {
// ExitError is created whenever your shell command exits with a non-zero exit status
type ExitError struct {
msg string
exitStatus int
Message string
Code int
}
func (e ExitError) Error() string {
return e.msg
return e.Message
}
func (e ExitError) ExitStatus() int {
return e.exitStatus
return e.Code
}
+3 -1
View File
@@ -5,7 +5,7 @@ type Interface interface {
SetExtraArgs(args ...string)
SetHelmBinary(bin string)
AddRepo(name, repository, certfile, keyfile, username, password string) error
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
UpdateRepo() error
BuildDeps(name, chart string) error
UpdateDeps(chart string) error
@@ -19,8 +19,10 @@ type Interface interface {
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
}
+1 -1
View File
@@ -220,7 +220,7 @@ func (r *Remote) Fetch(goGetterSrc string) (string, error) {
r.Logger.Debugf("downloading %s to %s", getterSrc, getterDst)
if err := r.Getter.Get(r.Home, getterSrc, getterDst); err != nil {
if err := r.Getter.Get(r.Home, getterSrc, cacheDirPath); err != nil {
rmerr := os.RemoveAll(cacheDirPath)
if rmerr != nil {
return "", multierr.Append(err, rmerr)
+53 -5
View File
@@ -3,6 +3,7 @@ package state
import (
"fmt"
"github.com/Masterminds/semver"
"github.com/r3labs/diff"
"github.com/roboll/helmfile/pkg/helmexec"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
@@ -194,6 +195,7 @@ func (st *HelmState) updateDependenciesInTempDir(shell helmexec.DependencyUpdate
}
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
}
@@ -278,8 +280,31 @@ func (m *chartDependencyManager) lockFileName() string {
}
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
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\n", m.Name))); err != nil {
chartMetaContent := fmt.Sprintf("name: %s\nversion: 1.0.0\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
}
@@ -292,16 +317,21 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
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()
lockFileContent, err := m.readBytes(lockFile)
originalLockFileContent, err := m.readBytes(lockFile)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if lockFileContent != nil {
if err := m.writeBytes(filepath.Join(wd, "requirements.lock"), lockFileContent); err != nil {
if shell.IsHelm3() && originalLockFileContent != nil {
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
return nil, err
}
}
@@ -311,7 +341,7 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
return nil, err
}
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, "requirements.lock"))
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, chartLockFile))
if err != nil {
return nil, err
}
@@ -326,6 +356,24 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
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
}
}
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
if err != nil {
+18 -14
View File
@@ -11,6 +11,7 @@ import (
"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"
)
@@ -33,27 +34,29 @@ func (e *UndefinedEnvError) Error() string {
}
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
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) *StateCreator {
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,
logger: logger,
readFile: readFile,
fileExists: fileExists,
abs: abs,
glob: glob,
Strict: true,
helm: helm,
valsRuntime: valsRuntime,
}
}
@@ -107,6 +110,7 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
state.removeFile = os.Remove
state.fileExists = c.fileExists
state.glob = c.glob
state.valsRuntime = c.valsRuntime
return &state, nil
}
+1 -1
View File
@@ -108,7 +108,7 @@ bar: {{ readFile "bar.txt" }}
})
testFs.Cwd = "/example/path/to"
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
+8 -4
View File
@@ -93,7 +93,7 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
result.Labels[key] = s.String()
}
if result.ValuesTemplate != nil && len(result.ValuesTemplate) > 0 {
if len(result.ValuesTemplate) > 0 {
for i, t := range result.ValuesTemplate {
switch ts := t.(type) {
case map[interface{}]interface{}:
@@ -117,7 +117,9 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
}
}
result.Values = result.ValuesTemplate
var newvals []interface{}
newvals = append(newvals, result.ValuesTemplate...)
result.Values = append(newvals, result.Values...)
}
for i, t := range result.Values {
@@ -139,7 +141,7 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
result.Secrets[i] = s.String()
}
if result.SetValuesTemplate != nil && len(result.SetValuesTemplate) > 0 {
if len(result.SetValuesTemplate) > 0 {
for i, val := range result.SetValuesTemplate {
{
// name
@@ -178,7 +180,9 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
}
}
result.SetValues = result.SetValuesTemplate
var newvals []SetValue
newvals = append(newvals, result.SetValuesTemplate...)
result.SetValues = append(newvals, result.SetValues...)
}
return result, nil
+471 -94
View File
@@ -10,6 +10,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -21,9 +22,8 @@ import (
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/tmpl"
"regexp"
"github.com/tatsushid/go-prettytable"
"github.com/variantdev/vals"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
@@ -43,7 +43,7 @@ type HelmState struct {
Helmfiles []SubHelmfileSpec `yaml:"helmfiles,omitempty"`
DeprecatedContext string `yaml:"context,omitempty"`
DeprecatedReleases []ReleaseSpec `yaml:"charts,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
OverrideNamespace string `yaml:"namespace,omitempty"`
Repositories []RepositorySpec `yaml:"repositories,omitempty"`
Releases []ReleaseSpec `yaml:"releases,omitempty"`
Selectors []string `yaml:"-"`
@@ -61,8 +61,9 @@ type HelmState struct {
glob func(string) ([]string, error)
tempDir func(string, string) (string, error)
runner helmexec.Runner
helm helmexec.Interface
runner helmexec.Runner
helm helmexec.Interface
valsRuntime vals.Evaluator
}
// SubHelmfileSpec defines the subhelmfile path and options
@@ -100,6 +101,8 @@ type HelmSpec struct {
Force bool `yaml:"force"`
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
Atomic bool `yaml:"atomic"`
// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"`
TLS bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert,omitempty"`
@@ -111,6 +114,7 @@ type HelmSpec struct {
type RepositorySpec struct {
Name string `yaml:"name,omitempty"`
URL string `yaml:"url,omitempty"`
CaFile string `yaml:"caFile,omitempty"`
CertFile string `yaml:"certFile,omitempty"`
KeyFile string `yaml:"keyFile,omitempty"`
Username string `yaml:"username,omitempty"`
@@ -137,10 +141,14 @@ type ReleaseSpec struct {
Installed *bool `yaml:"installed,omitempty"`
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
Atomic *bool `yaml:"atomic,omitempty"`
// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
CleanupOnFail *bool `yaml:"cleanupOnFail,omitempty"`
// MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
// The default value for MissingFileHandler is "Error".
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
// Needs is the [TILLER_NS/][NS/]NAME representations of releases that this release depends on.
Needs []string `yaml:"needs,omitempty"`
// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
Hooks []event.Hook `yaml:"hooks,omitempty"`
@@ -189,6 +197,12 @@ type ReleaseSpec struct {
installedVersion string
}
type Release struct {
ReleaseSpec
Filtered bool
}
// SetValue are the key values to set on a helm release
type SetValue struct {
Name string `yaml:"name,omitempty"`
@@ -211,14 +225,21 @@ const MissingFileHandlerInfo = "Info"
const MissingFileHandlerWarn = "Warn"
const MissingFileHandlerDebug = "Debug"
func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if st.Namespace != "" {
spec.Namespace = st.Namespace
func (st *HelmState) ApplyOverrides(spec *ReleaseSpec) {
if st.OverrideNamespace != "" {
spec.Namespace = st.OverrideNamespace
for i := 0; i < len(spec.Needs); i++ {
n := spec.Needs[i]
if len(strings.Split(n, "/")) == 1 {
spec.Needs[i] = st.OverrideNamespace + "/" + n
}
}
}
}
type RepoUpdater interface {
AddRepo(name, repository, certfile, keyfile, username, password string) error
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
UpdateRepo() error
}
@@ -227,7 +248,7 @@ func (st *HelmState) SyncRepos(helm RepoUpdater) []error {
errs := []error{}
for _, repo := range st.Repositories {
if err := helm.AddRepo(repo.Name, repo.URL, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil {
if err := helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil {
errs = append(errs, err)
}
}
@@ -253,7 +274,12 @@ type syncPrepareResult struct {
}
// SyncReleases wrapper for executing helm upgrade on the releases
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int, opt ...SyncOpt) ([]syncPrepareResult, []error) {
opts := &SyncOpts{}
for _, o := range opt {
o.Apply(opts)
}
releases := []*ReleaseSpec{}
for i, _ := range st.Releases {
releases = append(releases, &st.Releases[i])
@@ -279,7 +305,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
},
func(workerIndex int) {
for release := range jobs {
st.applyDefaultsTo(release)
st.ApplyOverrides(release)
// If `installed: false`, the only potential operation on this release would be uninstalling.
// We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those.
@@ -318,6 +344,12 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
flags = append(flags, "--values", valfile)
}
if opts.Set != nil {
for _, s := range opts.Set {
flags = append(flags, "--set", s)
}
}
if len(errs) > 0 {
results <- syncPrepareResult{errors: errs}
continue
@@ -344,7 +376,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
}
func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) {
out, err := helm.List(context, "^"+release.Name+"$", st.connectionFlags(&release)...)
out, err := st.listReleases(context, helm, &release)
if err != nil {
return false, err
} else if out != "" {
@@ -353,10 +385,10 @@ func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helme
return false, nil
}
func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) {
detected := []*ReleaseSpec{}
for i := range st.Releases {
release := st.Releases[i]
func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {
detected := []ReleaseSpec{}
for i := range releases {
release := releases[i]
if !release.Desired() {
installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
@@ -365,16 +397,152 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
} else if installed {
// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
r := release
detected = append(detected, &r)
detected = append(detected, r)
}
}
}
return detected, nil
}
func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {
detected := []ReleaseSpec{}
for i := range releases {
release := releases[i]
installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
if err != nil {
return nil, err
} else if installed {
// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
r := release
detected = append(detected, r)
}
}
return detected, nil
}
type SyncOpts struct {
Set []string
}
type SyncOpt interface{ Apply(*SyncOpts) }
func (o *SyncOpts) Apply(opts *SyncOpts) {
*opts = *o
}
func ReleaseToID(r *ReleaseSpec) string {
var id string
tns := r.TillerNamespace
if tns != "" {
id += tns + "/"
}
ns := r.Namespace
if ns != "" {
id += ns + "/"
}
id += r.Name
return id
}
// DeleteReleasesForSync deletes releases that are marked for deletion
func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int) []error {
errs := []error{}
releases := st.Releases
jobQueue := make(chan *ReleaseSpec, len(releases))
results := make(chan syncResult, len(releases))
if workerLimit == 0 {
workerLimit = len(releases)
}
m := new(sync.Mutex)
st.scatterGather(
workerLimit,
len(releases),
func() {
for i := 0; i < len(releases); i++ {
jobQueue <- &releases[i]
}
close(jobQueue)
},
func(workerIndex int) {
for release := range jobQueue {
var relErr *ReleaseError
context := st.createHelmContext(release, workerIndex)
if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
relErr = newReleaseError(release, err)
} else {
var args []string
if helm.IsHelm3() {
args = []string{}
if release.Namespace != "" {
args = append(args, "--namespace", release.Namespace)
}
} else {
args = []string{"--purge"}
}
deletionFlags := st.appendConnectionFlags(args, release)
m.Lock()
if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
relErr = newReleaseError(release, err)
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, release)
}
m.Unlock()
}
if relErr == nil {
results <- syncResult{}
} else {
results <- syncResult{errors: []*ReleaseError{relErr}}
}
if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
if _, err := st.triggerCleanupEvent(release, "sync"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
}
},
func() {
for i := 0; i < len(releases); {
select {
case res := <-results:
if len(res.errors) > 0 {
for _, e := range res.errors {
errs = append(errs, e)
}
}
}
i++
}
},
)
if len(errs) > 0 {
return errs
}
return nil
}
// SyncReleases wrapper for executing helm upgrade on the releases
func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit)
func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error {
opts := &SyncOpts{}
for _, o := range opt {
o.Apply(opts)
}
preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts)
if len(prepErrs) > 0 {
return prepErrs
}
@@ -382,6 +550,11 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
errs := []error{}
jobQueue := make(chan *syncPrepareResult, len(preps))
results := make(chan syncResult, len(preps))
if workerLimit == 0 {
workerLimit = len(preps)
}
m := new(sync.Mutex)
st.scatterGather(
workerLimit,
@@ -407,19 +580,31 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
if err != nil {
relErr = newReleaseError(release, err)
} else if installed {
deletionFlags := st.appendConnectionFlags([]string{"--purge"}, release)
var args []string
if helm.IsHelm3() {
args = []string{}
} else {
args = []string{"--purge"}
}
deletionFlags := st.appendConnectionFlags(args, release)
m.Lock()
if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
relErr = newReleaseError(release, err)
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, release)
}
m.Unlock()
}
} else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil {
m.Lock()
affectedReleases.Failed = append(affectedReleases.Failed, release)
m.Unlock()
relErr = newReleaseError(release, err)
} else {
m.Lock()
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
m.Unlock()
installedVersion, err := st.getDeployedVersion(context, helm, release)
if err != nil { //err is not really impacting so just log it
st.logger.Debugf("getting deployed release version failed:%v", err)
@@ -434,7 +619,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
results <- syncResult{errors: []*ReleaseError{relErr}}
}
if _, err := st.triggerPostsyncEvent(release, "sync"); err != nil {
if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
@@ -463,9 +648,17 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
return nil
}
func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
flags := st.connectionFlags(release)
if helm.IsHelm3() && release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
}
return helm.List(context, "^"+release.Name+"$", flags...)
}
func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
//retrieve the version
if out, err := helm.List(context, "^"+release.Name+"$", st.connectionFlags(release)...); err == nil {
if out, err := st.listReleases(context, helm, release); err == nil {
chartName := filepath.Base(release.Chart)
//the regexp without escapes : .*\s.*\s.*\s.*\schartName-(.*?)\s
pat := regexp.MustCompile(".*\\s.*\\s.*\\s.*\\s" + chartName + "-(.*?)\\s")
@@ -550,8 +743,23 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
return temp, nil
}
type TemplateOpts struct {
Set []string
}
type TemplateOpt interface{ Apply(*TemplateOpts) }
func (o *TemplateOpts) Apply(opts *TemplateOpts) {
*opts = *o
}
// TemplateReleases wrapper for executing helm template on the releases
func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, additionalValues []string, args []string, workerLimit int) []error {
func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, additionalValues []string, args []string, workerLimit int, opt ...TemplateOpt) []error {
opts := &TemplateOpts{}
for _, o := range opt {
o.Apply(opts)
}
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
helm.SetExtraArgs()
@@ -582,7 +790,7 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
continue
}
st.applyDefaultsTo(&release)
st.ApplyOverrides(&release)
flags, err := st.flagsForTemplate(helm, &release, 0)
if err != nil {
@@ -601,6 +809,12 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
flags = append(flags, "--values", valfile)
}
if opts.Set != nil {
for _, s := range opts.Set {
flags = append(flags, "--set", s)
}
}
if len(outputDir) > 0 {
releaseOutputDir, err := st.GenerateOutputDir(outputDir, release)
if err != nil {
@@ -630,8 +844,23 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
return nil
}
type LintOpts struct {
Set []string
}
type LintOpt interface{ Apply(*LintOpts) }
func (o *LintOpts) Apply(opts *LintOpts) {
*opts = *o
}
// LintReleases wrapper for executing helm lint on the releases
func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error {
func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int, opt ...LintOpt) []error {
opts := &LintOpts{}
for _, o := range opt {
o.Apply(opts)
}
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
helm.SetExtraArgs()
@@ -677,6 +906,12 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
flags = append(flags, "--values", valfile)
}
if opts.Set != nil {
for _, s := range opts.Set {
flags = append(flags, "--set", s)
}
}
if len(errs) == 0 {
if err := helm.Lint(release.Name, temp[release.Name], flags...); err != nil {
errs = append(errs, err)
@@ -705,7 +940,12 @@ type diffPrepareResult struct {
errors []*ReleaseError
}
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) {
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool, opt ...DiffOpt) ([]diffPrepareResult, []error) {
opts := &DiffOpts{}
for _, o := range opt {
o.Apply(opts)
}
releases := []*ReleaseSpec{}
for i, _ := range st.Releases {
if !st.Releases[i].Desired() {
@@ -736,7 +976,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
for release := range jobs {
errs := []error{}
st.applyDefaultsTo(release)
st.ApplyOverrides(release)
// TODO We need a long-term fix for this :)
// See https://github.com/roboll/helmfile/issues/737
@@ -767,6 +1007,20 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
flags = append(flags, "--suppress-secrets")
}
if opts.NoColor {
flags = append(flags, "--no-color")
}
if opts.Context > 0 {
flags = append(flags, "--context", fmt.Sprintf("%d", opts.Context))
}
if opts.Set != nil {
for _, s := range opts.Set {
flags = append(flags, "--set", s)
}
}
if len(errs) > 0 {
rsErrs := make([]*ReleaseError, len(errs))
for i, e := range errs {
@@ -812,18 +1066,35 @@ func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helme
}
}
type DiffOpts struct {
Context int
NoColor bool
Set []string
}
func (o *DiffOpts) Apply(opts *DiffOpts) {
*opts = *o
}
type DiffOpt interface{ Apply(*DiffOpts) }
// DiffReleases wrapper for executing helm diff on the releases
// It returns releases that had any changes
func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool) ([]*ReleaseSpec, []error) {
preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets)
func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool, opt ...DiffOpt) ([]ReleaseSpec, []error) {
opts := &DiffOpts{}
for _, o := range opt {
o.Apply(opts)
}
preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets, opts)
if len(prepErrs) > 0 {
return []*ReleaseSpec{}, prepErrs
return []ReleaseSpec{}, prepErrs
}
jobQueue := make(chan *diffPrepareResult, len(preps))
results := make(chan diffResult, len(preps))
rs := []*ReleaseSpec{}
rs := []ReleaseSpec{}
errs := []error{}
st.scatterGather(
@@ -865,7 +1136,7 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
if res.err != nil {
errs = append(errs, res.err)
if res.err.Code == 2 {
rs = append(rs, res.err.ReleaseSpec)
rs = append(rs, *res.err.ReleaseSpec)
}
}
}
@@ -891,31 +1162,25 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [
// DeleteReleases wrapper for executing helm delete on the releases
func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error {
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
if !release.Desired() {
return nil
}
st.ApplyOverrides(&release)
flags := []string{}
if purge && !st.isHelm3() {
if purge && !helm.IsHelm3() {
flags = append(flags, "--purge")
}
flags = st.appendConnectionFlags(flags, &release)
if helm.IsHelm3() && release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
}
context := st.createHelmContext(&release, workerIndex)
installed, err := st.isReleaseInstalled(context, helm, release)
if err != nil {
if err := helm.DeleteRelease(context, release.Name, flags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, &release)
return err
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, &release)
return nil
}
if installed {
if err := helm.DeleteRelease(context, release.Name, flags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, &release)
return err
} else {
affectedReleases.Deleted = append(affectedReleases.Deleted, &release)
return nil
}
}
return nil
})
}
@@ -931,7 +1196,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
flags = append(flags, "--cleanup")
}
duration := strconv.Itoa(timeout)
if st.isHelm3() {
if helm.IsHelm3() {
duration += "s"
}
flags = append(flags, "--timeout", duration)
@@ -941,10 +1206,6 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
})
}
func (st *HelmState) isHelm3() bool {
return os.Getenv("HELMFILE_HELM3") != ""
}
// Clean will remove any generated secrets
func (st *HelmState) Clean() []error {
errs := []error{}
@@ -965,19 +1226,36 @@ func (st *HelmState) Clean() []error {
return nil
}
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
func (st *HelmState) FilterReleases() error {
var filteredReleases []ReleaseSpec
releaseSet := map[string][]ReleaseSpec{}
func (st *HelmState) GetReleasesWithOverrides() []ReleaseSpec {
var rs []ReleaseSpec
for _, r := range st.Releases {
var spec ReleaseSpec
spec = r
st.ApplyOverrides(&spec)
rs = append(rs, spec)
}
return rs
}
func (st *HelmState) SelectReleasesWithOverrides() ([]Release, error) {
rs, err := markFilteredReleases(st.GetReleasesWithOverrides(), st.Selectors)
if err != nil {
return nil, err
}
return rs, nil
}
func markFilteredReleases(releases []ReleaseSpec, selectors []string) ([]Release, error) {
var filteredReleases []Release
filters := []ReleaseFilter{}
for _, label := range st.Selectors {
for _, label := range selectors {
f, err := ParseLabels(label)
if err != nil {
return err
return nil, err
}
filters = append(filters, f)
}
for _, r := range st.Releases {
for _, r := range releases {
if r.Labels == nil {
r.Labels = map[string]string{}
}
@@ -987,22 +1265,48 @@ func (st *HelmState) FilterReleases() error {
// Strip off just the last portion for the name stable/newrelic would give newrelic
chartSplit := strings.Split(r.Chart, "/")
r.Labels["chart"] = chartSplit[len(chartSplit)-1]
var matched bool
for _, f := range filters {
if r.Labels == nil {
r.Labels = map[string]string{}
}
if f.Match(r) {
releaseSet[r.Name] = append(releaseSet[r.Name], r)
continue
matched = true
break
}
}
res := Release{
ReleaseSpec: r,
Filtered: len(filters) > 0 && !matched,
}
filteredReleases = append(filteredReleases, res)
}
for _, r := range releaseSet {
filteredReleases = append(filteredReleases, r...)
return filteredReleases, nil
}
func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) {
filteredReleases, err := st.SelectReleasesWithOverrides()
if err != nil {
return nil, err
}
st.Releases = filteredReleases
numFound := len(filteredReleases)
st.logger.Debugf("%d release(s) matching %s found in %s\n", numFound, strings.Join(st.Selectors, ","), st.FilePath)
var releases []ReleaseSpec
for _, r := range filteredReleases {
if !r.Filtered {
releases = append(releases, r.ReleaseSpec)
}
}
return releases, nil
}
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
func (st *HelmState) FilterReleases() error {
releases, err := st.GetSelectedReleasesWithOverrides()
if err != nil {
return err
}
st.Releases = releases
st.logger.Debugf("%d release(s) matching %s found in %s\n", len(releases), strings.Join(st.Selectors, ","), st.FilePath)
return nil
}
@@ -1032,27 +1336,27 @@ func (st *HelmState) PrepareReleases(helm helmexec.Interface, helmfileCommand st
}
func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("prepare", r, helmfileCommand)
return st.triggerReleaseEvent("prepare", nil, r, helmfileCommand)
}
func (st *HelmState) triggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("cleanup", r, helmfileCommand)
return st.triggerReleaseEvent("cleanup", nil, r, helmfileCommand)
}
func (st *HelmState) triggerPresyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("presync", r, helmfileCommand)
return st.triggerReleaseEvent("presync", nil, r, helmfileCommand)
}
func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("postsync", r, helmfileCommand)
func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, evtErr error, helmfileCommand string) (bool, error) {
return st.triggerReleaseEvent("postsync", evtErr, r, helmfileCommand)
}
func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd string) (bool, error) {
func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpec, helmfileCmd string) (bool, error) {
bus := &event.Bus{
Hooks: r.Hooks,
StateFilePath: st.FilePath,
BasePath: st.basePath,
Namespace: st.Namespace,
Namespace: st.OverrideNamespace,
Env: st.Env,
Logger: st.logger,
ReadFile: st.readFile,
@@ -1061,7 +1365,7 @@ func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd
"Release": r,
"HelmfileCommand": helmfileCmd,
}
return bus.Trigger(evt, data)
return bus.Trigger(evt, evtErr, data)
}
// ResolveDeps returns a copy of this helmfile state with the concrete chart version numbers filled in for remote chart dependencies
@@ -1227,7 +1531,11 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
timeout = *release.Timeout
}
if timeout != 0 {
flags = append(flags, "--timeout", fmt.Sprintf("%d", timeout))
duration := strconv.Itoa(timeout)
if helm.IsHelm3() {
duration += "s"
}
flags = append(flags, "--timeout", duration)
}
if release.Force != nil && *release.Force || release.Force == nil && st.HelmDefaults.Force {
@@ -1242,6 +1550,10 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--atomic")
}
if release.CleanupOnFail != nil && *release.CleanupOnFail || release.CleanupOnFail == nil && st.HelmDefaults.CleanupOnFail {
flags = append(flags, "--cleanup-on-fail")
}
flags = st.appendConnectionFlags(flags, release)
var err error
@@ -1323,7 +1635,33 @@ func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec,
func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.valuesFileTemplateData())
return r.RenderToBytes(path)
rawBytes, err := r.RenderToBytes(path)
if err != nil {
return nil, err
}
// If 'ref+.*' exists in file, run vals against the file
match, err := regexp.Match("ref\\+.*", rawBytes)
if err != nil {
return nil, err
}
if match {
var rawYaml map[string]interface{}
if err := yaml.Unmarshal(rawBytes, &rawYaml); err != nil {
return nil, err
}
parsedYaml, err := st.valsRuntime.Eval(rawYaml)
if err != nil {
return nil, err
}
return yaml.Marshal(parsedYaml)
}
return rawBytes, nil
}
func (st *HelmState) storage() *Storage {
@@ -1348,7 +1686,7 @@ func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) {
return nil, err
}
if len(matches) == 0 {
return nil, fmt.Errorf("no file matching %s found", hf.Path)
continue
}
for _, match := range matches {
newHelmfile := hf
@@ -1395,7 +1733,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
}
st.logger.Debugf("successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes))
generatedFiles = append(generatedFiles, valfile.Name())
case map[interface{}]interface{}:
case map[interface{}]interface{}, map[string]interface{}:
valfile, err := ioutil.TempFile("", "values")
if err != nil {
return nil, err
@@ -1431,7 +1769,17 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
}
}
generatedFiles, err := st.generateTemporaryValuesFiles(values, release.MissingFileHandler)
valuesMapSecretsRendered, err := st.valsRuntime.Eval(map[string]interface{}{"values": values})
if err != nil {
return nil, err
}
valuesSecretsRendered, ok := valuesMapSecretsRendered["values"].([]interface{})
if !ok {
return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
}
generatedFiles, err := st.generateTemporaryValuesFiles(valuesSecretsRendered, release.MissingFileHandler)
if err != nil {
return nil, err
}
@@ -1468,12 +1816,20 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
if len(release.SetValues) > 0 {
for _, set := range release.SetValues {
if set.Value != "" {
flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(set.Value)))
renderedValue, err := renderValsSecrets(st.valsRuntime, set.Value)
if err != nil {
return nil, fmt.Errorf("Failed to render set value entry in %s for release %s: %v", st.FilePath, release.Name, err)
}
flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(renderedValue[0])))
} else if set.File != "" {
flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizePath(set.File)))
} else if len(set.Values) > 0 {
items := make([]string, len(set.Values))
for i, raw := range set.Values {
renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...)
if err != nil {
return nil, fmt.Errorf("Failed to render set values entry in %s for release %s: %v", st.FilePath, release.Name, err)
}
items := make([]string, len(renderedValues))
for i, raw := range renderedValues {
items[i] = escape(raw)
}
v := strings.Join(items, ",")
@@ -1512,11 +1868,32 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
return flags, nil
}
// renderValsSecrets helper function which renders 'ref+.*' secrets
func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) {
output := make([]string, len(input))
if len(input) > 0 {
mapRendered, err := e.Eval(map[string]interface{}{"values": input})
if err != nil {
return nil, err
}
rendered, ok := mapRendered["values"].([]interface{})
if !ok {
return nil, fmt.Errorf("type %T isn't supported", mapRendered["values"])
}
for i := 0; i < len(rendered); i++ {
output[i] = fmt.Sprintf("%v", rendered[i])
}
}
return output, nil
}
// DisplayAffectedReleases logs the upgraded, deleted and in error releases
func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
if ar.Upgraded != nil {
logger.Info("\nList of updated releases :")
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "RELEASE"},
if ar.Upgraded != nil && len(ar.Upgraded) > 0 {
logger.Info("\nUPDATED RELEASES:")
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
prettytable.Column{Header: "CHART", MinWidth: 6},
prettytable.Column{Header: "VERSION", AlignRight: true},
)
@@ -1524,18 +1901,18 @@ func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
for _, release := range ar.Upgraded {
tbl.AddRow(release.Name, release.Chart, release.installedVersion)
}
tbl.Print()
logger.Info(tbl.String())
}
if ar.Deleted != nil {
logger.Info("\nList of deleted releases :")
logger.Info("RELEASE")
if ar.Deleted != nil && len(ar.Deleted) > 0 {
logger.Info("\nDELETED RELEASES:")
logger.Info("NAME")
for _, release := range ar.Deleted {
logger.Info(release.Name)
}
}
if ar.Failed != nil {
logger.Info("\nList of releases in error :")
logger.Info("RELEASE")
if ar.Failed != nil && len(ar.Failed) > 0 {
logger.Info("\nFAILED RELEASES:")
logger.Info("NAME")
for _, release := range ar.Failed {
logger.Info(release.Name)
}
+1 -1
View File
@@ -39,7 +39,7 @@ func (st *HelmState) mustLoadVals() map[string]interface{} {
func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
Environment: st.Env,
Namespace: st.Namespace,
Namespace: st.OverrideNamespace,
Values: st.mustLoadVals(),
}
}
+6 -6
View File
@@ -138,9 +138,9 @@ func TestHelmState_executeTemplates(t *testing.T) {
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
Namespace: "test-namespace_",
Repositories: nil,
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
@@ -238,9 +238,9 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
Namespace: "test-namespace_",
Repositories: nil,
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
+110 -1
View File
@@ -2,9 +2,11 @@ package state
import (
"fmt"
"sort"
"sync"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/variantdev/dag/pkg/dag"
)
type result struct {
@@ -51,9 +53,14 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun
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
inputs := st.Releases
inputsSize := len(inputs)
releases := make(chan ReleaseSpec)
@@ -96,3 +103,105 @@ func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency
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
}
+282 -258
View File
File diff suppressed because it is too large Load Diff
+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
}
+16 -3
View File
@@ -45,9 +45,15 @@ function retry() {
set -e
info "Using namespace: ${test_ns}"
info "Using Helm version: $(helm version --short --client | grep -o v.*$)"
${helm} init --wait --override spec.template.spec.automountServiceAccountToken=true
${helm} plugin install https://github.com/databus23/helm-diff --version master
# 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
@@ -60,6 +66,12 @@ test_start "happypath - simple rollout of httpbin chart"
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=$?
@@ -85,6 +97,7 @@ 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