Compare commits

...
44 Commits
Author SHA1 Message Date
mdaisuke 954673d742 Fix: forceNamespace needs shouldRun is true (#1510) 2020-10-02 08:27:39 +09:00
Yusuke Kuoka 942b9a64c0 Add support for ChartCenter (#1492)
Resolves #1326
2020-09-21 15:31:24 +09:00
Yusuke Kuoka b176408eb2 Enable helmfile test testing only enabled and selected releases (#1486)
Resolves #1483
2020-09-21 09:44:05 +09:00
Yusuke Kuoka ff81b2f279 Enable release hook template to access .Values (#1485)
Fixes #1484
2020-09-21 09:43:30 +09:00
Andrii Dembitskyi 8db03f979c Drop duplicated entry in extra functions list from readme (#1490) 2020-09-21 09:42:47 +09:00
Wi1dcard 988c218096 Support the latest Helm (>=v3.3.2) and bump the Helm version in Docker image. (#1488)
Changes:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Should fix #1338

* Bump minimum Helm 3 version to v3.1.0 for `helm template --include-crds`
2020-08-12 09:52:51 +09:00
KUOKA Yusuke c8c444debc Add test case (#1410)
To ensure that `helmfile -l foo!=bar` does match against release without the label at all.
2020-08-12 09:07:22 +09:00
KUOKA Yusuke 9a03d79a84 Fix chart fetched by go-getter not to fail due to missing dependencies (#1408)
Ref https://github.com/roboll/helmfile/issues/1401#issuecomment-670915272
2020-08-08 21:22:57 +09:00
41 changed files with 1546 additions and 314 deletions
+2 -2
View File
@@ -95,7 +95,7 @@ jobs:
- run:
name: Install helm
environment:
HELM_VERSION: v2.16.1
HELM_VERSION: v2.16.12
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM_FILENAME}"
@@ -153,7 +153,7 @@ jobs:
- run:
name: Install helm
environment:
HELM_VERSION: v3.0.0
HELM_VERSION: v3.3.3
command: |
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -Lo ${HELM_FILENAME} "https://get.helm.sh/${HELM_FILENAME}"
+2 -2
View File
@@ -11,10 +11,10 @@ FROM alpine:3.11
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION=v2.16.1
ARG HELM_VERSION=v2.16.12
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="7eebaaa2da4734242bbcdced62cc32ba8c7164a18792c8acdf16c77abffce202"
ARG HELM_SHA256="756ab375314329b66b452c0f9d569f74b0760141670217c07b79890ad314c214"
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
echo Verifying ${HELM_FILENAME}... && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
+3 -3
View File
@@ -11,10 +11,10 @@ FROM alpine:3.11
RUN apk add --no-cache ca-certificates git bash curl jq
ARG HELM_VERSION=v3.2.1
ARG HELM_VERSION=v3.3.3
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
ARG HELM_SHA256="018f9908cb950701a5d59e757653a790c66d8eda288625dbb185354ca6f41f6b"
ARG HELM_SHA256="246d58b6b353e63ae8627415a7340089015e3eb542ff7b5ce124b0b1409369cc"
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
echo Verifying ${HELM_FILENAME}... && \
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
@@ -32,7 +32,7 @@ RUN curl --retry 3 --retry-connrefused -LO "https://storage.googleapis.com/kuber
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 && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.1.3 && \
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
+77 -2
View File
@@ -103,6 +103,9 @@ helmDefaults:
# when using helm 3.2+, automatically create release namespaces if they do not exist (default true)
createNamespace: true
# these labels will be applied to all releases in a Helmfile. Useful in templating if you have a helmfile per environment or customer and don't want to copy the same label to each release
commonLabels:
hello: world
# The desired states of Helm releases.
#
@@ -298,6 +301,20 @@ apiVersions:
Helmfile uses [Go templates](https://godoc.org/text/template) for templating your helmfile.yaml. While go ships several built-in functions, we have added all of the functions in the [Sprig library](https://godoc.org/github.com/Masterminds/sprig).
We also added the following functions:
- `requiredEnv`
- `exec`
- `readFile`
- `toYaml`
- `fromYaml`
- `setValueAtPath`
- `get` (Sprig's original `get` is available as `sprigGet`)
- `tpl`
- `required`
- `fetchSecretValue`
- `expandSecretRefs`
We also added one special template function: `requiredEnv`.
The `requiredEnv` function allows you to declare a particular environment variable as required for template rendering.
If the environment variable is unset or empty, the template rendering will fail with an error message.
@@ -420,6 +437,9 @@ GLOBAL OPTIONS:
--interactive, -i Request confirmation before attempting to modify clusters
--help, -h show help
--version, -v print the version
Environment variables:
HELMFILE_ENVIRONMENT specify the environment name, the command line option '-e' have precedence
```
### sync
@@ -519,6 +539,41 @@ The `selector` parameter can be specified multiple times. Each parameter is reso
In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`).
`commonLabels` can be used when you want to apply the same label to all releases and use [templating](##Templates) based on that.
For instance, you install a number of charts on every customer but need to provide different values file per customer.
templates/common.yaml:
```
templates:
nginx: &nginx
name: nginx
chart: stable/nginx-ingress
values:
- ../values/common/{{ .Release.Name }}.yaml
- ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml
cert-manager: &cert-manager
name: cert-manager
chart: jetstack/cert-manager
values:
- ../values/common/{{ .Release.Name }}.yaml
- ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml
```
helmfile.yaml:
```
{{ readFile "templates/common.yaml" }}
commonLabels:
customer: company
releases:
- <<: *nginx
- <<: *cert-manager
```
## Templates
You can use go's text/template expressions in `helmfile.yaml` and `values.yaml.gotmpl` (templated helm values files). `values.yaml` references will be used verbatim. In other words:
@@ -1023,7 +1078,7 @@ hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
```
@@ -1166,9 +1221,29 @@ For 1, you run `helmfile apply` on CI to deploy ArgoCD and the infrastructure co
> helmfile config for this phase often reside within the same directory as your Terraform project. So connecting the two with [terraform-provider-helmfile](https://github.com/mumoshu/terraform-provider-helmfile) may be helpful
For 2, another app-centric CI or bot should run `helmfile template --output-dir-template gitops//{{.Release.Name}} && cd gitops && git add . && git commit && git push` to render/commit manifests,
For 2, another app-centric CI or bot should render/commit manifests by running:
```
helmfile template --output-dir-template $(pwd)/gitops//{{.Release.Name}}
cd gitops
git add .
git commit -m 'some message'
git push origin $BRANCH
```
> Note that `$(pwd)` is necessary when `hemlfile.yaml` has one or more sub-helmfiles in nested directories,
> because setting a relative file path in `--output-dir` or `--output-dir-template` results in each sub-helmfile render
> to the directory relative to the specified path.
so that they can be deployed by Argo CD as usual.
The CI or bot can optionally submit a PR to be review by human, running:
```
hub pull-request -b main -l gitops -m 'some description'
```
Recommendations:
- Do create ArgoCD `Application` custom resource per Helm/Helmfile release, each point to respective sub-directory generated by `helmfile template --output-dir-template`
+2
View File
@@ -11,8 +11,10 @@ information to this file.
|---|---|---|---|---|
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249), [Talk](https://www.slideshare.net/GregoryTaylor11/helm-at-reddit-from-local-dev-staging-to-production) | San Francisco, CA | April 2018 |
| [ricardo.ch](https://www.ricardo.ch/) | production | We're deploying our complete application platform using Helmfile. | Zug, Switzerland | April 2018 |
| [Hellofresh](https://engineering.hellofresh.com/) | production | We've been using helmfile in production since April 2018 for deploying all our infrastructure applications | Berlin, Germany | April 2018 |
| [Cherre](https://cherre.com/) | production | We have no public posts about using Helmfile, but we have been using it for a long time now | New York, NY | October 2018 |
| [Sight Machine](https://sightmachine.com/) | production | We don't have anything publicly posted about it, but have been using it for quite a while in production. | San Francisco, CA and Ann Arbor, MI | December 2018 |
| [TailorMed](https://tailormed.co/) | production | We're deploying all of our platform and all K8s resources using Helmfile. The power of Helmfile are more then amazing. Thank you! | Tel-Aviv, Israel | September 2020 |
| [VSHN – The DevOps Company](https://vshn.ch) | production | | Zurich, Switzerland | March 2019 |
| [Vlocity](https://vlocity.com/) | proof-of-concept | | Melbourne, Australia | March 2019 |
| [transit](https://transit.app/) | production | [Blog post](https://medium.com/@naseem_60378/helmfile-its-like-a-helm-for-your-helm-74a908581599). | Montreal, Canada | March 2019 |
+5 -8
View File
@@ -4,9 +4,8 @@ go 1.14
require (
github.com/Azure/azure-sdk-for-go v35.0.0+incompatible // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/Masterminds/sprig/v3 v3.1.0
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/go-test/deep v1.0.3
github.com/golang/mock v1.4.3
@@ -16,23 +15,21 @@ require (
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
github.com/hashicorp/go-version v1.2.0
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.9
github.com/imdario/mergo v0.3.11
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad
github.com/spf13/cobra v0.0.3
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/urfave/cli v1.20.0
github.com/variantdev/chartify v0.3.11
github.com/variantdev/chartify v0.4.2
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
github.com/variantdev/vals v0.10.0
github.com/variantdev/vals v0.10.2
go.uber.org/multierr v1.1.0
go.uber.org/zap v1.9.1
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.3.0
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.0.3-0.20200410202438-4e4a41b7851a
k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b
+33
View File
@@ -114,8 +114,12 @@ github.com/Masterminds/semver v1.4.1 h1:CaDA1wAoM3rj9sAFyyZP37LloExUzxFGYt+DqJ87
github.com/Masterminds/semver v1.4.1/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y=
github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA=
github.com/Microsoft/go-winio v0.4.13 h1:Hmi80lzZuI/CaYmlJp/b+FjZdRZhKu9c2mDVqKlLWVs=
github.com/Microsoft/go-winio v0.4.13/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
@@ -264,6 +268,8 @@ github.com/fujiwara/tfstate-lookup v0.0.4 h1:NGH9y+U4RDO3ooMYjK+fq1iYAkrBfCNZbZw
github.com/fujiwara/tfstate-lookup v0.0.4/go.mod h1:kluvnw25uJ8bszZldMoewWxylt7QFvuzTGN+Rr911qs=
github.com/fujiwara/tfstate-lookup v0.0.9 h1:U/iAQusJBJ5EsxdU48rZGaUSMiZmK8cTCkyCJpiFnMw=
github.com/fujiwara/tfstate-lookup v0.0.9/go.mod h1:08o5Rm5pKzvIxoZe3D0NIT8AHYBqTzyUU03BZ/c0IKA=
github.com/fujiwara/tfstate-lookup v0.0.12 h1:Ax8Xws/BvXRO2NH13QqGZJtk6X5aITJCSBdlaWqMLwU=
github.com/fujiwara/tfstate-lookup v0.0.12/go.mod h1:08o5Rm5pKzvIxoZe3D0NIT8AHYBqTzyUU03BZ/c0IKA=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/gammazero/deque v0.0.0-20190130191400-2afb3858e9c7 h1:D2LrfOPgGHQprIxmsTpxtzhpmF66HoM6rXSmcqaX7h8=
@@ -358,6 +364,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
@@ -516,9 +524,15 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 h1:3K3KcD4S6/Y2hevi70EzUTNKOS3cryQyhUnkjE6Tz0w=
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
@@ -761,6 +775,8 @@ github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:s
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -777,6 +793,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw=
github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 h1:BhIUXV2ySTLrKgh/Hnts+QTQlIbWtomXt3LMdzME0A0=
@@ -827,6 +844,12 @@ github.com/variantdev/chartify v0.3.10 h1:hi/9z0GPPOKAubemWhJwwsnYJnCKKdIZyo1ouO
github.com/variantdev/chartify v0.3.10/go.mod h1:0tw+4doFHsNnhttYx7I9Pv/dsZ82BD4UuTV9saBOcfw=
github.com/variantdev/chartify v0.3.11 h1:+jknp9CQ4kvgVMifuwxuu8/5UAVajuNrPBLWnTCTNpA=
github.com/variantdev/chartify v0.3.11/go.mod h1:0tw+4doFHsNnhttYx7I9Pv/dsZ82BD4UuTV9saBOcfw=
github.com/variantdev/chartify v0.4.0 h1:Ze+gfykTqFuQgfH2w6/ZqMwsrGrRU8yIsW8M+e7jAHg=
github.com/variantdev/chartify v0.4.0/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
github.com/variantdev/chartify v0.4.1 h1:0a8Ipf7n65mbQaAAfI9JEbdDauMnwKs0Gdq/yu5jI7I=
github.com/variantdev/chartify v0.4.1/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
github.com/variantdev/chartify v0.4.2 h1:Vw647JysxJ4kz+6vOxOmrctRoTTf5qmpleB6hTQEjUk=
github.com/variantdev/chartify v0.4.2/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363 h1:KrfQBEUn+wEOQ/6UIfoqRDvn+Q/wZridQ7t0G1vQqKE=
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363/go.mod h1:pH1TQsNSLj2uxMo9NNl9zdGy01Wtn+/2MT96BrKmVyE=
github.com/variantdev/vals v0.4.0 h1:O1O7/sWhlvozcY2DjZBzlE1notxwVo6UBT1+w7HsO/k=
@@ -841,6 +864,10 @@ github.com/variantdev/vals v0.9.3 h1:l9AT8UgvgU1GBADg5ZPssDVkZTkRr9SaVjvinVjQebk
github.com/variantdev/vals v0.9.3/go.mod h1:tAQZUHZAYfgNJJ3BzmUDXeN+r6RLk0HQPSQ+gdYSP5I=
github.com/variantdev/vals v0.10.0 h1:2diYujperePiJGQbQx5TEjWzhM0J7WM5+8UZnPXiaOc=
github.com/variantdev/vals v0.10.0/go.mod h1:tAQZUHZAYfgNJJ3BzmUDXeN+r6RLk0HQPSQ+gdYSP5I=
github.com/variantdev/vals v0.10.1 h1:yJHcElbczPP4adoR8FQloyV9kvnO5zdDzziRBq71C3Q=
github.com/variantdev/vals v0.10.1/go.mod h1:tAQZUHZAYfgNJJ3BzmUDXeN+r6RLk0HQPSQ+gdYSP5I=
github.com/variantdev/vals v0.10.2 h1:2ZtEV1BcED2WkfL8pZd0kO4HGpow6faIuFSz+c8zXoM=
github.com/variantdev/vals v0.10.2/go.mod h1:0KbR+woANJnnZakBreV1s8CBsmAY0O2mZNQBqilsU00=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
@@ -883,6 +910,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIoF2s4qxv0xSSS0BVZUE/ss=
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1077,6 +1106,8 @@ google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY=
google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1167,6 +1198,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA=
gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d h1:LCPbGQ34PMrwad11aMZ+dbz5SAsq/0ySjRwQ8I9Qwd8=
+48 -2
View File
@@ -257,6 +257,36 @@ func main() {
return run.Template(c)
}),
},
{
Name: "write-values",
Usage: "write values files for releases. Similar to `helmfile template`, write values files instead of manifests.",
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "set",
Usage: "additional values to be merged into the command",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.StringFlag{
Name: "output-file-template",
Usage: "go text template for generating the output file. Default: {{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}/{{ .Release.Name}}.yaml",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent downloads of release charts",
},
cli.BoolFlag{
Name: "skip-deps",
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.WriteValues(c)
}),
},
{
Name: "lint",
Usage: "lint charts from state file (helm lint)",
@@ -467,7 +497,12 @@ func main() {
{
Name: "build",
Usage: "output compiled helmfile state(s) as YAML",
Flags: []cli.Flag{},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "embed-values",
Usage: "Read all the values files for every release and embed into the output helmfile.yaml",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.PrintState(c)
}),
@@ -569,6 +604,10 @@ func (c configImpl) OutputDirTemplate() string {
return c.c.String("output-dir-template")
}
func (c configImpl) OutputFileTemplate() string {
return c.c.String("output-file-template")
}
func (c configImpl) Validate() bool {
return c.c.Bool("validate")
}
@@ -674,6 +713,10 @@ func (c configImpl) Context() int {
return c.c.Int("context")
}
func (c configImpl) EmbedValues() bool {
return c.c.Bool("embed-values")
}
func (c configImpl) Logger() *zap.SugaredLogger {
return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
}
@@ -681,7 +724,10 @@ func (c configImpl) Logger() *zap.SugaredLogger {
func (c configImpl) Env() string {
env := c.c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
env = os.Getenv("HELMFILE_ENVIRONMENT")
if env == "" {
env = state.DefaultEnv
}
}
return env
}
+158 -18
View File
@@ -103,7 +103,10 @@ func Init(app *App) *App {
func (a *App) Deps(c DepsConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
prepErr := run.withPreparedCharts(false, c.SkipRepos(), "deps", func() {
prepErr := run.withPreparedCharts("deps", state.ChartPrepareOptions{
SkipRepos: c.SkipRepos(),
SkipResolve: true,
}, func() {
errs = run.Deps(c)
})
@@ -119,7 +122,7 @@ func (a *App) Repos(c ReposConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
var reposErr error
err := run.withPreparedCharts(false, true, "repos", func() {
err := run.withPreparedCharts("repos", state.ChartPrepareOptions{SkipRepos: true}, func() {
reposErr = run.Repos(c)
})
@@ -137,7 +140,7 @@ func (a *App) Repos(c ReposConfigProvider) error {
func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
err := run.withPreparedCharts(false, true, "charts", func() {
err := run.withPreparedCharts("charts", state.ChartPrepareOptions{SkipRepos: true}, func() {
errs = run.DeprecatedSyncCharts(c)
})
@@ -163,7 +166,7 @@ func (a *App) Diff(c DiffConfigProvider) error {
var errs []error
prepErr := run.withPreparedCharts(false, c.SkipDeps(), "diff", func() {
prepErr := run.withPreparedCharts("diff", state.ChartPrepareOptions{SkipRepos: c.SkipDeps()}, func() {
msg, matched, affected, errs = a.diff(run, c)
})
@@ -218,7 +221,10 @@ func (a *App) Template(c TemplateConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
// `helm template` in helm v2 does not support local chart.
// So, we set forceDownload=true for helm v2 only
prepErr := run.withPreparedCharts(!run.helm.IsHelm3(), c.SkipDeps(), "template", func() {
prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{
ForceDownload: !run.helm.IsHelm3(),
SkipRepos: c.SkipDeps(),
}, func() {
ok, errs = a.template(run, c)
})
@@ -230,10 +236,32 @@ func (a *App) Template(c TemplateConfigProvider) error {
}, SetFilter(true))
}
func (a *App) WriteValues(c WriteValuesConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
// `helm template` in helm v2 does not support local chart.
// So, we set forceDownload=true for helm v2 only
prepErr := run.withPreparedCharts("write-values", state.ChartPrepareOptions{
ForceDownload: !run.helm.IsHelm3(),
SkipRepos: c.SkipDeps(),
}, func() {
ok, errs = a.writeValues(run, c)
})
if prepErr != nil {
errs = append(errs, prepErr)
}
return
}, SetFilter(true))
}
func (a *App) Lint(c LintConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
// `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here
prepErr := run.withPreparedCharts(true, c.SkipDeps(), "lint", func() {
prepErr := run.withPreparedCharts("lint", state.ChartPrepareOptions{
ForceDownload: true,
SkipRepos: c.SkipDeps(),
}, func() {
errs = run.Lint(c)
})
@@ -247,7 +275,9 @@ func (a *App) Lint(c LintConfigProvider) error {
func (a *App) Sync(c SyncConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
prepErr := run.withPreparedCharts(false, c.SkipDeps(), "sync", func() {
prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{
SkipRepos: c.SkipDeps(),
}, func() {
ok, errs = a.sync(run, c)
})
@@ -269,7 +299,9 @@ func (a *App) Apply(c ApplyConfigProvider) error {
opts = append(opts, SetRetainValuesFiles(c.RetainValuesFiles()))
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
prepErr := run.withPreparedCharts(false, c.SkipDeps(), "apply", func() {
prepErr := run.withPreparedCharts("apply", state.ChartPrepareOptions{
SkipRepos: c.SkipDeps(),
}, func() {
matched, updated, es := a.apply(run, c)
mut.Lock()
@@ -301,7 +333,9 @@ func (a *App) Apply(c ApplyConfigProvider) error {
func (a *App) Status(c StatusesConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
err := run.withPreparedCharts(false, true, "status", func() {
err := run.withPreparedCharts("status", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
errs = run.Status(c)
})
@@ -315,7 +349,9 @@ func (a *App) Status(c StatusesConfigProvider) error {
func (a *App) Delete(c DeleteConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
err := run.withPreparedCharts(false, true, "delete", func() {
err := run.withPreparedCharts("delete", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
ok, errs = a.delete(run, c.Purge(), c)
})
@@ -329,7 +365,9 @@ func (a *App) Delete(c DeleteConfigProvider) error {
func (a *App) Destroy(c DestroyConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
err := run.withPreparedCharts(false, true, "destroy", func() {
err := run.withPreparedCharts("destroy", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
ok, errs = a.delete(run, true, c)
})
@@ -349,8 +387,10 @@ func (a *App) Test(c TestConfigProvider) error {
"or set helm.sh/hook-delete-policy\n")
}
err := run.withPreparedCharts(false, true, "test", func() {
errs = run.Test(c)
err := run.withPreparedCharts("test", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
errs = a.test(run, c)
})
if err != nil {
@@ -363,13 +403,38 @@ func (a *App) Test(c TestConfigProvider) error {
func (a *App) PrintState(c StateConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
err := run.withPreparedCharts(false, true, "build", func() {
state, err := run.state.ToYaml()
err := run.withPreparedCharts("build", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
if c.EmbedValues() {
for i := range run.state.Releases {
r := run.state.Releases[i]
values, err := run.state.LoadYAMLForEmbedding(&r, r.Values, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
}
run.state.Releases[i].Values = values
secrets, err := run.state.LoadYAMLForEmbedding(&r, r.Secrets, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
}
run.state.Releases[i].Secrets = secrets
}
}
stateYaml, err := run.state.ToYaml()
if err != nil {
errs = []error{err}
return
}
fmt.Printf("---\n# Source: %s\n\n%+v", run.state.FilePath, state)
fmt.Printf("---\n# Source: %s\n\n%+v", run.state.FilePath, stateYaml)
errs = []error{}
})
@@ -386,15 +451,32 @@ func (a *App) ListReleases(c ListConfigProvider) error {
var releases []*HelmRelease
err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
err := run.withPreparedCharts(false, true, "list", func() {
err := run.withPreparedCharts("list", state.ChartPrepareOptions{
SkipRepos: true,
}, func() {
//var releases m
for _, r := range run.state.Releases {
labels := ""
for k, v := range r.Labels {
if r.Labels == nil {
r.Labels = map[string]string{}
}
for k, v := range run.state.CommonLabels {
r.Labels[k] = v
}
var keys []string
for k, _ := range r.Labels {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := r.Labels[k]
labels = fmt.Sprintf("%s,%s:%s", labels, k, v)
}
labels = strings.Trim(labels, ",")
installed := r.Installed == nil || *r.Installed
releases = append(releases, &HelmRelease{
Name: r.Name,
@@ -1355,6 +1437,64 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) {
return true, errs
}
func (a *App) writeValues(r *Run, c WriteValuesConfigProvider) (bool, []error) {
st := r.state
helm := r.helm
allReleases := st.GetReleasesWithOverrides()
toRender, err := a.getSelectedReleases(r)
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
releasesToWrite := map[string]state.ReleaseSpec{}
for _, r := range toRender {
id := state.ReleaseToID(&r)
if r.Installed != nil && !*r.Installed {
continue
}
releasesToWrite[id] = r
}
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(releasesToWrite) > 0 {
_, writeErrs := 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 := releasesToWrite[state.ReleaseToID(&r)]; ok {
rs = append(rs, r2)
}
}
subst.Releases = rs
opts := &state.WriteValuesOpts{
Set: c.Set(),
OutputFileTemplate: c.OutputFileTemplate(),
}
return subst.WriteReleasesValues(helm, c.Values(), opts)
}))
if writeErrs != nil && len(writeErrs) > 0 {
errs = append(errs, writeErrs...)
}
}
return true, errs
}
func fileExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsRegular()
+18 -8
View File
@@ -4,8 +4,6 @@ import (
"bufio"
"bytes"
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/remote"
"io"
"log"
"os"
@@ -17,6 +15,9 @@ import (
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/exectest"
"gotest.tools/v3/assert"
@@ -746,6 +747,8 @@ func runFilterSubHelmFilesTests(testcases []struct {
expectErr bool
errMsg string
}, files map[string]string, t *testing.T, testName string) {
t.Helper()
for _, testcase := range testcases {
actual := []string{}
@@ -2271,6 +2274,10 @@ func (c configImpl) Concurrency() int {
return 1
}
func (c configImpl) EmbedValues() bool {
return false
}
func (c configImpl) Output() string {
return c.output
}
@@ -2455,7 +2462,7 @@ func (helm *mockHelmExec) GetVersion() helmexec.Version {
return helmexec.Version{}
}
func (helm *mockHelmExec) IsVersionAtLeast(major int, minor int) bool {
func (helm *mockHelmExec) IsVersionAtLeast(major int, minor int, patch int) bool {
return false
}
@@ -2490,6 +2497,7 @@ releases:
var wantRepos = []mockRepo{
{Name: "stable"},
{Name: "stable2"},
}
var buffer bytes.Buffer
@@ -4050,6 +4058,8 @@ releases:
func TestList(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
commonLabels:
common: label
releases:
- name: myrelease1
chart: mychart1
@@ -4093,11 +4103,11 @@ releases:
assert.NilError(t, err)
})
expected := `NAME NAMESPACE ENABLED LABELS
myrelease1 false id:myrelease1
myrelease2 true
myrelease3 true
myrelease4 true id:myrelease1
expected := `NAME NAMESPACE ENABLED LABELS
myrelease1 false common:label,id:myrelease1
myrelease2 true common:label
myrelease3 true
myrelease4 true id:myrelease1
`
assert.Equal(t, expected, out)
}
+8
View File
@@ -137,6 +137,13 @@ type TemplateConfigProvider interface {
concurrencyConfig
}
type WriteValuesConfigProvider interface {
Values() []string
Set() []string
OutputFileTemplate() string
SkipDeps() bool
}
type StatusesConfigProvider interface {
Args() string
@@ -144,6 +151,7 @@ type StatusesConfigProvider interface {
}
type StateConfigProvider interface {
EmbedValues() bool
}
type concurrencyConfig interface {
+5 -11
View File
@@ -4,16 +4,14 @@ import (
"bytes"
"errors"
"fmt"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"path/filepath"
"sort"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/state"
"github.com/variantdev/vals"
"go.uber.org/zap"
"path/filepath"
)
const (
@@ -68,11 +66,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
}
if opts.Reverse {
rev := func(i, j int) bool {
return j < i
}
sort.Slice(st.Releases, rev)
sort.Slice(st.Helmfiles, rev)
st.Reverse()
}
if ld.overrideKubeContext != "" {
@@ -222,7 +216,7 @@ func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Enviro
if finalState == nil {
finalState = currentState
} else {
if err := mergo.Merge(finalState, currentState, mergo.WithOverride); err != nil {
if err := mergo.Merge(&finalState.ReleaseSetSpec, &currentState.ReleaseSetSpec, mergo.WithOverride); err != nil {
return nil, err
}
}
+5 -5
View File
@@ -156,8 +156,8 @@ anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0
{Name: "backend-v2", Flags: []string{}},
{Name: "backend-v1", Flags: []string{}},
{Name: "anotherbackend", Flags: []string{}},
{Name: "database", Flags: []string{}},
{Name: "servicemesh", Flags: []string{}},
{Name: "database", Flags: []string{}},
{Name: "front-proxy", Flags: []string{}},
{Name: "logging", Flags: []string{}},
},
@@ -291,7 +291,7 @@ GROUP RELEASES
1 frontend-v3, frontend-v2, frontend-v1
2 backend-v2, backend-v1
3 anotherbackend
4 database, servicemesh
4 servicemesh, database
5 front-proxy, logging
processing releases in group 1/5: frontend-v3, frontend-v2, frontend-v1
@@ -303,9 +303,9 @@ release "backend-v2" processed
release "backend-v1" processed
processing releases in group 3/5: anotherbackend
release "anotherbackend" processed
processing releases in group 4/5: database, servicemesh
release "database" processed
processing releases in group 4/5: servicemesh, database
release "servicemesh" processed
release "database" processed
processing releases in group 5/5: front-proxy, logging
release "front-proxy" processed
release "logging" processed
@@ -318,8 +318,8 @@ frontend-v1
backend-v2
backend-v1
anotherbackend
database
servicemesh
database
front-proxy
logging
`,
+1 -1
View File
@@ -97,7 +97,7 @@ func (helm *noCallHelmExec) GetVersion() helmexec.Version {
return helmexec.Version{}
}
func (helm *noCallHelmExec) IsVersionAtLeast(major int, minor int) bool {
func (helm *noCallHelmExec) IsVersionAtLeast(major int, minor int, patch int) bool {
helm.doPanic()
return false
}
+20 -5
View File
@@ -36,12 +36,12 @@ func (r *Run) askForConfirmation(msg string) bool {
return AskForConfirmation(msg)
}
func (r *Run) withPreparedCharts(forceDownload, skipRepos bool, helmfileCommand string, f func()) error {
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
if r.ReleaseToChart != nil {
panic("Run.PrepareCharts can be called only once")
}
if !skipRepos {
if !opts.SkipRepos {
ctx := r.ctx
if err := ctx.SyncReposOnce(r.state, r.helm); err != nil {
return err
@@ -59,7 +59,7 @@ func (r *Run) withPreparedCharts(forceDownload, skipRepos bool, helmfileCommand
return err
}
releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, forceDownload, skipRepos)
releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, opts)
if len(errs) > 0 {
return fmt.Errorf("%v", errs)
@@ -161,14 +161,29 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error)
return infoMsg, true, len(deleted) > 0 || len(updated) > 0, errs
}
func (r *Run) Test(c TestConfigProvider) []error {
func (a *App) test(r *Run, c TestConfigProvider) []error {
cleanup := c.Cleanup()
timeout := c.Timeout()
concurrency := c.Concurrency()
st := r.state
toTest, err := a.getSelectedReleases(r)
if err != nil {
return []error{err}
}
if len(toTest) == 0 {
return nil
}
// Do test only on selected releases, because that's what the user intended
// with conditions and selectors
st.Releases = toTest
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.TestReleases(r.helm, cleanup, timeout, concurrency)
return st.TestReleases(r.helm, cleanup, timeout, concurrency)
}
func (r *Run) Lint(c LintConfigProvider) []error {
+10 -2
View File
@@ -10,7 +10,11 @@ func TestGetArgs(t *testing.T) {
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns"
defaultArgs := []string{"--recreate-pods", "--force"}
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
testState := &state.HelmState{HelmDefaults: Helmdefaults}
testState := &state.HelmState{
ReleaseSetSpec: state.ReleaseSetSpec{
HelmDefaults: Helmdefaults,
},
}
receivedArgs := GetArgs(args, testState)
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force"
@@ -24,7 +28,11 @@ func Test2(t *testing.T) {
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns"
defaultArgs := []string{"--recreate-pods", "--force"}
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
testState := &state.HelmState{HelmDefaults: Helmdefaults}
testState := &state.HelmState{
ReleaseSetSpec: state.ReleaseSetSpec{
HelmDefaults: Helmdefaults,
},
}
receivedArgs := GetArgs(args, testState)
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force"
+2 -1
View File
@@ -41,7 +41,8 @@ type Bus struct {
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,
Dir: bus.BasePath,
Logger: bus.Logger,
}
}
+2 -2
View File
@@ -170,12 +170,12 @@ func (helm *Helm) GetVersion() helmexec.Version {
return helmexec.Version{}
}
func (helm *Helm) IsVersionAtLeast(major int, minor int) bool {
func (helm *Helm) IsVersionAtLeast(major int, minor int, patch int) bool {
if helm.Version == nil {
return false
}
return helm.Version.Major >= major && minor >= helm.Version.Minor
return helm.Version.Major >= major && minor >= helm.Version.Minor && patch >= helm.Version.Patch
}
func (helm *Helm) sync(m *sync.Mutex, f func()) {
+37 -30
View File
@@ -48,40 +48,28 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
return zap.New(core).Sugar()
}
func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner) Version {
// versionRegex matches versions like v1.1.1 and v1.1
var versionRegex = regexp.MustCompile("v(?P<major>\\d+)\\.(?P<minor>\\d+)(?:\\.(?P<patch>\\d+))?")
// Autodetect from `helm verison`
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil {
panic(err)
func parseHelmVersion(versionStr string) (Version, error) {
if len(versionStr) == 0 {
return Version{}, nil
}
if bytes == nil || len(bytes) == 0 {
return Version{}
matches := versionRegex.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return Version{}, fmt.Errorf("error parsing helm verion '%s'", versionStr)
}
re := regexp.MustCompile("v(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)")
matches := re.FindStringSubmatch(string(bytes))
result := make(map[string]string)
for i, name := range re.SubexpNames() {
for i, name := range versionRegex.SubexpNames() {
result[name] = matches[i]
}
major, err := strconv.Atoi(result["major"])
if err != nil {
panic(err)
}
minor, err := strconv.Atoi(result["minor"])
if err != nil {
panic(err)
}
patch, err := strconv.Atoi(result["patch"])
if err != nil {
panic(err)
}
// We ignore errors because regex matches only integers
// If any of the parts does not exist - default "0" will be used
major, _ := strconv.Atoi(result["major"])
minor, _ := strconv.Atoi(result["minor"])
patch, _ := strconv.Atoi(result["patch"])
// Support explicit helm3 opt-in via environment variable
if os.Getenv("HELMFILE_HELM3") != "" && major < 3 {
@@ -89,21 +77,37 @@ func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner)
Major: 3,
Minor: 0,
Patch: 0,
}
}, nil
}
return Version{
Major: major,
Minor: minor,
Patch: patch,
}, nil
}
func getHelmVersion(helmBinary string, runner Runner) (Version, error) {
// Autodetect from `helm verison`
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
if err != nil {
return Version{}, fmt.Errorf("error determining helm version: %w", err)
}
return parseHelmVersion(string(bytes))
}
// New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
// TODO: proper error handling
version, err := getHelmVersion(helmBinary, runner)
if err != nil {
panic(err)
}
return &execer{
helmBinary: helmBinary,
version: getHelmVersion(helmBinary, logger, runner),
version: version,
logger: logger,
kubeContext: kubeContext,
runner: runner,
@@ -126,6 +130,9 @@ func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, usernam
return fmt.Errorf("empty field name")
}
args = append(args, "repo", "add", name, repository)
if helm.IsHelm3() && helm.IsVersionAtLeast(3, 3, 2) {
args = append(args, "--force-update")
}
if certfile != "" && keyfile != "" {
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
}
@@ -394,6 +401,6 @@ func (helm *execer) GetVersion() Version {
return helm.version
}
func (helm *execer) IsVersionAtLeast(major int, minor int) bool {
return helm.version.Major >= major && helm.version.Minor >= minor
func (helm *execer) IsVersionAtLeast(major int, minor int, patch int) bool {
return helm.version.Major >= major && helm.version.Minor >= minor && helm.version.Patch >= patch
}
+50 -3
View File
@@ -557,16 +557,63 @@ func Test_GetVersion(t *testing.T) {
func Test_IsVersionAtLeast(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
if !helm.IsVersionAtLeast(2, 1) {
if !helm.IsVersionAtLeast(2, 1, 0) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
}
if helm.IsVersionAtLeast(2, 19) {
if helm.IsVersionAtLeast(2, 19, 0) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
}
if helm.IsVersionAtLeast(3, 2) {
if helm.IsVersionAtLeast(3, 2, 0) {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
}
}
func Test_parseHelmVersion(t *testing.T) {
tests := []struct {
ver string
want Version
wantErr bool
}{
{
ver: "v1.2.3",
want: Version{
Major: 1,
Minor: 2,
Patch: 3,
},
wantErr: false,
},
{
ver: "v1.2",
want: Version{
Major: 1,
Minor: 2,
Patch: 0,
},
wantErr: false,
},
{
ver: "v1",
wantErr: true,
},
{
ver: "1.1.1",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.ver, func(t *testing.T) {
got, err := parseHelmVersion(tt.ver)
if (err != nil) != tt.wantErr {
t.Errorf("parseHelmVersion() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseHelmVersion() got = %v, want %v", got, tt.want)
}
})
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ type Interface interface {
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
IsHelm3() bool
GetVersion() Version
IsVersionAtLeast(major int, minor int) bool
IsVersionAtLeast(major int, minor int, patch int) bool
}
type DependencyUpdater interface {
+13
View File
@@ -0,0 +1,13 @@
package helmexec
import "math/rand"
var executionIDComponents = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func newExecutionID() string {
b := make([]rune, 5)
for i := range b {
b[i] = executionIDComponents[rand.Intn(len(executionIDComponents))]
}
return string(b)
}
+27
View File
@@ -0,0 +1,27 @@
package helmexec
import (
"go.uber.org/zap"
"strings"
)
type logWriterGenerator struct {
log *zap.SugaredLogger
}
func (g logWriterGenerator) Writer(prefix string) *logWriter {
return &logWriter{
log: g.log,
prefix: prefix,
}
}
type logWriter struct {
log *zap.SugaredLogger
prefix string
}
func (w *logWriter) Write(p []byte) (int, error) {
w.log.Debugf("%s%s", w.prefix, strings.TrimSpace(string(p)))
return len(p), nil
}
+30 -6
View File
@@ -4,12 +4,14 @@ import (
"bytes"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"go.uber.org/zap"
)
const (
@@ -34,21 +36,34 @@ func (shell ShellRunner) Execute(cmd string, args []string, env map[string]strin
preparedCmd := exec.Command(cmd, args...)
preparedCmd.Dir = shell.Dir
preparedCmd.Env = mergeEnv(os.Environ(), env)
return Output(preparedCmd)
return Output(preparedCmd, &logWriterGenerator{
log: shell.Logger,
})
}
func Output(c *exec.Cmd) ([]byte, error) {
func Output(c *exec.Cmd, logWriterGenerators ...*logWriterGenerator) ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
var stdout bytes.Buffer
var stderr bytes.Buffer
var combined bytes.Buffer
c.Stdout = io.MultiWriter(&stdout, &combined)
c.Stderr = io.MultiWriter(&stderr, &combined)
var logWriters []io.Writer
id := newExecutionID()
for _, g := range logWriterGenerators {
logPrefix := fmt.Sprintf("%s:%s> ", filepath.Base(c.Path), id)
logWriters = append(logWriters, g.Writer(logPrefix))
}
c.Stdout = io.MultiWriter(append([]io.Writer{&stdout, &combined}, logWriters...)...)
c.Stderr = io.MultiWriter(append([]io.Writer{&stderr, &combined}, logWriters...)...)
err := c.Run()
if err != nil {
@@ -92,7 +107,16 @@ func env2map(env []string) map[string]string {
wanted := map[string]string{}
for _, cur := range env {
pair := strings.SplitN(cur, "=", 2)
wanted[pair[0]] = pair[1]
var v string
// An environment can completely miss `=` and the right side.
// If we didn't deal with that, this may fail due to an index-out-of-range error
if len(pair) > 1 {
v = pair[1]
}
wanted[pair[0]] = v
}
return wanted
}
+102 -3
View File
@@ -100,9 +100,15 @@ bar: {{ readFile "bar.txt" }}
}
valuesFile := "/example/path/to/values.yaml.gotmpl"
valuesContent := []byte(`env: {{ .Environment.Name }}`)
valuesContent := []byte(`env: {{ .Environment.Name }}
releaseName: {{ .Release.Name }}
releaseNamespace: {{ .Release.Namespace }}
`)
expectedValues := `env: production`
expectedValues := `env: production
releaseName: myrelease
releaseNamespace: mynamespace
`
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
@@ -124,7 +130,98 @@ bar: {{ readFile "bar.txt" }}
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
release := state.Releases[0]
state.ApplyOverrides(&release)
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actualValues := string(actualValuesData)
if !reflect.DeepEqual(expectedValues, actualValues) {
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
}
}
func TestReadFromYaml_OverrideNamespace(t *testing.T) {
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
production:
values:
- foo.yaml
- bar.yaml.gotmpl
# A.k.a helmfile apply --namespace myns
namespace: myns
releases:
- name: myrelease
namespace: mynamespace
chart: mychart
values:
- values.yaml.gotmpl
`)
fooYamlFile := "/example/path/to/foo.yaml"
fooYamlContent := []byte(`foo: foo
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
baz: "{{ readFile \"baz.txt\" }}"`)
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
barYamlContent := []byte(`foo: FOO
bar: {{ readFile "bar.txt" }}
`)
barTextFile := "/example/path/to/bar.txt"
barTextContent := []byte("BAR")
expected := map[string]interface{}{
"foo": "FOO",
"bar": "BAR",
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
"baz": "{{ readFile \"baz.txt\" }}",
}
valuesFile := "/example/path/to/values.yaml.gotmpl"
valuesContent := []byte(`env: {{ .Environment.Name }}
releaseName: {{ .Release.Name }}
releaseNamespace: {{ .Release.Namespace }}
overrideNamespace: {{ .Namespace }}
`)
expectedValues := `env: production
releaseName: myrelease
releaseNamespace: myns
overrideNamespace: myns
`
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
actual := state.Env.Values
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
}
release := state.Releases[0]
state.ApplyOverrides(&release)
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@@ -250,6 +347,8 @@ func TestReadFromYaml_FilterNegatives(t *testing.T) {
[]bool{false, true, false}},
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
[]bool{false, false, true}},
{LabelFilter{negativeLabels: [][]string{[]string{"foo", "bar"}}},
[]bool{false, true, true}},
}
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
if err != nil {
+8 -2
View File
@@ -114,7 +114,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
jsonPatches := release.JSONPatches
if len(jsonPatches) > 0 {
generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
if err != nil {
return nil, clean, err
}
@@ -130,7 +130,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
strategicMergePatches := release.StrategicMergePatches
if len(strategicMergePatches) > 0 {
generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
if err != nil {
return nil, clean, err
}
@@ -144,6 +144,12 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
shouldRun = true
}
if release.ForceNamespace != "" {
chartify.Opts.OverrideNamespace = release.ForceNamespace
shouldRun = true
}
if shouldRun {
generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex)
if err != nil {
+8 -5
View File
@@ -133,12 +133,15 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
}
}
for i, ts := range result.Secrets {
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
for i, t := range result.Secrets {
switch ts := t.(type) {
case string:
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
}
result.Secrets[i] = s.String()
}
result.Secrets[i] = s.String()
}
if len(result.SetValuesTemplate) > 0 {
+1 -1
View File
@@ -38,7 +38,7 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
return true
} else if rVal == v {
return false
}
+97
View File
@@ -0,0 +1,97 @@
package state
import (
"github.com/google/go-cmp/cmp"
"gopkg.in/yaml.v2"
"testing"
)
func TestSelectReleasesWithOverrides(t *testing.T) {
type testcase struct {
subject string
selector []string
want []string
}
testcases := []testcase{
{
subject: "multiple OR selectors (nillable label first)",
selector: []string{"type=bar", "name=nolabel2", "name=nolabel1"},
want: []string{"nolabel1", "nolabel2", "foo"},
},
{
subject: "multiple OR selectors (non-nillable label first)",
selector: []string{"name=foo", "type!=bar"},
want: []string{"nolabel1", "nolabel2", "foo"},
},
{
subject: "multiple AND conditions (nillable label first)",
selector: []string{"type!=bar,name!=nolabel2"},
want: []string{"nolabel1"},
},
{
subject: "multiple AND conditions (non-nillable label first)",
selector: []string{"name!=nolabel2,type!=bar"},
want: []string{"nolabel1"},
},
{
subject: "inequality on nillable label",
selector: []string{"type!=bar"},
want: []string{"nolabel1", "nolabel2"},
},
{
subject: "equality on nillable label",
selector: []string{"type=bar"},
want: []string{"foo"},
},
{
subject: "inequality on non-nillable label",
selector: []string{"name!=nolabel1"},
want: []string{"nolabel2", "foo"},
},
{
subject: "equality on non-nillable label",
selector: []string{"name=nolabel1"},
want: []string{"nolabel1"},
},
}
example := []byte(`releases:
- name: nolabel1
namespace: kube-system
chart: stable/nolabel
- name: nolabel2
namespace: default
chart: stable/nolabel
- name: foo
namespace: kube-system
chart: stable/foo
labels:
type: bar
`)
var state HelmState
if err := yaml.Unmarshal(example, &state); err != nil {
t.Fatal(err)
}
for _, tc := range testcases {
state.Selectors = tc.selector
rs, err := state.GetSelectedReleasesWithOverrides()
if err != nil {
t.Fatalf("%s %s: %v", tc.selector, tc.subject, err)
}
var got []string
for _, r := range rs {
got = append(got, r.Name)
}
if d := cmp.Diff(tc.want, got); d != "" {
t.Errorf("%s %s: %s", tc.selector, tc.subject, d)
}
}
}
+486 -59
View File
@@ -18,6 +18,9 @@ import (
"sync"
"text/template"
"github.com/imdario/mergo"
"golang.org/x/sync/errgroup"
"github.com/variantdev/chartify"
"github.com/roboll/helmfile/pkg/environment"
@@ -39,11 +42,7 @@ const (
EmptyTimeout = -1
)
// HelmState structure for the helmfile
type HelmState struct {
basePath string
FilePath string
type ReleaseSetSpec struct {
DefaultHelmBinary string `yaml:"helmBinary,omitempty"`
// DefaultValues is the default values to be overrode by environment values and command-line overrides
@@ -58,6 +57,7 @@ type HelmState struct {
DeprecatedReleases []ReleaseSpec `yaml:"charts,omitempty"`
OverrideNamespace string `yaml:"namespace,omitempty"`
Repositories []RepositorySpec `yaml:"repositories,omitempty"`
CommonLabels map[string]string `yaml:"commonLabels,omitempty"`
Releases []ReleaseSpec `yaml:"releases,omitempty"`
Selectors []string `yaml:"-"`
ApiVersions []string `yaml:"apiVersions,omitempty"`
@@ -69,6 +69,19 @@ type HelmState struct {
Env environment.Environment `yaml:"-"`
// If set to "Error", return an error when a subhelmfile points to a
// non-existent path. The default behavior is to print a warning. Note the
// differing default compared to other MissingFileHandlers.
MissingFileHandler string `yaml:"missingFileHandler"`
}
// HelmState structure for the helmfile
type HelmState struct {
basePath string
FilePath string
ReleaseSetSpec `yaml:",inline"`
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
@@ -80,11 +93,6 @@ type HelmState struct {
runner helmexec.Runner
valsRuntime vals.Evaluator
// If set to "Error", return an error when a subhelmfile points to a
// non-existent path. The default behavior is to print a warning. Note the
// differing default compared to other MissingFileHandlers.
MissingFileHandler string `yaml:"missingFileHandler"`
}
// SubHelmfileSpec defines the subhelmfile path and options
@@ -209,7 +217,7 @@ type ReleaseSpec struct {
Namespace string `yaml:"namespace,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
Values []interface{} `yaml:"values,omitempty"`
Secrets []string `yaml:"secrets,omitempty"`
Secrets []interface{} `yaml:"secrets,omitempty"`
SetValues []SetValue `yaml:"set,omitempty"`
ValuesTemplate []interface{} `yaml:"valuesTemplate,omitempty"`
@@ -251,6 +259,13 @@ type ReleaseSpec struct {
// Without this, any error in url parsing result in silently falling-back to normal process of treating `chart:` as the regular
// helm chart name.
ForceGoGetter bool `yaml:"forceGoGetter,omitempty"`
// ForceNamespace is an experimental feature to set metadata.namespace in every K8s resource rendered by the chart,
// regardless of the template, even when it doesn't have `namespace: {{ .Namespace | quote }}`.
// This is only needed when you can't FIX your chart to have `namespace: {{ .Namespace }}` AND you're using `helmfile template`.
// In standard use-cases, `Namespace` should be sufficient.
// Use this only when you know what you want to do!
ForceNamespace string `yaml:"forceNamespace"`
}
type Release struct {
@@ -330,15 +345,10 @@ func (st *HelmState) getRepositoriesToSync() (map[string]bool, error) {
}
func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]string, error) {
shouldUpdate, err := st.getRepositoriesToSync()
if err != nil {
return nil, err
}
var updated []string
for _, repo := range st.Repositories {
if !shouldUpdate[repo.Name] || shouldSkip[repo.Name] {
if shouldSkip[repo.Name] {
continue
}
@@ -524,6 +534,11 @@ func (o *SyncOpts) Apply(opts *SyncOpts) {
func ReleaseToID(r *ReleaseSpec) string {
var id string
kc := r.KubeContext
if kc != "" {
id += kc + "/"
}
tns := r.TillerNamespace
if tns != "" {
id += tns + "/"
@@ -797,6 +812,21 @@ func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
return result
}
type ChartPrepareOptions struct {
ForceDownload bool
SkipRepos bool
SkipResolve bool
}
type chartPrepareResult struct {
releaseName string
chartName string
chartPath string
err error
buildDeps bool
chartFetchedByGoGetter bool
}
// PrepareCharts creates temporary directories of charts.
//
// Each resulting "chart" can be one of the followings:
@@ -811,20 +841,15 @@ func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
//
// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, forceDownload, skipDeps bool) (map[string]string, []error) {
func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[string]string, []error) {
releases := releasesNeedCharts(st.Releases)
temp := make(map[string]string, len(releases))
type downloadResults struct {
releaseName string
chartPath string
err error
}
errs := []error{}
jobQueue := make(chan *ReleaseSpec, len(releases))
results := make(chan *downloadResults, len(releases))
results := make(chan *chartPrepareResult, len(releases))
var helm3 bool
@@ -832,11 +857,15 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
helm3 = helm.IsHelm3()
}
updated, err := st.ResolveDeps()
if err != nil {
return nil, []error{err}
if !opts.SkipResolve {
updated, err := st.ResolveDeps()
if err != nil {
return nil, []error{err}
}
*st = *updated
}
*st = *updated
var builds []*chartPrepareResult
st.scatterGather(
concurrency,
@@ -855,7 +884,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
// If it wasn't called here, Helmfile can end up an issue like
// https://github.com/roboll/helmfile/issues/1328
if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil {
results <- &downloadResults{err: err}
results <- &chartPrepareResult{err: err}
return
}
@@ -863,17 +892,21 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
chartPath, err := st.goGetterChart(chartName, release.Directory, release.ForceGoGetter)
if err != nil {
results <- &downloadResults{err: fmt.Errorf("release %q: %w", release.Name, err)}
results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
return
}
chartFetchedByGoGetter := chartPath != chartName
chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex)
defer clean()
if err != nil {
results <- &downloadResults{err: err}
results <- &chartPrepareResult{err: err}
return
}
var buildDeps bool
if chartification != nil {
c := chartify.New(
chartify.HelmBin(st.DefaultHelmBinary),
@@ -882,28 +915,51 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartification.Opts))
if err != nil {
results <- &downloadResults{err: err}
results <- &chartPrepareResult{err: err}
return
} else {
// TODO Chartify
chartPath = out
}
} else if !forceDownload {
} else if pathExists(normalizeChart(st.basePath, chartPath)) {
// At this point, we are sure that chartPath is a local directory containing either:
// - A remote chart fetched by go-getter or
// - A local chart
//
// The chart may have Chart.yaml(and requirements.yaml for Helm 2), and optionally Chart.lock/requirements.lock,
// but no `charts/` directory populated at all, or a subet of chart dependencies are missing in the directory.
//
// In such situation, Helm fails with an error like:
// Error: found in Chart.yaml, but missing in charts/ directory: cert-manager, prometheus, postgresql, gitlab-runner, grafana, redis
//
// (See also https://github.com/roboll/helmfile/issues/1401#issuecomment-670854495)
//
// To avoid it, we need to call a `helm dep build` command on the chart.
// But the command may consistently fail when an outdated Chart.lock exists.
//
// (I've mentioned about such case in https://github.com/roboll/helmfile/pull/1400.)
//
// Trying to run `helm dep build` on the chart regardless of if it's from local or remote is
// problematic, as usually the user would have no way to fix the remote chart on their own.
//
// Given that, we always run `helm dep build` on the chart here, but tolerate any error caused by it
// for a remote chart, so that the user can notice/fix the issue in a local chart while
// a broken remote chart won't completely block their job.
chartPath = normalizeChart(st.basePath, chartPath)
buildDeps = !opts.SkipRepos
} else if !opts.ForceDownload {
// At this point, we are sure that either:
// 1. It is a local chart and we can use it in later process (helm upgrade/template/lint/etc)
// without any modification, or
// 2. It is a remote chart which can be safely handed over to helm,
// because the version of Helm used in this transaction support downloading the chart instead,
// and we don't need any modification to the chart
} else if pathExists(normalizeChart(st.basePath, chartPath)) {
chartPath = normalizeChart(st.basePath, chartPath)
if !skipDeps {
if err := helm.BuildDeps(release.Name, chartPath); err != nil {
results <- &downloadResults{err: err}
return
}
}
// because the version of Helm used in this transaction (helm v3 or greater) support downloading
// the chart instead, AND we don't need any modification to the chart
//
// Also see HelmState.chartVersionFlags(). For `helmfile template`, it's called before `helm template`
// only on helm v3.
// For helm 2, we `helm fetch` with the version flags and call `helm template`
// WITHOUT the version flags.
} else {
pathElems := []string{
dir,
@@ -917,6 +973,10 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
pathElems = append(pathElems, release.Namespace)
}
if release.KubeContext != "" {
pathElems = append(pathElems, release.KubeContext)
}
chartVersion := "latest"
if release.Version != "" {
chartVersion = release.Version
@@ -931,7 +991,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
fetchFlags := st.chartVersionFlags(release)
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(chartName, fetchFlags...); err != nil {
results <- &downloadResults{err: err}
results <- &chartPrepareResult{err: err}
return
}
}
@@ -943,7 +1003,13 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
}
}
results <- &downloadResults{releaseName: release.Name, chartPath: chartPath}
results <- &chartPrepareResult{
releaseName: release.Name,
chartName: chartName,
chartPath: chartPath,
buildDeps: buildDeps,
chartFetchedByGoGetter: chartFetchedByGoGetter,
}
}
},
func() {
@@ -956,6 +1022,10 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
return
}
temp[downloadRes.releaseName] = downloadRes.chartPath
if downloadRes.buildDeps {
builds = append(builds, downloadRes)
}
}
},
)
@@ -963,9 +1033,104 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
if len(errs) > 0 {
return nil, errs
}
if len(builds) > 0 {
if err := st.runHelmDepBuilds(helm, concurrency, builds); err != nil {
return nil, []error{err}
}
}
return temp, nil
}
func (st *HelmState) runHelmDepBuilds(helm helmexec.Interface, concurrency int, builds []*chartPrepareResult) error {
type buildOutput struct {
diagnostic string
}
buildQueue := make(chan *chartPrepareResult)
buildOutputQueue := make(chan *buildOutput)
buildWorkers := &errgroup.Group{}
for i := 0; i < concurrency; i++ {
buildWorkers.Go(func() error {
for r := range buildQueue {
out, err := func() (*buildOutput, error) {
if err := helm.BuildDeps(r.releaseName, r.chartPath); err != nil {
if r.chartFetchedByGoGetter {
return &buildOutput{
diagnostic: fmt.Sprintf(
"WARN: `helm dep build` failed. While processing release %q, Helmfile observed that remote chart %q fetched by go-getter is seemingly broken. "+
"One of well-known causes of this is that the chart has outdated Chart.lock, which needs the chart maintainer needs to run `helm dep up`. "+
"Helmfile is tolerating the error to avoid blocking you until the remote chart gets fixed. "+
"But this may result in any failure later if the chart is broken badly. FYI, the tolerated error was: %v",
r.releaseName,
r.chartName,
err.Error(),
),
}, nil
}
return nil, err
}
return nil, nil
}()
if err != nil {
return err
}
if out != nil {
buildOutputQueue <- out
}
}
return nil
})
}
diagnosticsAggregator := &sync.WaitGroup{}
diagnosticsAggregator.Add(1)
go func() {
var diagnostics []string
for r := range buildOutputQueue {
if d := r.diagnostic; d != "" {
diagnostics = append(diagnostics, d)
}
}
sort.Strings(diagnostics)
for _, d := range diagnostics {
st.logger.Warn(d)
}
diagnosticsAggregator.Done()
}()
built := map[string]bool{}
for _, b := range builds {
// `helm dep build` fails when it was run concurrency on the same chart.
// To avoid that, we run `helm dep build` only once per each local chart.
//
// See https://github.com/roboll/helmfile/issues/1438
if !built[b.chartPath] {
built[b.chartPath] = true
buildQueue <- b
}
}
close(buildQueue)
if err := buildWorkers.Wait(); err != nil {
return err
}
close(buildOutputQueue)
diagnosticsAggregator.Wait()
return nil
}
type TemplateOpts struct {
Set []string
OutputDirTemplate string
@@ -1066,6 +1231,103 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
return nil
}
type WriteValuesOpts struct {
Set []string
OutputFileTemplate string
}
type WriteValuesOpt interface{ Apply(*WriteValuesOpts) }
func (o *WriteValuesOpts) Apply(opts *WriteValuesOpts) {
*opts = *o
}
// WriteReleasesValues writes values files for releases
func (st *HelmState) WriteReleasesValues(helm helmexec.Interface, additionalValues []string, opt ...WriteValuesOpt) []error {
opts := &WriteValuesOpts{}
for _, o := range opt {
o.Apply(opts)
}
for i := range st.Releases {
release := &st.Releases[i]
if !release.Desired() {
continue
}
st.ApplyOverrides(release)
generatedFiles, err := st.generateValuesFiles(helm, release, i)
if err != nil {
return []error{err}
}
defer func() {
st.removeFiles(generatedFiles)
}()
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
return []error{err}
}
if _, err := os.Stat(valfile); os.IsNotExist(err) {
return []error{err}
}
generatedFiles = append(generatedFiles, valfile)
}
outputValuesFile, err := st.GenerateOutputFilePath(release, opts.OutputFileTemplate)
if err != nil {
return []error{err}
}
if err := os.MkdirAll(filepath.Dir(outputValuesFile), 0755); err != nil {
return []error{err}
}
st.logger.Infof("Writing values file %s", outputValuesFile)
merged := map[string]interface{}{}
for _, f := range generatedFiles {
src := map[string]interface{}{}
srcBytes, err := st.readFile(f)
if err != nil {
return []error{fmt.Errorf("reading %s: %w", f, err)}
}
if err := yaml.Unmarshal(srcBytes, &src); err != nil {
return []error{fmt.Errorf("unmarshalling yaml %s: %w", f, err)}
}
if err := mergo.Merge(&merged, &src, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
return []error{fmt.Errorf("merging %s: %w", f, err)}
}
}
var buf bytes.Buffer
y := yaml.NewEncoder(&buf)
if err := y.Encode(merged); err != nil {
return []error{err}
}
if err := ioutil.WriteFile(outputValuesFile, buf.Bytes(), 0644); err != nil {
return []error{fmt.Errorf("writing values file %s: %w", outputValuesFile, err)}
}
if _, err := st.TriggerCleanupEvent(release, "write-values"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
}
return nil
}
type LintOpts struct {
Set []string
}
@@ -1487,14 +1749,14 @@ func (st *HelmState) SelectReleasesWithOverrides() ([]Release, error) {
if err != nil {
return nil, err
}
rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, values)
rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, st.CommonLabels, values)
if err != nil {
return nil, err
}
return rs, nil
}
func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map[string]interface{}) ([]Release, error) {
func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]interface{}) ([]Release, error) {
var filteredReleases []Release
filters := []ReleaseFilter{}
for _, label := range selectors {
@@ -1514,6 +1776,10 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map
// 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]
//Merge CommonLabels into release labels
for k, v := range commonLabels {
r.Labels[k] = v
}
var filterMatch bool
for _, f := range filters {
if r.Labels == nil {
@@ -1619,7 +1885,12 @@ func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpe
Logger: st.logger,
ReadFile: st.readFile,
}
vals, err := st.Values()
if err != nil {
return false, err
}
data := map[string]interface{}{
"Values": vals,
"Release": r,
"HelmfileCommand": helmfileCmd,
}
@@ -1798,7 +2069,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
if release.CreateNamespace != nil && *release.CreateNamespace ||
release.CreateNamespace == nil && (st.HelmDefaults.CreateNamespace == nil || *st.HelmDefaults.CreateNamespace) {
if helm.IsVersionAtLeast(3, 2) {
if helm.IsVersionAtLeast(3, 2, 0) {
flags = append(flags, "--create-namespace")
} else if release.CreateNamespace != nil || st.HelmDefaults.CreateNamespace != nil {
// createNamespace was set explicitly, but not running supported version of helm - error
@@ -1827,7 +2098,16 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
}
func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
flags := st.chartVersionFlags(release)
var flags []string
// `helm template` in helm v2 does not support `--version` flag. So we fetch with the version flag and then template
// without the flag. See PrepareCharts function to see the Helmfile implementation of chart fetching.
//
// `helm template` in helm v3 supports `--version` and it automatically fetches the remote chart to template,
// so we skip fetching on helmfile-side and let helm fetch it.
if helm.IsHelm3() {
flags = st.chartVersionFlags(release)
}
var err error
flags, err = st.appendHelmXFlags(flags, release)
@@ -1928,8 +2208,14 @@ func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec,
return flags, files, nil
}
func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.valuesFileTemplateData())
func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) {
vals, err := st.Values()
if err != nil {
return nil, err
}
templateData := st.createReleaseTemplateData(release, vals)
r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), templateData)
rawBytes, err := r.RenderToBytes(path)
if err != nil {
return nil, err
@@ -2008,7 +2294,7 @@ func (st *HelmState) removeFiles(files []string) {
}
}
func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingFileHandler *string) ([]string, error) {
func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []interface{}, missingFileHandler *string) ([]string, error) {
generatedFiles := []string{}
for _, value := range values {
@@ -2027,7 +2313,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
}
path := paths[0]
yamlBytes, err := st.RenderValuesFileToBytes(path)
yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path)
if err != nil {
return generatedFiles, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
}
@@ -2084,7 +2370,7 @@ func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string,
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)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered, release.MissingFileHandler)
if err != nil {
return nil, err
}
@@ -2095,11 +2381,41 @@ func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string,
func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
var generatedFiles []string
for _, value := range release.Secrets {
paths, skip, err := st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value)
if err != nil {
return nil, err
for _, v := range release.Secrets {
var (
paths []string
skip bool
err error
)
switch value := v.(type) {
case string:
paths, skip, err = st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value)
if err != nil {
return nil, err
}
default:
bs, err := yaml.Marshal(value)
if err != nil {
return nil, err
}
path, err := ioutil.TempFile(os.TempDir(), "helmfile-embdedded-secrets-*.yaml.enc")
if err != nil {
return nil, err
}
_ = path.Close()
defer func() {
_ = os.Remove(path.Name())
}()
if err := ioutil.WriteFile(path.Name(), bs, 0644); err != nil {
return nil, err
}
paths = []string{path.Name()}
}
if skip {
continue
}
@@ -2372,6 +2688,68 @@ func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, o
return buf.String(), nil
}
func (st *HelmState) GenerateOutputFilePath(release *ReleaseSpec, outputFileTemplate string) (string, error) {
// get absolute path of state file to generate a hash
// use this hash to write helm output in a specific directory by state file and release name
// ie. in a directory named stateFileName-stateFileHash-releaseName
stateAbsPath, err := filepath.Abs(st.FilePath)
if err != nil {
return stateAbsPath, err
}
hasher := sha1.New()
io.WriteString(hasher, stateAbsPath)
var stateFileExtension = filepath.Ext(st.FilePath)
var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)]
sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8]
var sb strings.Builder
sb.WriteString(stateFileName)
sb.WriteString("-")
sb.WriteString(sha1sum)
sb.WriteString("-")
sb.WriteString(release.Name)
if outputFileTemplate == "" {
outputFileTemplate = filepath.Join("{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}", "{{ .Release.Name}}.yaml")
}
t, err := template.New("output-file").Parse(outputFileTemplate)
if err != nil {
return "", fmt.Errorf("parsing output-file templmate")
}
buf := &bytes.Buffer{}
type state struct {
BaseName string
Path string
AbsPath string
AbsPathSHA1 string
}
data := struct {
State state
Release *ReleaseSpec
}{
State: state{
BaseName: stateFileName,
Path: st.FilePath,
AbsPath: stateAbsPath,
AbsPathSHA1: sha1sum,
},
Release: release,
}
if err := t.Execute(buf, data); err != nil {
return "", fmt.Errorf("executing output-file template: %w", err)
}
return buf.String(), nil
}
func (st *HelmState) ToYaml() (string, error) {
if result, err := yaml.Marshal(st); err != nil {
return "", err
@@ -2379,3 +2757,52 @@ func (st *HelmState) ToYaml() (string, error) {
return string(result), nil
}
}
func (st *HelmState) LoadYAMLForEmbedding(release *ReleaseSpec, entries []interface{}, missingFileHandler *string, pathPrefix string) ([]interface{}, error) {
var result []interface{}
for _, v := range entries {
switch t := v.(type) {
case string:
var values map[string]interface{}
paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", pathPrefix+t)
if err != nil {
return nil, err
}
if skip {
continue
}
if len(paths) > 1 {
return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary")
}
yamlOrTemplatePath := paths[0]
yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, yamlOrTemplatePath)
if err != nil {
return nil, fmt.Errorf("failed to render values files \"%s\": %v", t, err)
}
if err := yaml.Unmarshal(yamlBytes, &values); err != nil {
return nil, err
}
result = append(result, values)
default:
result = append(result, v)
}
}
return result, nil
}
func (st *HelmState) Reverse() {
for i, j := 0, len(st.Releases)-1; i < j; i, j = i+1, j-1 {
st.Releases[i], st.Releases[j] = st.Releases[j], st.Releases[i]
}
for i, j := 0, len(st.Helmfiles)-1; i < j; i, j = i+1, j-1 {
st.Helmfiles[i], st.Helmfiles[j] = st.Helmfiles[j], st.Helmfiles[i]
}
}
+16 -16
View File
@@ -12,19 +12,17 @@ func (st *HelmState) Values() (map[string]interface{}, error) {
return st.Env.GetMergedValues()
}
func (st *HelmState) mustLoadVals() map[string]interface{} {
vals, err := st.Values()
if err != nil {
panic(err)
}
return vals
}
func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]interface{}) releaseTemplateData {
return releaseTemplateData{
Environment: st.Env,
Namespace: st.OverrideNamespace,
Values: st.mustLoadVals(),
Values: vals,
Release: releaseTemplateDataRelease{
Name: release.Name,
Chart: release.Chart,
Namespace: release.Namespace,
Labels: release.Labels,
},
}
}
@@ -86,13 +84,15 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
}
for i, rt := range st.Releases {
if rt.Labels == nil {
rt.Labels = map[string]string{}
}
for k, v := range st.CommonLabels {
rt.Labels[k] = v
}
successFlag := false
for it, prev := 0, &rt; it < 6; it++ {
tmplData := releaseTemplateData{
Environment: st.Env,
Release: *prev,
Values: vals,
}
tmplData := st.createReleaseTemplateData(prev, vals)
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
r, err := rt.ExecuteTemplateExpressions(renderer)
if err != nil {
+22 -18
View File
@@ -36,7 +36,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
Name: "test-app",
Namespace: "test-namespace-{{ .Release.Name }}",
ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
Secrets: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
Labels: map[string]string{"id": "{{ .Release.Name }}"},
},
want: ReleaseSpec{
@@ -45,7 +45,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
Name: "test-app",
Namespace: "test-namespace-test-app",
Values: []interface{}{"config/test_env/test-app/values.yaml"},
Secrets: []string{"config/test_env/test-app/secrets.yaml"},
Secrets: []interface{}{"config/test_env/test-app/secrets.yaml"},
Labels: map[string]string{"id": "test-app"},
},
},
@@ -135,14 +135,16 @@ func TestHelmState_executeTemplates(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
},
}
@@ -235,14 +237,16 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
OverrideNamespace: "test-namespace_",
Repositories: nil,
Releases: []ReleaseSpec{
tt.input,
},
},
}
+154 -84
View File
@@ -127,12 +127,14 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: tt.fields.BaseChartPath,
DeprecatedContext: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
OverrideNamespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
basePath: tt.fields.BaseChartPath,
ReleaseSetSpec: ReleaseSetSpec{
DeprecatedContext: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
OverrideNamespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
},
}
if state.ApplyOverrides(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
t.Errorf("HelmState.ApplyOverrides() = %v, want %v", tt.args.spec, tt.want)
@@ -695,11 +697,13 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: "./",
DeprecatedContext: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
valsRuntime: valsRuntime,
basePath: "./",
ReleaseSetSpec: ReleaseSetSpec{
DeprecatedContext: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
},
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Version: tt.version,
@@ -771,11 +775,11 @@ func Test_isLocalChart(t *testing.T) {
want: true,
},
{
name: "local chart in 3-level deep dir",
name: "remote chart in 3-level deep dir (e.g. ChartCenter)",
args: args{
chart: "foo/bar/baz",
chart: "center/bar/baz",
},
want: true,
want: false,
},
}
for i := range tests {
@@ -814,6 +818,14 @@ func Test_normalizeChart(t *testing.T) {
},
want: "remote/app",
},
{
name: "chartcenter repo path",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "center/stable/myapp",
},
want: "center/stable/myapp",
},
{
name: "construct local chart path, parent dir",
args: args{
@@ -921,7 +933,9 @@ func TestHelmState_SyncRepos(t *testing.T) {
}
}
state := &HelmState{
Repositories: tt.repos,
ReleaseSetSpec: ReleaseSetSpec{
Repositories: tt.repos,
},
}
if _, _ = state.SyncRepos(tt.helm, map[string]bool{}); !reflect.DeepEqual(tt.helm.Repo, tt.want) {
t.Errorf("HelmState.SyncRepos() for [%s] = %v, want %v", tt.name, tt.helm.Repo, tt.want)
@@ -1032,7 +1046,9 @@ func TestHelmState_SyncReleases(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
}
@@ -1135,8 +1151,10 @@ func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
Releases: []ReleaseSpec{tt.release},
basePath: ".",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{tt.release},
},
logger: logger,
valsRuntime: valsRuntime,
}
@@ -1281,7 +1299,9 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
}
@@ -1383,7 +1403,9 @@ func TestGetDeployedVersion(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: []ReleaseSpec{tt.release},
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{tt.release},
},
logger: logger,
valsRuntime: valsRuntime,
}
@@ -1510,7 +1532,9 @@ func TestHelmState_DiffReleases(t *testing.T) {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
}
@@ -1582,7 +1606,9 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
removeFile: func(f string) error {
@@ -1666,7 +1692,9 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
removeFile: func(f string) error {
@@ -1728,35 +1756,37 @@ generated: 2019-05-16T15:42:45.50486+09:00
state := &HelmState{
basePath: "/src",
FilePath: "/src/helmfile.yaml",
Releases: []ReleaseSpec{
{
Chart: "./..",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "./..",
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
Version: "1.5.0",
},
{
Chart: "stable/envoy",
Version: "1.4.0",
},
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
Version: "1.5.0",
},
{
Chart: "stable/envoy",
Version: "1.4.0",
},
},
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
},
},
},
tempDir: tempDir,
@@ -1790,30 +1820,32 @@ func TestHelmState_ResolveDeps_NoLockFile(t *testing.T) {
state := &HelmState{
basePath: "/src",
FilePath: "/src/helmfile.yaml",
Releases: []ReleaseSpec{
{
Chart: "./..",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "./..",
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
},
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
},
},
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
},
},
},
logger: logger,
@@ -1906,8 +1938,10 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
fileExists: func(f string) (bool, error) {
if f != "foo.yaml" {
return false, fmt.Errorf("unexpected file: %s", f)
@@ -1991,8 +2025,10 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
}
errs := state.TestReleases(tt.helm, tt.cleanup, 1, 1)
if (errs != nil) != tt.wantErr {
@@ -2042,8 +2078,10 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: releases,
logger: logger,
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
},
logger: logger,
}
state.Selectors = []string{tt.labels}
errs := state.FilterReleases()
@@ -2208,11 +2246,13 @@ func TestHelmState_Delete(t *testing.T) {
release,
}
state := &HelmState{
HelmDefaults: HelmSpec{
KubeContext: tt.defKubeContext,
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: tt.defKubeContext,
},
Releases: releases,
},
Releases: releases,
logger: logger,
logger: logger,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
@@ -2235,3 +2275,33 @@ func TestHelmState_Delete(t *testing.T) {
t.Run(tt.name, f)
}
}
func TestReverse(t *testing.T) {
num := 8
st := &HelmState{}
for i := 0; i < num; i++ {
name := fmt.Sprintf("%d", i)
st.Helmfiles = append(st.Helmfiles, SubHelmfileSpec{
Path: name,
})
st.Releases = append(st.Releases, ReleaseSpec{
Name: name,
})
}
st.Reverse()
for i := 0; i < num; i++ {
j := num - 1 - i
want := fmt.Sprintf("%d", j)
if got := st.Helmfiles[i].Path; got != want {
t.Errorf("sub-helmfile at %d has incorrect path: want %q, got %q", i, want, got)
}
if got := st.Releases[i].Name; got != want {
t.Errorf("release at %d has incorrect name: want %q, got %q", i, want, got)
}
}
}
+23 -3
View File
@@ -19,12 +19,32 @@ type EnvironmentTemplateData struct {
Values map[string]interface{}
}
// releaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
// releaseTemplateData provides variables accessible while executing golang text/template expressions in release templates
// and release values templates within a Helmfile YAML file.
type releaseTemplateData struct {
// Environment is accessible as `.Environment` from any template expression executed by the renderer
Environment environment.Environment
// Release is accessible as `.Release` from any template expression executed by the renderer
Release ReleaseSpec
// Release is accessible as `.Release` from any template expression executed by the renderer.
// It contains a subset of ReleaseSpec that is known to be useful to dynamically render values.
Release releaseTemplateDataRelease
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
Values map[string]interface{}
// Namespace is HelmState.OverrideNamespace.
// You should better use Release.Namespace as it might work as you'd expect even if OverrideNamespace is not set.
// See releaseTemplateDataRelease.Namespace for more information.
Namespace string
}
type releaseTemplateDataRelease struct {
// Name is basically ReleaseSpec.Name exposed to the template
Name string
// Namespace is HelmState.OverrideNamespace, or if it's empty, ReleaseSpec.Namespace.
Namespace string
// Labels is ReleaseSpec.Labels
Labels map[string]string
// Chart is ReleaseSpec.Chart
Chart string
}
+9 -3
View File
@@ -21,7 +21,8 @@ func isLocalChart(chart string) bool {
return chart == "" ||
chart[0] == '/' ||
strings.Index(chart, "/") == -1 ||
len(strings.Split(chart, "/")) != 2
(len(strings.Split(chart, "/")) != 2 &&
len(strings.Split(chart, "/")) != 3)
}
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
@@ -29,8 +30,13 @@ func resolveRemoteChart(repoAndChart string) (string, string, bool) {
return "", "", false
}
parts := strings.Split(repoAndChart, "/")
if len(parts) != 2 {
uriLike := strings.Index(repoAndChart, "://") > -1
if uriLike {
return "", "", false
}
parts := strings.SplitN(repoAndChart, "/", 2)
if len(parts) < 2 {
return "", "", false
}
+10 -4
View File
@@ -15,13 +15,17 @@ func TestIsLocalChart(t *testing.T) {
input: "stable/mysql",
expected: false,
},
{
input: "center/stable/mysql",
expected: false,
},
{
input: "./charts/myapp",
expected: true,
},
{
input: "charts/mysubsystem/myapp",
expected: true,
input: "center/stable/myapp",
expected: false,
},
{
input: "./charts/mysubsystem/myapp",
@@ -77,8 +81,10 @@ func TestResolveRemortChart(t *testing.T) {
remote: false,
},
{
input: "charts/mysubsystem/myapp",
remote: false,
input: "center/stable/myapp",
repo: "center",
chart: "stable/myapp",
remote: true,
},
{
input: "./charts/mysubsystem/myapp",
+17 -1
View File
@@ -2,15 +2,31 @@ package tmpl
import (
"bytes"
"github.com/Masterminds/sprig"
"github.com/Masterminds/sprig/v3"
"text/template"
)
func (c *Context) newTemplate() *template.Template {
aliased := template.FuncMap{}
aliases := map[string]string{
"get": "sprigGet",
}
funcMap := sprig.TxtFuncMap()
for orig, alias := range aliases {
aliased[alias] = funcMap[orig]
}
for name, f := range c.createFuncMap() {
funcMap[name] = f
}
for name, f := range aliased {
funcMap[name] = f
}
tmpl := template.New("stringTemplate").Funcs(funcMap)
if c.preRender {
tmpl = tmpl.Option("missingkey=zero")
+23
View File
@@ -0,0 +1,23 @@
package tmpl
import (
"testing"
)
func TestMergeOverwrite(t *testing.T) {
ctx := &Context{}
buf, err := ctx.RenderTemplateToBuffer(`
{{- $v1 := dict "bool" true "int" 2 "str" "v1" "str2" "v1" -}}
{{- $v2 := dict "bool" false "int" 0 "str" "v2" "str2" "" -}}
{{- $mo1 := mergeOverwrite (dict) $v1 $v2 }}
{{- $mo1 -}}
`)
expected := "map[bool:false int:0 str:v2 str2:]"
if err != nil {
t.Errorf("unexpected error: %v", err)
}
actual := buf.String()
if actual != expected {
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
}
}
+11 -3
View File
@@ -6,8 +6,8 @@ environments:
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com/
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
- name: center
url: https://repo.chartcenter.io
helmDefaults:
kubeContext: minikube
@@ -19,9 +19,17 @@ releases:
set:
- name: ingress.enabled
value: false
hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args:
- "{{`{{.Environment.Name}}`}}"
- "{{`{{.Release.Name}}`}}"
- "{{`{{.HelmfileCommand}}`}}"
- name: raw
chart: incubator/raw
chart: center/incubator/raw
values:
- mysecret: {{ .Environment.Values.mysecret }}
- values.yaml