mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 16:37:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
216c228c0b | ||
|
|
ef63a05513 | ||
|
|
06b0c99a0b | ||
|
|
fd0133e10a | ||
|
|
9d851cda3b | ||
|
|
f79db2ec8d | ||
|
|
94a6fcfb9f | ||
|
|
fb2041555e | ||
|
|
cbf5b8b1e7 | ||
|
|
267e0fa1fe | ||
|
|
5488198d6d | ||
|
|
4bbd09ccb2 | ||
|
|
4e4f1bee59 | ||
|
|
6dfc9be0d1 | ||
|
|
1823bb1e5b | ||
|
|
4bc99337b2 | ||
|
|
01ae59fedd | ||
|
|
2a6bd24e3c | ||
|
|
11d0abba6e | ||
|
|
dd58badf81 | ||
|
|
e24c15324b | ||
|
|
ba0891b7b7 | ||
|
|
b58e082a8f | ||
|
|
bf9dcc0982 | ||
|
|
cd5d906afb | ||
|
|
a584aeab2e | ||
|
|
765bfe6cfd | ||
|
|
622cba9f19 | ||
|
|
63a337ee57 | ||
|
|
6baad71b1f | ||
|
|
bce2f4728b | ||
|
|
e5038fb04f | ||
|
|
fe5102e777 | ||
|
|
eda961edc2 | ||
|
|
b2a6231dcf | ||
|
|
4166b418c9 | ||
|
|
408bf7b8bf | ||
|
|
2f9f52033c | ||
|
|
63b5040ec4 | ||
|
|
4cc40cf0f1 | ||
|
|
1df9b0e9e9 | ||
|
|
b82f77ee50 | ||
|
|
e6bb8282db | ||
|
|
b6a04d012c | ||
|
|
9eef44ffe6 | ||
|
|
cfe309ec21 | ||
|
|
65818cd345 | ||
|
|
ddb5be1b9d | ||
|
|
f61334d9bb | ||
|
|
3bf0f7d54e | ||
|
|
7dec948950 | ||
|
|
e0d0a1cf7f | ||
|
|
31768b5bac | ||
|
|
1bde201eae | ||
|
|
1831d3375f | ||
|
|
2e38f42a40 | ||
|
|
1da3488599 | ||
|
|
78b03e0d92 | ||
|
|
11eda66eaa | ||
|
|
65d404b276 | ||
|
|
72425aebfc |
+3
-1
@@ -1,3 +1,5 @@
|
||||
dist/
|
||||
.idea/
|
||||
helmfile
|
||||
helmfile
|
||||
helmfile.lock
|
||||
vendor/
|
||||
|
||||
+13
-2
@@ -9,7 +9,7 @@ RUN make static-linux
|
||||
|
||||
FROM alpine:3.8
|
||||
|
||||
RUN apk add --no-cache ca-certificates git bash curl
|
||||
RUN apk add --no-cache ca-certificates git bash curl jq
|
||||
|
||||
ARG HELM_VERSION=v2.13.0
|
||||
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
|
||||
@@ -20,11 +20,22 @@ RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
|
||||
tar zxf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
|
||||
rm ${HELM_FILENAME} && rm -r /linux-amd64
|
||||
|
||||
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
|
||||
# for now but in a future version of alpine (in the testing version at the time of writing)
|
||||
# we should be able to install using apk add.
|
||||
ENV KUBECTL_VERSION="v1.14.5"
|
||||
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
|
||||
RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
|
||||
sha256sum kubectl | grep ${KUBECTL_SHA256} && \
|
||||
chmod +x kubectl && \
|
||||
mv kubectl /usr/local/bin/kubectl
|
||||
|
||||
RUN mkdir -p "$(helm home)/plugins"
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff && \
|
||||
helm plugin install https://github.com/futuresimple/helm-secrets && \
|
||||
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
|
||||
helm plugin install https://github.com/aslafy-z/helm-git.git
|
||||
helm plugin install https://github.com/aslafy-z/helm-git.git && \
|
||||
helm plugin install https://github.com/rimusz/helm-tiller
|
||||
|
||||
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ integration:
|
||||
.PHONY: integration
|
||||
|
||||
cross:
|
||||
env CGO_ENABLED=0 gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!arm !mips !mipsle !mips64 !mips64le !s390x' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: cross
|
||||
|
||||
static-linux:
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: linux
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS=-mod=vendor go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: static-linux
|
||||
|
||||
install:
|
||||
env CGO_ENABLED=0 go install -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
# Helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
|
||||
Deploy Kubernetes Helm Charts
|
||||
|
||||
[](https://quay.io/repository/roboll/helmfile)
|
||||
[](https://slack.sweetops.com)
|
||||
|
||||
## status
|
||||
## Status
|
||||
|
||||
Even though Helmfile is used in production environments [across multiple organizations](USERS.md), it is still in its early stage of development, hence versioned 0.x.
|
||||
|
||||
Helmfile complies to Semantic Versioning 2.0.0 in which v0.x means that there could be backward-incompatible changes for every release.
|
||||
|
||||
Note that we will try our best to document any backward incompatibility.
|
||||
Note that we will try our best to document any backward incompatibility. And in reality, helmfile had no breaking change for a year or so.
|
||||
|
||||
## about
|
||||
## About
|
||||
|
||||
Helmfile is a declarative spec for deploying helm charts. It lets you...
|
||||
|
||||
@@ -23,9 +23,19 @@ Helmfile is a declarative spec for deploying helm charts. It lets you...
|
||||
|
||||
To avoid upgrades for each iteration of `helm`, the `helmfile` executable delegates to `helm` - as a result, `helm` must be installed.
|
||||
|
||||
## configuration syntax
|
||||
## Highlights
|
||||
|
||||
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.31.0](https://github.com/roboll/helmfile/tree/v0.31.0).
|
||||
**Declarative**: Write, version-control, apply the desired state file for visibility and reproducibility.
|
||||
|
||||
**Modules**: Modularize common patterns of your infrastructure, distribute it via Git, S3, etc. to be reused across the entire company (See [#648](https://github.com/roboll/helmfile/pull/648))
|
||||
|
||||
**Versatility**: Manage your cluster consisting of charts, [kustomizations](https://github.com/kubernetes-sigs/kustomize), and directories of Kubernetes resources, turning everything to Helm releases (See [#673](https://github.com/roboll/helmfile/pull/673))
|
||||
|
||||
**Patch**: JSON/Strategic-Merge Patch Kubernetes resources before `helm-install`ing, without forking upstream charts (See [#673](https://github.com/roboll/helmfile/pull/673))
|
||||
|
||||
## Configuration
|
||||
|
||||
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.79.1](https://github.com/roboll/helmfile/tree/v0.79.1).
|
||||
|
||||
The default helmfile is `helmfile.yaml`:
|
||||
|
||||
@@ -51,6 +61,11 @@ repositories:
|
||||
keyFile: optional_client_key
|
||||
username: optional_username
|
||||
password: optional_password
|
||||
# Advanced configuration: You can use a ca bundle to use an https repo
|
||||
# with a self-signed certificate
|
||||
- name: insecure
|
||||
url: https://charts.my-insecure-domain.com
|
||||
caFile: optional_ca_crt
|
||||
|
||||
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
|
||||
|
||||
@@ -154,6 +169,10 @@ releases:
|
||||
tlsCert: "path/to/cert.pem"
|
||||
# path to TLS key file (default "$HELM_HOME/key.pem")
|
||||
tlsKey: "path/to/key.pem"
|
||||
# --kube-context to be passed to helm commands
|
||||
# CAUTION: this doesn't work as expected for `tilerless: true`.
|
||||
# See https://github.com/roboll/helmfile/issues/642
|
||||
kubeContext: kube-context
|
||||
|
||||
# Local chart example
|
||||
- name: grafana # name of this release
|
||||
@@ -182,7 +201,7 @@ helmfiles:
|
||||
# Inline state values merged into the nested state's values
|
||||
- key1: val1
|
||||
- # All the nested state files under `helmfiles:` is processed in the order of definition.
|
||||
# So it can be used for preparation for your main `releases`. An example would be creating CRDs required by `reelases` in the parent state file.
|
||||
# So it can be used for preparation for your main `releases`. An example would be creating CRDs required by `releases` in the parent state file.
|
||||
path: path/to/mycrd.helmfile.yaml
|
||||
- # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file
|
||||
# The nested-state file is locally checked-out along with the remote directory containing it.
|
||||
@@ -198,17 +217,17 @@ helmfiles:
|
||||
# The default is `environments: {"default": {}}` which implies:
|
||||
#
|
||||
# - `{{ .Environment.Name }}` evaluates to "default"
|
||||
# - `{{ .Environment.Values }}` being empty
|
||||
# - `{{ .Values }}` being empty
|
||||
environments:
|
||||
# The "default" environment is available and used when `helmfile` is run without `--environment NAME`.
|
||||
default:
|
||||
# Everything from the values.yaml is available via `{{ .Environment.Values.KEY }}`.
|
||||
# Everything from the values.yaml is available via `{{ .Values.KEY }}`.
|
||||
# Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below,
|
||||
# `{{ .Environment.Values.foo.bar }}` is evaluated to `1`.
|
||||
# `{{ .Values.foo.bar }}` is evaluated to `1`.
|
||||
values:
|
||||
- environments/default/values.yaml
|
||||
# Each entry in values can be either a file path or inline values.
|
||||
# The below is an example of inline values, which is merged to the `.Environment.Values`
|
||||
# The below is an example of inline values, which is merged to the `.Values`
|
||||
- myChartVer: 1.0.0-dev
|
||||
# Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`.
|
||||
# That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`.
|
||||
@@ -362,7 +381,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
### sync
|
||||
|
||||
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any yaml file can be passed by specifying a `--file path/to/your/yaml/file` flag.
|
||||
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any YAML file can be passed by specifying a `--file path/to/your/yaml/file` flag.
|
||||
|
||||
Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. It also updates specified chart repositories and updates the
|
||||
dependencies of any referenced local charts.
|
||||
@@ -611,7 +630,7 @@ releaseName: prod
|
||||
`values.yaml.gotmpl`
|
||||
|
||||
```yaml
|
||||
domain: {{ .Environment.Values | getOrNil "my.domain" | default "dev.example.com" }}
|
||||
domain: {{ .Values | getOrNil "my.domain" | default "dev.example.com" }}
|
||||
```
|
||||
|
||||
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
|
||||
@@ -638,17 +657,26 @@ environments:
|
||||
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
|
||||
|
||||
releases:
|
||||
- name: myapp-{{ .Environment.Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
||||
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
||||
values:
|
||||
- values.yaml.gotmpl
|
||||
|
||||
{{ if eq (.Environment.Values.releaseName "prod" ) }}
|
||||
{{ if eq (.Values.releaseName "prod" ) }}
|
||||
# this release would be installed only if selected environment is `production`
|
||||
- name: production-specific-release
|
||||
...
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Note
|
||||
|
||||
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
|
||||
|
||||
Prior to this [pull request](https://github.com/roboll/helmfile/pull/647), environment values were made available through the `{{ .Environment.Values.foo }}` syntax.
|
||||
This is still working but is **deprecated** and the new `{{ .Values.foo }}` syntax should be used instead.
|
||||
|
||||
You can read more infos about the feature proposal [here](https://github.com/roboll/helmfile/issues/640).
|
||||
|
||||
## Environment Secrets
|
||||
|
||||
Environment Secrets (not to be confused with Kubernetes Secrets) are encrypted versions of `Environment Values`.
|
||||
@@ -683,7 +711,7 @@ releases:
|
||||
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
|
||||
|
||||
```yaml
|
||||
{{ .Environment.Values.foo.bar }}
|
||||
{{ .Values.foo.bar }}
|
||||
```
|
||||
|
||||
## Tillerless
|
||||
@@ -693,9 +721,6 @@ With the [helm-tiller](https://github.com/rimusz/helm-tiller) plugin installed,
|
||||
To enable this mode, you need to define `tillerless: true` and set the `tillerNamespace` in the `helmDefaults` section
|
||||
or in the `releases` entries.
|
||||
|
||||
Since every commands is run with `helm tiller run ...`, you have to disable concurrency. Otherwise you'll get
|
||||
mysterious errors about the tiller daemon.
|
||||
|
||||
## Separating helmfile.yaml into multiple independent files
|
||||
|
||||
Once your `helmfile.yaml` got to contain too many releases,
|
||||
@@ -818,9 +843,10 @@ A Helmfile hook is a per-release extension point that is composed of:
|
||||
- `events`
|
||||
- `command`
|
||||
- `args`
|
||||
- `showlogs`
|
||||
|
||||
Helmfile triggers various `events` while it is running.
|
||||
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`.
|
||||
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`. The standard output of the `command` will be displayed if `showlogs` is set and it's value is `true`.
|
||||
|
||||
Currently supported `events` are:
|
||||
|
||||
@@ -846,6 +872,7 @@ releases:
|
||||
# *snip*
|
||||
hooks:
|
||||
- events: ["prepare", "cleanup"]
|
||||
showlogs: true
|
||||
command: "echo"
|
||||
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
|
||||
"]
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# Helmfile Users
|
||||
|
||||
The following table is compiled from public information or from survey responses. If you are using Helmfile in your
|
||||
company or organization, we would like to invite you to [participate in our user survey](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform)
|
||||
to help us extend this list.
|
||||
Who's actually using Helmfile? The following table is compiled from public information, survey response and PRs. It's
|
||||
likely this is only a small selection of actual users, but it's a start.
|
||||
|
||||
If you are using Helmfile in your company or organization, we would like to invite you to create a PR to add your
|
||||
information to this file.
|
||||
|
||||
<!-- TABLE_START -->
|
||||
| Organization | Workload | More Info | Location |
|
||||
|---|---|---|---|
|
||||
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249) | San Francisco, CA, US |
|
||||
| [ricardo.ch](https://www.ricardo.ch/) | proof-of-concept | | Zug, Switzerland |
|
||||
| Organization | Workload | More Info | Location | Added |
|
||||
|---|---|---|---|---|
|
||||
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249), [Talk](https://www.slideshare.net/GregoryTaylor11/helm-at-reddit-from-local-dev-staging-to-production) | San Francisco, CA | April 2018 |
|
||||
| [ricardo.ch](https://www.ricardo.ch/) | production | We're deploying our complete application platform using Helmfile. | Zug, Switzerland | April 2018 |
|
||||
| [Sight Machine](https://sightmachine.com/) | production | We don't have anything publicly posted about it, but have been using it for quite a while in production. | San Francisco, CA and Ann Arbor, MI | December 2018 |
|
||||
| [VSHN – The DevOps Company](https://vshn.ch) | production | | Zurich, Switzerland | March 2019 |
|
||||
| [Vlocity](https://vlocity.com/) | proof-of-concept | | Melbourne, Australia | March 2019 |
|
||||
| [transit](https://transit.app/) | production | [Blog post](https://medium.com/@naseem_60378/helmfile-its-like-a-helm-for-your-helm-74a908581599). | Montreal, Canada | March 2019 |
|
||||
| [uniqkey](https://uniqkey.eu/) | production | [Wiki Page](https://ocd-scm.github.io/ocd-meta/) | Copenhagen, Denmark | April 2019 |
|
||||
| [bitsofinfo](https://github.com/bitsofinfo/helmfile-deploy) | proof-of-concept, stage | Used with [helmfile-deploy](https://github.com/bitsofinfo/helmfile-deploy) to manage releases for dozens of apps | USA | July 2019 |
|
||||
| [kloeckner-i](https://www.kloeckner-i.com/) | production | We are deploying our standard tools via helmfile to all our clusters. | Berlin, Germany | September 2019 |
|
||||
<!-- TABLE_END -->
|
||||
|
||||
## Contact
|
||||
|
||||
Please fill out [the Google form](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform) to add your organization instead of opening a PR.
|
||||
|
||||
You can also [reach the maintainer on Twitter](https://twitter.com/DerCed).
|
||||
+50
-10
@@ -11,13 +11,13 @@ One example of how helmfile achieves it is that, `helmfile` fails when you tried
|
||||
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
|
||||
|
||||
```
|
||||
{{ .Environment.Values.eventApi.replicas | default 1 }}
|
||||
{{ .Values.eventApi.replicas | default 1 }}
|
||||
```
|
||||
|
||||
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
|
||||
|
||||
```
|
||||
{{ .Environment.Values | getOrNil "eventApi.replicas" }}
|
||||
{{ .Values | getOrNil "eventApi.replicas" }}
|
||||
```
|
||||
|
||||
This result in printing `<no value` in your template, that may or may not result in a failure.
|
||||
@@ -25,7 +25,7 @@ This result in printing `<no value` in your template, that may or may not result
|
||||
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
|
||||
|
||||
```
|
||||
{{ .Environment.Values | getOrNil "eventApi.replicas" | default 1 }}
|
||||
{{ .Values | getOrNil "eventApi.replicas" | default 1 }}
|
||||
```
|
||||
|
||||
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
|
||||
@@ -89,8 +89,48 @@ releases:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
Release Templating supports the following parts of release definition:
|
||||
- basic fields: `name`, `namespace`, `chart`, `version`
|
||||
- boolean fields: `installed`, `wait`, `tillerless`, `verify` by the means of additional text
|
||||
fields designed for templating only: `installedTemplate`, `waitTemplate`, `tillerlessTemplate`, `verifyTemplate`
|
||||
```yaml
|
||||
# ...
|
||||
installedTemplate: '{{`{{ eq .Release.Namespace "kube-system" }}`}}'
|
||||
waitTemplate: '{{`{{ eq .Release.Labels.tag "safe" | not }}`}}'
|
||||
# ...
|
||||
```
|
||||
- `set` block values:
|
||||
```yaml
|
||||
# ...
|
||||
setTemplate:
|
||||
- name: '{{`{{ .Release.Name }}`}}'
|
||||
values: '{{`{{ .Release.Namespace }}`}}'
|
||||
# ...
|
||||
```
|
||||
- `values` and `secrets` file paths:
|
||||
```yaml
|
||||
# ...
|
||||
valuesTemplate:
|
||||
- config/{{`{{ .Release.Name }}`}}/values.yaml
|
||||
secrets:
|
||||
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
|
||||
# ...
|
||||
```
|
||||
- inline `values` map:
|
||||
```yaml
|
||||
# ...
|
||||
valuesTemplate:
|
||||
- image:
|
||||
tag: `{{ .Release.Labels.tag }}`
|
||||
# ...
|
||||
```
|
||||
|
||||
See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work.
|
||||
|
||||
## Layering Release Values
|
||||
|
||||
Please note, that it is not possible to layer `values` sections. If `values` is defined in the release and in the release template, only the `values` defined in the release will be considered. The same applies to `secrets` and `set`.
|
||||
|
||||
## Layering State Files
|
||||
|
||||
> See **Layering State Template Files** if you're layering templates.
|
||||
@@ -227,7 +267,7 @@ bases:
|
||||
# Part 3: Dynamic Releases
|
||||
releases:
|
||||
- name: test1
|
||||
chart: mychart-{{ .Environment.Values.myname }}
|
||||
chart: mychart-{{ .Values.myname }}
|
||||
values:
|
||||
replicaCount: 1
|
||||
image:
|
||||
@@ -262,9 +302,9 @@ Where the gotmpl file loaded in the second part looks like:
|
||||
```yaml
|
||||
helmDefaults:
|
||||
tillerNamespace: kube-system
|
||||
kubeContext: {{ .Environment.Values.kubeContext }}
|
||||
kubeContext: {{ .Values.kubeContext }}
|
||||
verify: false
|
||||
{{ if .Environment.Values.wait }}
|
||||
{{ if .Values.wait }}
|
||||
wait: true
|
||||
{{ else }}
|
||||
wait: false
|
||||
@@ -274,9 +314,9 @@ helmDefaults:
|
||||
force: true
|
||||
```
|
||||
|
||||
Each go template is rendered in the context where `.Environment.Values` is inherited from the previous part.
|
||||
Each go template is rendered in the context where `.Values` is inherited from the previous part.
|
||||
|
||||
So in `mydefaults.yaml.gotmpl`, both `.Environment.Values.kubeContext` and `.Environment.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to:
|
||||
So in `mydefaults.yaml.gotmpl`, both `.Values.kubeContext` and `.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to:
|
||||
|
||||
```yaml
|
||||
helmDefaults:
|
||||
@@ -289,13 +329,13 @@ helmDefaults:
|
||||
force: true
|
||||
```
|
||||
|
||||
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Environment.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
|
||||
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
|
||||
|
||||
```yaml
|
||||
# Part 3: Dynamic Releases
|
||||
releases:
|
||||
- name: test1
|
||||
chart: mychart-{{ .Environment.Values.myname }}
|
||||
chart: mychart-{{ .Values.myname }}
|
||||
values:
|
||||
replicaCount: 1
|
||||
image:
|
||||
|
||||
@@ -3,20 +3,25 @@ module github.com/roboll/helmfile
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||
github.com/Masterminds/semver v1.4.1
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible
|
||||
github.com/aokoli/goutils v1.0.1 // indirect
|
||||
github.com/Masterminds/sprig v2.20.0+incompatible
|
||||
github.com/go-test/deep v1.0.3
|
||||
github.com/google/go-cmp v0.3.0
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
|
||||
github.com/gosuri/uitable v0.0.3
|
||||
github.com/hashicorp/go-getter v1.3.0
|
||||
github.com/huandu/xstrings v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad
|
||||
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
|
||||
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/multierr v1.1.0
|
||||
go.uber.org/zap v1.8.0
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
gotest.tools v2.2.0+incompatible
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0
|
||||
|
||||
@@ -6,17 +6,15 @@ dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.4.1 h1:CaDA1wAoM3rj9sAFyyZP37LloExUzxFGYt+DqJ870JA=
|
||||
github.com/Masterminds/semver v1.4.1/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRDINkUyuvbb4b2Xm8w=
|
||||
github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8=
|
||||
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY=
|
||||
github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -26,14 +24,19 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT
|
||||
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -46,6 +49,7 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c=
|
||||
@@ -55,6 +59,8 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk
|
||||
github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mqJt0Cc=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gosuri/uitable v0.0.3 h1:9ZY4qCODg6JL1Ui4dL9LqCF4ghWnAOSV2h7xG98SkHE=
|
||||
github.com/gosuri/uitable v0.0.3/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
|
||||
@@ -65,8 +71,8 @@ github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhE
|
||||
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
@@ -77,7 +83,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
@@ -92,11 +100,14 @@ github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad h1:j5pg/OewZJyE6i3hIG4v3eQUvUyFdQkC8Nd/mjaEkxE=
|
||||
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
@@ -123,6 +134,7 @@ github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYED
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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=
|
||||
@@ -141,8 +153,6 @@ go.uber.org/zap v1.8.0 h1:r6Za1Rii8+EGOYRDLvpooNOF6kP3iyDnkpzbw67gCQ8=
|
||||
go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc h1:Kx1Ke+iCR1aDjbWXgmEQGFxoHtNL49aRZGV7/+jJ41Y=
|
||||
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -161,10 +171,12 @@ golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcp
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
|
||||
@@ -179,6 +191,7 @@ google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
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 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -190,6 +203,7 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
|
||||
@@ -2,6 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/app"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
@@ -9,8 +12,6 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Version string
|
||||
@@ -74,6 +75,10 @@ func main() {
|
||||
Name: "kube-context",
|
||||
Usage: "Set kubectl context. Uses current context by default",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-color",
|
||||
Usage: "Output without color",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Usage: "Set log level, default info",
|
||||
@@ -110,6 +115,10 @@ func main() {
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-repos",
|
||||
Usage: "skip running `helm repo update` before running `helm dependency build`",
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.Deps(c)
|
||||
@@ -138,6 +147,10 @@ func main() {
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
@@ -161,6 +174,10 @@ func main() {
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
@@ -182,6 +199,11 @@ func main() {
|
||||
Value: 0,
|
||||
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "context",
|
||||
Value: 0,
|
||||
Usage: "output NUM lines of context around changes",
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.Diff(c)
|
||||
@@ -196,10 +218,18 @@ func main() {
|
||||
Value: "",
|
||||
Usage: "pass args to helm template",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "output-dir",
|
||||
Usage: "output directory to pass to helm template (helm template --output-dir)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Value: 0,
|
||||
@@ -223,6 +253,10 @@ func main() {
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
@@ -245,6 +279,10 @@ func main() {
|
||||
Name: "sync",
|
||||
Usage: "sync all resources from state file (repos, releases and chart deps)",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
@@ -272,6 +310,10 @@ func main() {
|
||||
Name: "apply",
|
||||
Usage: "apply all resources from state file only when there are changes",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
@@ -281,6 +323,11 @@ func main() {
|
||||
Value: 0,
|
||||
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "context",
|
||||
Value: 0,
|
||||
Usage: "output NUM lines of context around changes",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
@@ -322,6 +369,11 @@ func main() {
|
||||
Name: "delete",
|
||||
Usage: "DEPRECATED: delete releases from state file (helm delete)",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Value: 0,
|
||||
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
@@ -340,6 +392,11 @@ func main() {
|
||||
Name: "destroy",
|
||||
Usage: "deletes and then purges releases",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Value: 0,
|
||||
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
@@ -378,6 +435,22 @@ func main() {
|
||||
return run.Test(c)
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "build",
|
||||
Usage: "output compiled helmfile state(s) as YAML",
|
||||
Flags: []cli.Flag{},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.PrintState(c)
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list releases defined in state file",
|
||||
Flags: []cli.Flag{},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.ListReleases(c)
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
err := cliApp.Run(os.Args)
|
||||
@@ -409,7 +482,7 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
|
||||
for i := range optsSet {
|
||||
ops := strings.Split(optsSet[i], ",")
|
||||
for j := range ops {
|
||||
op := strings.Split(ops[j], "=")
|
||||
op := strings.SplitN(ops[j], "=", 2)
|
||||
k := strings.Split(op[0], ".")
|
||||
v := op[1]
|
||||
|
||||
@@ -422,6 +495,14 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (c configImpl) Set() []string {
|
||||
return c.c.StringSlice("set")
|
||||
}
|
||||
|
||||
func (c configImpl) SkipRepos() bool {
|
||||
return c.c.Bool("skip-repos")
|
||||
}
|
||||
|
||||
func (c configImpl) Values() []string {
|
||||
return c.c.StringSlice("values")
|
||||
}
|
||||
@@ -430,6 +511,10 @@ func (c configImpl) Args() string {
|
||||
return c.c.String("args")
|
||||
}
|
||||
|
||||
func (c configImpl) OutputDir() string {
|
||||
return c.c.String("output-dir")
|
||||
}
|
||||
|
||||
func (c configImpl) Concurrency() int {
|
||||
return c.c.Int("concurrency")
|
||||
}
|
||||
@@ -490,11 +575,11 @@ func (c configImpl) Selectors() []string {
|
||||
return c.c.GlobalStringSlice("selector")
|
||||
}
|
||||
|
||||
func (c configImpl) Set() map[string]interface{} {
|
||||
func (c configImpl) StateValuesSet() map[string]interface{} {
|
||||
return c.set
|
||||
}
|
||||
|
||||
func (c configImpl) ValuesFiles() []string {
|
||||
func (c configImpl) StateValuesFiles() []string {
|
||||
return c.c.GlobalStringSlice("state-values-file")
|
||||
}
|
||||
|
||||
@@ -502,6 +587,14 @@ func (c configImpl) Interactive() bool {
|
||||
return c.c.GlobalBool("interactive")
|
||||
}
|
||||
|
||||
func (c configImpl) NoColor() bool {
|
||||
return c.c.GlobalBool("no-color")
|
||||
}
|
||||
|
||||
func (c configImpl) Context() int {
|
||||
return c.c.Int("context")
|
||||
}
|
||||
|
||||
func (c configImpl) Logger() *zap.SugaredLogger {
|
||||
return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
|
||||
}
|
||||
|
||||
+60
-25
@@ -2,9 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@@ -12,6 +9,11 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"path/filepath"
|
||||
@@ -45,6 +47,8 @@ type App struct {
|
||||
chdir func(string) error
|
||||
|
||||
remote *remote.Remote
|
||||
|
||||
helmExecer helmexec.Interface
|
||||
}
|
||||
|
||||
func New(conf ConfigProvider) *App {
|
||||
@@ -57,8 +61,11 @@ func New(conf ConfigProvider) *App {
|
||||
HelmBinary: conf.HelmBinary(),
|
||||
Args: conf.Args(),
|
||||
FileOrDir: conf.FileOrDir(),
|
||||
ValuesFiles: conf.ValuesFiles(),
|
||||
Set: conf.Set(),
|
||||
ValuesFiles: conf.StateValuesFiles(),
|
||||
Set: conf.StateValuesSet(),
|
||||
helmExecer: helmexec.New(conf.Logger(), conf.KubeContext(), &helmexec.ShellRunner{
|
||||
Logger: conf.Logger(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -152,6 +159,38 @@ func (a *App) Test(c TestConfigProvider) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) PrintState(c StateConfigProvider) error {
|
||||
|
||||
return a.ForEachState(func(run *Run) []error {
|
||||
state, err := run.state.ToYaml()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
fmt.Printf("---\n# Source: %s\n\n%+v", run.state.FilePath, state)
|
||||
return []error{}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) ListReleases(c StateConfigProvider) error {
|
||||
table := uitable.New()
|
||||
table.AddRow("NAME", "NAMESPACE", "INSTALLED", "LABELS")
|
||||
|
||||
err := a.ForEachState(func(run *Run) []error {
|
||||
//var releases m
|
||||
for _, r := range run.state.Releases {
|
||||
labels := ""
|
||||
for k, v := range r.Labels {
|
||||
labels = fmt.Sprintf("%s,%s:%s", labels, k, v)
|
||||
}
|
||||
installed := r.Installed == nil || *r.Installed
|
||||
table.AddRow(r.Name, r.Namespace, fmt.Sprintf("%t", installed), strings.Trim(labels, ","))
|
||||
}
|
||||
return []error{}
|
||||
})
|
||||
fmt.Println(table.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) within(dir string, do func() error) error {
|
||||
if dir == "." {
|
||||
return do()
|
||||
@@ -234,6 +273,7 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He
|
||||
Reverse: a.Reverse,
|
||||
KubeContext: a.KubeContext,
|
||||
glob: a.glob,
|
||||
helm: a.helmExecer,
|
||||
}
|
||||
|
||||
var op LoadOpts
|
||||
@@ -274,7 +314,7 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
|
||||
|
||||
ctx := context{a, st}
|
||||
|
||||
helm := helmexec.New(a.Logger, a.KubeContext)
|
||||
helm := a.helmExecer
|
||||
|
||||
if err != nil {
|
||||
switch stateLoadErr := err.(type) {
|
||||
@@ -306,13 +346,6 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
|
||||
optsForNestedState.Selectors = m.Selectors
|
||||
}
|
||||
|
||||
path, err := a.remote.Locate(m.Path)
|
||||
if err != nil {
|
||||
return appError(fmt.Sprintf("in .helmfiles[%d]", i), err)
|
||||
}
|
||||
|
||||
m.Path = path
|
||||
|
||||
if err := a.visitStates(m.Path, optsForNestedState, converge); err != nil {
|
||||
switch err.(type) {
|
||||
case *NoMatchingHelmfileError:
|
||||
@@ -348,9 +381,8 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
|
||||
}
|
||||
|
||||
func (a *App) ForEachState(do func(*Run) []error) error {
|
||||
ctx := NewContext()
|
||||
err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
ctx := NewContext()
|
||||
|
||||
run := NewRun(st, helm, ctx)
|
||||
|
||||
return do(run)
|
||||
@@ -384,11 +416,9 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
|
||||
opts.Environment.OverrideValues = envvals
|
||||
}
|
||||
|
||||
var dir string
|
||||
if a.directoryExistsAt(fileOrDir) {
|
||||
dir = fileOrDir
|
||||
} else {
|
||||
dir = filepath.Dir(fileOrDir)
|
||||
dir, err := a.getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getter := &remote.GoGetter{Logger: a.Logger}
|
||||
@@ -404,7 +434,7 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
|
||||
|
||||
a.remote = remote
|
||||
|
||||
err := a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
|
||||
return a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
|
||||
if len(st.Selectors) > 0 {
|
||||
err := st.FilterReleases()
|
||||
if err != nil {
|
||||
@@ -440,10 +470,6 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
|
||||
|
||||
return processed, errs
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) {
|
||||
@@ -463,6 +489,15 @@ func (a *App) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
|
||||
path, err := a.remote.Locate(specifiedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("locate: %v", err)
|
||||
}
|
||||
if specifiedPath != path {
|
||||
a.Logger.Debugf("fetched remote \"%s\" to local cache \"%s\" and loading the latter...", specifiedPath, path)
|
||||
}
|
||||
specifiedPath = path
|
||||
|
||||
var helmfileDir string
|
||||
if specifiedPath != "" {
|
||||
if a.fileExistsAt(specifiedPath) {
|
||||
|
||||
+478
-29
@@ -1,15 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gotest.tools/env"
|
||||
)
|
||||
|
||||
@@ -902,8 +912,8 @@ bar: "bar1"
|
||||
}
|
||||
|
||||
func TestVisitDesiredStatesWithReleasesFiltered_StateValueOverrides(t *testing.T) {
|
||||
envTmplExpr := "{{ .Values.foo }}-{{ .Values.bar }}-{{ .Values.baz }}-{{ .Values.hoge }}-{{ .Values.fuga }}-{{ .Values.a | first | pluck \"b\" | first | first | pluck \"c\" | first }}"
|
||||
relTmplExpr := "\"{{`{{ .Values.foo }}-{{ .Values.bar }}-{{ .Values.baz }}-{{ .Values.hoge }}-{{ .Values.fuga }}-{{ .Values.a | first | pluck \\\"b\\\" | first | first | pluck \\\"c\\\" | first }}`}}\""
|
||||
envTmplExpr := "{{ .Values.x.foo }}-{{ .Values.x.bar }}-{{ .Values.x.baz }}-{{ .Values.x.hoge }}-{{ .Values.x.fuga }}-{{ .Values.x.a | first | pluck \"b\" | first | first | pluck \"c\" | first }}"
|
||||
relTmplExpr := "\"{{`{{ .Values.x.foo }}-{{ .Values.x.bar }}-{{ .Values.x.baz }}-{{ .Values.x.hoge }}-{{ .Values.x.fuga }}-{{ .Values.x.a | first | pluck \\\"b\\\" | first | first | pluck \\\"c\\\" | first }}`}}\""
|
||||
|
||||
testcases := []struct {
|
||||
expr, env, expected string
|
||||
@@ -953,35 +963,35 @@ releases:
|
||||
namespace: %s
|
||||
`, testcase.expr, testcase.expr, testcase.expr),
|
||||
"/path/to/values.yaml": `
|
||||
foo: foo
|
||||
bar: bar
|
||||
baz: baz
|
||||
hoge: hoge
|
||||
fuga: fuga
|
||||
|
||||
a: []
|
||||
x:
|
||||
foo: foo
|
||||
bar: bar
|
||||
baz: baz
|
||||
hoge: hoge
|
||||
fuga: fuga
|
||||
a: []
|
||||
`,
|
||||
"/path/to/default.yaml": `
|
||||
bar: "bar_default"
|
||||
baz: "baz_default"
|
||||
|
||||
a:
|
||||
- b: []
|
||||
x:
|
||||
bar: "bar_default"
|
||||
baz: "baz_default"
|
||||
a:
|
||||
- b: []
|
||||
`,
|
||||
"/path/to/production.yaml": `
|
||||
bar: "bar_production"
|
||||
baz: "baz_production"
|
||||
|
||||
a:
|
||||
- b: []
|
||||
x:
|
||||
bar: "bar_production"
|
||||
baz: "baz_production"
|
||||
a:
|
||||
- b: []
|
||||
`,
|
||||
"/path/to/overrides.yaml": `
|
||||
baz: baz_override
|
||||
hoge: hoge_override
|
||||
|
||||
a:
|
||||
- b:
|
||||
- c: C
|
||||
x:
|
||||
baz: baz_override
|
||||
hoge: hoge_override
|
||||
a:
|
||||
- b:
|
||||
- c: C
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -1001,7 +1011,7 @@ a:
|
||||
Selectors: []string{},
|
||||
Env: testcase.env,
|
||||
ValuesFiles: []string{"overrides.yaml"},
|
||||
Set: map[string]interface{}{"hoge": "hoge_set", "fuga": "fuga_set"},
|
||||
Set: map[string]interface{}{"x": map[string]interface{}{"hoge": "hoge_set", "fuga": "fuga_set"}},
|
||||
}, files)
|
||||
err := app.VisitDesiredStatesWithReleasesFiltered(
|
||||
"helmfile.yaml", collectReleases,
|
||||
@@ -1025,6 +1035,99 @@ a:
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisitDesiredStatesWithReleasesFiltered_ChartAtAbsPath(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: /path/to/mychart
|
||||
`,
|
||||
}
|
||||
|
||||
actual := []state.ReleaseSpec{}
|
||||
|
||||
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
for _, r := range st.Releases {
|
||||
actual = append(actual, r)
|
||||
}
|
||||
return []error{}
|
||||
}
|
||||
app := appWithFs(&App{
|
||||
KubeContext: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Reverse: false,
|
||||
Namespace: "",
|
||||
Env: "default",
|
||||
Selectors: []string{},
|
||||
}, files)
|
||||
err := app.VisitDesiredStatesWithReleasesFiltered(
|
||||
"helmfile.yaml", collectReleases,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(actual) != 1 {
|
||||
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
|
||||
}
|
||||
if actual[0].Name != "myapp" {
|
||||
t.Errorf("unexpected name: expected=%s, got=%s", "myapp", actual[0].Name)
|
||||
}
|
||||
if actual[0].Chart != "/path/to/mychart" {
|
||||
t.Errorf("unexpected chart: expected=%s, got=%s", "/path/to/mychart", actual[0].Chart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisitDesiredStatesWithReleasesFiltered_RemoteTgzAsChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
expr, env, expected string
|
||||
}{
|
||||
{
|
||||
expected: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
|
||||
},
|
||||
}
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: arangodb-crd
|
||||
chart: https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz
|
||||
`,
|
||||
}
|
||||
|
||||
actual := []state.ReleaseSpec{}
|
||||
|
||||
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
for _, r := range st.Releases {
|
||||
actual = append(actual, r)
|
||||
}
|
||||
return []error{}
|
||||
}
|
||||
app := appWithFs(&App{
|
||||
KubeContext: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Reverse: false,
|
||||
Namespace: "",
|
||||
Selectors: []string{},
|
||||
Env: "default",
|
||||
}, files)
|
||||
err := app.VisitDesiredStatesWithReleasesFiltered(
|
||||
"helmfile.yaml", collectReleases,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(actual) != 1 {
|
||||
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
|
||||
}
|
||||
if actual[0].Chart != testcase.expected {
|
||||
t.Errorf("unexpected chart: expected=%s, got=%s", testcase.expected, actual[0].Chart)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
@@ -1127,6 +1230,10 @@ helmDefaults:
|
||||
if st.Releases[1].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
|
||||
t.Errorf("unexpected releases[0].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
|
||||
}
|
||||
|
||||
if st.FilePath != yamlFile {
|
||||
t.Errorf("unexpected filePath: expected=%s, got=%s", yamlFile, st.FilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) {
|
||||
@@ -1649,6 +1756,24 @@ releases:
|
||||
{stateExternal, `{{ if (keys .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
|
||||
// See https://github.com/roboll/helmfile/issues/643
|
||||
{stateExternal, `{{ range $service := .Environment.Values.services }}{{ $service.name }}{{ if hasKey $service "something" }}{{ $service.something }}{{ end }}{{ end }}`, `xyfalse`},
|
||||
// Same test with .Values
|
||||
{stateInline, `{{ getOrNil "foo" .Values }}`, `FOO`},
|
||||
{stateInline, `{{ getOrNil "baz" (getOrNil "bar" .Values) }}`, `BAZ`},
|
||||
{stateInline, `{{ if hasKey .Values "foo" }}{{ .Values.foo }}{{ end }}`, `FOO`},
|
||||
{stateInline, `{{ if hasKey .Values "bar" }}{{ .Values.bar.baz }}{{ end }}`, `BAZ`},
|
||||
{stateInline, `{{ if (keys .Values | has "foo") }}{{ .Values.foo }}{{ end }}`, `FOO`},
|
||||
// See https://github.com/roboll/helmfile/issues/624
|
||||
// This fails when .Values.bar is not map[string]interface{}. At the time of #624 it was map[interface{}]interface{}, which sprig's dict funcs don't support.
|
||||
{stateInline, `{{ if (keys .Values | has "bar") }}{{ if (keys .Values.bar | has "baz") }}{{ .Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
|
||||
{stateExternal, `{{ getOrNil "foo" .Values }}`, `FOO`},
|
||||
{stateExternal, `{{ getOrNil "baz" (getOrNil "bar" .Values) }}`, `BAZ`},
|
||||
{stateExternal, `{{ if hasKey .Values "foo" }}{{ .Values.foo }}{{ end }}`, `FOO`},
|
||||
{stateExternal, `{{ if hasKey .Values "bar" }}{{ .Values.bar.baz }}{{ end }}`, `BAZ`},
|
||||
{stateExternal, `{{ if (keys .Values | has "foo") }}{{ .Values.foo }}{{ end }}`, `FOO`},
|
||||
// See https://github.com/roboll/helmfile/issues/624
|
||||
{stateExternal, `{{ if (keys .Values | has "bar") }}{{ if (keys .Values.bar | has "baz") }}{{ .Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
|
||||
// See https://github.com/roboll/helmfile/issues/643
|
||||
{stateExternal, `{{ range $service := .Values.services }}{{ $service.name }}{{ if hasKey $service "something" }}{{ $service.something }}{{ end }}{{ end }}`, `xyfalse`},
|
||||
}
|
||||
for i := range testcases {
|
||||
tc := testcases[i]
|
||||
@@ -1683,3 +1808,327 @@ services:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type configImpl struct {
|
||||
set []string
|
||||
}
|
||||
|
||||
func (c configImpl) Set() []string {
|
||||
return c.set
|
||||
}
|
||||
|
||||
func (c configImpl) Values() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (c configImpl) Args() string {
|
||||
return "some args"
|
||||
}
|
||||
|
||||
func (c configImpl) SkipDeps() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c configImpl) OutputDir() string {
|
||||
return "output/subdir"
|
||||
}
|
||||
|
||||
func (c configImpl) Concurrency() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Mocking the command-line runner
|
||||
|
||||
type mockRunner struct {
|
||||
output []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface {
|
||||
execer := helmexec.New(logger, kubeContext, &mockRunner{})
|
||||
return execer
|
||||
}
|
||||
|
||||
// mocking helmexec.Interface
|
||||
|
||||
type listKey struct {
|
||||
filter string
|
||||
flags string
|
||||
}
|
||||
|
||||
type mockHelmExec struct {
|
||||
templated []mockTemplates
|
||||
|
||||
updateDepsCallbacks map[string]func(string) error
|
||||
}
|
||||
|
||||
type mockTemplates struct {
|
||||
name, chart string
|
||||
flags []string
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) TemplateRelease(name, chart string, flags ...string) error {
|
||||
helm.templated = append(helm.templated, mockTemplates{name: name, chart: chart, flags: flags})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) UpdateDeps(chart string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) BuildDeps(name, chart string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) SetExtraArgs(args ...string) {
|
||||
return
|
||||
}
|
||||
func (helm *mockHelmExec) SetHelmBinary(bin string) {
|
||||
return
|
||||
}
|
||||
func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) UpdateRepo() error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (helm *mockHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (helm *mockHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTemplate_SingleStateFile(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
- name: myrelease2
|
||||
chart: mychart2
|
||||
`,
|
||||
}
|
||||
|
||||
var helm = &mockHelmExec{}
|
||||
var wantReleases = []mockTemplates{
|
||||
{name: "myrelease1", chart: "mychart1", flags: []string{"--namespace", "testNamespace", "--set", "foo=a", "--set", "bar=b", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease1"}},
|
||||
{name: "myrelease2", chart: "mychart2", flags: []string{"--namespace", "testNamespace", "--set", "foo=a", "--set", "bar=b", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease2"}},
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
logger := helmexec.NewLogger(&buffer, "debug")
|
||||
|
||||
app := appWithFs(&App{
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helmExecer: helm,
|
||||
Namespace: "testNamespace",
|
||||
}, files)
|
||||
app.Template(configImpl{set: []string{"foo=a", "bar=b"}})
|
||||
|
||||
for i := range wantReleases {
|
||||
if wantReleases[i].name != helm.templated[i].name {
|
||||
t.Errorf("name = [%v], want %v", helm.templated[i].name, wantReleases[i].name)
|
||||
}
|
||||
if !strings.Contains(helm.templated[i].chart, wantReleases[i].chart) {
|
||||
t.Errorf("chart = [%v], want %v", helm.templated[i].chart, wantReleases[i].chart)
|
||||
}
|
||||
for j := range wantReleases[i].flags {
|
||||
if j == 7 {
|
||||
matched, _ := regexp.Match(wantReleases[i].flags[j], []byte(helm.templated[i].flags[j]))
|
||||
if !matched {
|
||||
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
|
||||
}
|
||||
} else if wantReleases[i].flags[j] != helm.templated[i].flags[j] {
|
||||
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(f func()) string {
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = stdout
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
os.Stdout = writer
|
||||
log.SetOutput(writer)
|
||||
out := make(chan string)
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
wg.Done()
|
||||
io.Copy(&buf, reader)
|
||||
out <- buf.String()
|
||||
}()
|
||||
wg.Wait()
|
||||
f()
|
||||
writer.Close()
|
||||
return <-out
|
||||
}
|
||||
|
||||
func TestPrint_SingleStateFile(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
- name: myrelease2
|
||||
chart: mychart1
|
||||
`,
|
||||
}
|
||||
stdout := os.Stdout
|
||||
defer func() { os.Stdout = stdout }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
logger := helmexec.NewLogger(&buffer, "debug")
|
||||
|
||||
app := appWithFs(&App{
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
Namespace: "testNamespace",
|
||||
}, files)
|
||||
out := captureStdout(func() {
|
||||
err := app.PrintState(configImpl{})
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
assert.Assert(t, strings.Count(out, "---") == 1,
|
||||
"state should contain '---' yaml doc separator:\n%s\n", out)
|
||||
assert.Assert(t, strings.Contains(out, "helmfile.yaml"),
|
||||
"state should contain source helmfile name:\n%s\n", out)
|
||||
assert.Assert(t, strings.Contains(out, "name: myrelease1"),
|
||||
"state should contain releases:\n%s\n", out)
|
||||
}
|
||||
|
||||
func TestPrint_MultiStateFile(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.d/first.yaml": `
|
||||
releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
- name: myrelease2
|
||||
chart: mychart1
|
||||
`,
|
||||
"/path/to/helmfile.d/second.yaml": `
|
||||
releases:
|
||||
- name: myrelease3
|
||||
chart: mychart1
|
||||
- name: myrelease4
|
||||
chart: mychart1
|
||||
`,
|
||||
}
|
||||
stdout := os.Stdout
|
||||
defer func() { os.Stdout = stdout }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
logger := helmexec.NewLogger(&buffer, "debug")
|
||||
|
||||
app := appWithFs(&App{
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
Namespace: "testNamespace",
|
||||
}, files)
|
||||
out := captureStdout(func() {
|
||||
err := app.PrintState(configImpl{})
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
assert.Assert(t, strings.Count(out, "---") == 2,
|
||||
"state should contain '---' yaml doc separators:\n%s\n", out)
|
||||
assert.Assert(t, strings.Contains(out, "second.yaml"),
|
||||
"state should contain source helmfile name:\n%s\n", out)
|
||||
assert.Assert(t, strings.Contains(out, "second.yaml"),
|
||||
"state should contain source helmfile name:\n%s\n", out)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.d/first.yaml": `
|
||||
releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
installed: no
|
||||
labels:
|
||||
id: myrelease1
|
||||
- name: myrelease2
|
||||
chart: mychart1
|
||||
`,
|
||||
"/path/to/helmfile.d/second.yaml": `
|
||||
releases:
|
||||
- name: myrelease3
|
||||
chart: mychart1
|
||||
installed: yes
|
||||
- name: myrelease4
|
||||
chart: mychart1
|
||||
labels:
|
||||
id: myrelease1
|
||||
`,
|
||||
}
|
||||
stdout := os.Stdout
|
||||
defer func() { os.Stdout = stdout }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
logger := helmexec.NewLogger(&buffer, "debug")
|
||||
|
||||
app := appWithFs(&App{
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
KubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
Namespace: "testNamespace",
|
||||
}, files)
|
||||
out := captureStdout(func() {
|
||||
err := app.ListReleases(configImpl{})
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
|
||||
expected := `NAME NAMESPACE INSTALLED LABELS
|
||||
myrelease1 false id:myrelease1
|
||||
myrelease2 true
|
||||
myrelease3 true
|
||||
myrelease4 true id:myrelease1
|
||||
`
|
||||
assert.Equal(t, expected, out)
|
||||
}
|
||||
|
||||
+19
-2
@@ -10,8 +10,8 @@ type ConfigProvider interface {
|
||||
KubeContext() string
|
||||
Namespace() string
|
||||
Selectors() []string
|
||||
Set() map[string]interface{}
|
||||
ValuesFiles() []string
|
||||
StateValuesSet() map[string]interface{}
|
||||
StateValuesFiles() []string
|
||||
Env() string
|
||||
|
||||
loggingConfig
|
||||
@@ -26,6 +26,7 @@ type DeprecatedChartsConfigProvider interface {
|
||||
|
||||
type DepsConfigProvider interface {
|
||||
Args() string
|
||||
SkipRepos() bool
|
||||
}
|
||||
|
||||
type ReposConfigProvider interface {
|
||||
@@ -36,10 +37,14 @@ type ApplyConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
|
||||
NoColor() bool
|
||||
Context() int
|
||||
|
||||
concurrencyConfig
|
||||
interactive
|
||||
loggingConfig
|
||||
@@ -49,6 +54,7 @@ type SyncConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
concurrencyConfig
|
||||
@@ -59,11 +65,14 @@ type DiffConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
|
||||
DetailedExitcode() bool
|
||||
NoColor() bool
|
||||
Context() int
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
@@ -75,6 +84,7 @@ type DeleteConfigProvider interface {
|
||||
|
||||
interactive
|
||||
loggingConfig
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type DestroyConfigProvider interface {
|
||||
@@ -82,6 +92,7 @@ type DestroyConfigProvider interface {
|
||||
|
||||
interactive
|
||||
loggingConfig
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type TestConfigProvider interface {
|
||||
@@ -97,6 +108,7 @@ type LintConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
concurrencyConfig
|
||||
@@ -106,7 +118,9 @@ type TemplateConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
OutputDir() string
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
@@ -117,6 +131,9 @@ type StatusesConfigProvider interface {
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type StateConfigProvider interface {
|
||||
}
|
||||
|
||||
type concurrencyConfig interface {
|
||||
Concurrency() int
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"go.uber.org/zap"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type desiredStateLoader struct {
|
||||
@@ -25,6 +27,7 @@ type desiredStateLoader struct {
|
||||
glob func(string) ([]string, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
helm helmexec.Interface
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
|
||||
@@ -37,7 +40,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
|
||||
return nil, fmt.Errorf("bug: opts.CalleePath was nil: f=%s, opts=%v", f, opts)
|
||||
}
|
||||
storage := state.NewStorage(opts.CalleePath, ld.logger, ld.glob)
|
||||
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile)
|
||||
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile, ld.logger)
|
||||
handler := state.MissingFileHandlerError
|
||||
vals, err := envld.LoadEnvironmentValues(&handler, args)
|
||||
if err != nil {
|
||||
@@ -125,7 +128,7 @@ func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *e
|
||||
}
|
||||
|
||||
func (a *desiredStateLoader) underlying() *state.StateCreator {
|
||||
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob)
|
||||
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.helm)
|
||||
c.LoadFile = a.loadFile
|
||||
return c
|
||||
}
|
||||
|
||||
+47
-21
@@ -2,10 +2,11 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/argparser"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Run struct {
|
||||
@@ -30,8 +31,10 @@ func (r *Run) askForConfirmation(msg string) bool {
|
||||
func (r *Run) Deps(c DepsConfigProvider) []error {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
if !c.SkipRepos() {
|
||||
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
|
||||
return r.state.UpdateDeps(r.helm)
|
||||
@@ -83,7 +86,7 @@ Do you really want to delete?
|
||||
if !interactive || interactive && r.askForConfirmation(msg) {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, purge)
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), purge)
|
||||
}
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
@@ -109,7 +112,7 @@ Do you really want to delete?
|
||||
if !interactive || interactive && r.askForConfirmation(msg) {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, true)
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), true)
|
||||
}
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
@@ -136,7 +139,13 @@ func (r *Run) Apply(c ApplyConfigProvider) []error {
|
||||
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
|
||||
detailedExitCode := true
|
||||
|
||||
releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false)
|
||||
diffOpts := &state.DiffOpts{
|
||||
NoColor: c.NoColor(),
|
||||
Context: c.Context(),
|
||||
Set: c.Set(),
|
||||
}
|
||||
|
||||
releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false, diffOpts)
|
||||
|
||||
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
|
||||
if err != nil {
|
||||
@@ -195,7 +204,10 @@ Do you really want to apply?
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
st.Releases = rs
|
||||
return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
|
||||
syncOpts := &state.SyncOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,7 +235,12 @@ func (r *Run) Diff(c DiffConfigProvider) []error {
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true)
|
||||
opts := &state.DiffOpts{
|
||||
Context: c.Context(),
|
||||
NoColor: c.NoColor(),
|
||||
Set: c.Set(),
|
||||
}
|
||||
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true, opts)
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -247,30 +264,36 @@ func (r *Run) Sync(c SyncConfigProvider) []error {
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
|
||||
opts := &state.SyncOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *Run) Template(c TemplateConfigProvider) []error {
|
||||
state := r.state
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
|
||||
if errs := st.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
args := argparser.GetArgs(c.Args(), state)
|
||||
return state.TemplateReleases(helm, c.Values(), args, c.Concurrency())
|
||||
args := argparser.GetArgs(c.Args(), st)
|
||||
opts := &state.TemplateOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency(), opts)
|
||||
}
|
||||
|
||||
func (r *Run) Test(c TestConfigProvider) []error {
|
||||
@@ -284,23 +307,26 @@ func (r *Run) Test(c TestConfigProvider) []error {
|
||||
}
|
||||
|
||||
func (r *Run) Lint(c LintConfigProvider) []error {
|
||||
state := r.state
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
values := c.Values()
|
||||
args := argparser.GetArgs(c.Args(), state)
|
||||
args := argparser.GetArgs(c.Args(), st)
|
||||
workers := c.Concurrency()
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
|
||||
if errs := st.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return state.LintReleases(helm, values, args, workers)
|
||||
opts := &state.LintOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.LintReleases(helm, values, args, workers, opts)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
||||
@@ -82,10 +82,6 @@ func GetArgs(args string, state *state.HelmState) []string {
|
||||
}
|
||||
}
|
||||
|
||||
if state.HelmDefaults.KubeContext != "" {
|
||||
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
|
||||
}
|
||||
|
||||
var argArr []string
|
||||
|
||||
for _, flag := range argsMap.flags {
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestGetArgs(t *testing.T) {
|
||||
testState := &state.HelmState{HelmDefaults: Helmdefaults}
|
||||
receivedArgs := GetArgs(args, testState)
|
||||
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force --kube-context=test"
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force"
|
||||
|
||||
if compareArgs(expectedOutput, receivedArgs) == false {
|
||||
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
|
||||
@@ -27,7 +27,7 @@ func Test2(t *testing.T) {
|
||||
testState := &state.HelmState{HelmDefaults: Helmdefaults}
|
||||
receivedArgs := GetArgs(args, testState)
|
||||
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force --kube-context=test"
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force"
|
||||
|
||||
if compareArgs(expectedOutput, receivedArgs) == false {
|
||||
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
|
||||
|
||||
+12
-5
@@ -2,18 +2,21 @@ package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Hook struct {
|
||||
Name string `yaml:"name"`
|
||||
Events []string `yaml:"events"`
|
||||
Command string `yaml:"command"`
|
||||
Args []string `yaml:"args"`
|
||||
Name string `yaml:"name"`
|
||||
Events []string `yaml:"events"`
|
||||
Command string `yaml:"command"`
|
||||
Args []string `yaml:"args"`
|
||||
ShowLogs bool `yaml:"showlogs"`
|
||||
}
|
||||
|
||||
type event struct {
|
||||
@@ -90,6 +93,10 @@ func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error
|
||||
|
||||
bytes, err := bus.Runner.Execute(command, args, map[string]string{})
|
||||
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
|
||||
if hook.ShowLogs {
|
||||
prefix := fmt.Sprintf("\nhook[%s] logs | ", evt)
|
||||
bus.Logger.Infow(prefix + strings.ReplaceAll(string(bytes), "\n", prefix))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
|
||||
|
||||
+22
-7
@@ -2,10 +2,13 @@ package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
var logger = helmexec.NewLogger(os.Stdout, "warn")
|
||||
@@ -35,14 +38,21 @@ func TestTrigger(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"okhook1",
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}, true},
|
||||
"foo",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"okhooké",
|
||||
&Hook{"okhook2", []string{"foo"}, "ok", []string{}, false},
|
||||
"foo",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"missinghook1",
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}, false},
|
||||
"bar",
|
||||
false,
|
||||
"",
|
||||
@@ -56,14 +66,14 @@ func TestTrigger(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"nghook1",
|
||||
&Hook{"nghook1", []string{"foo"}, "ng", []string{}},
|
||||
&Hook{"nghook1", []string{"foo"}, "ng", []string{}, false},
|
||||
"foo",
|
||||
false,
|
||||
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
|
||||
},
|
||||
{
|
||||
"nghook2",
|
||||
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}},
|
||||
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}, false},
|
||||
"foo",
|
||||
false,
|
||||
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
|
||||
@@ -77,13 +87,15 @@ func TestTrigger(t *testing.T) {
|
||||
if c.hook != nil {
|
||||
hooks = append(hooks, *c.hook)
|
||||
}
|
||||
observer, observedLogs := observer.New(zap.InfoLevel)
|
||||
zeLogger := zap.New(observer).Sugar()
|
||||
bus := &Bus{
|
||||
Hooks: hooks,
|
||||
StateFilePath: "path/to/helmfile.yaml",
|
||||
BasePath: "path/to",
|
||||
Namespace: "myns",
|
||||
Env: environment.Environment{Name: "prod"},
|
||||
Logger: logger,
|
||||
Logger: zeLogger,
|
||||
ReadFile: readFile,
|
||||
}
|
||||
|
||||
@@ -109,5 +121,8 @@ func TestTrigger(t *testing.T) {
|
||||
t.Errorf("unexpected error for case \"%s\": %v", c.name, err)
|
||||
}
|
||||
}
|
||||
if observedLogs.Len() != 0 && !hooks[0].ShowLogs {
|
||||
t.Errorf("unexpected error for case \"%s\": Logs should not be created : %v", c.name, observedLogs.All())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+113
-68
@@ -17,13 +17,19 @@ const (
|
||||
command = "helm"
|
||||
)
|
||||
|
||||
type decryptedSecret struct {
|
||||
mutex sync.RWMutex
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
type execer struct {
|
||||
helmBinary string
|
||||
runner Runner
|
||||
logger *zap.SugaredLogger
|
||||
kubeContext string
|
||||
extra []string
|
||||
decryptionMutex sync.Mutex
|
||||
helmBinary string
|
||||
runner Runner
|
||||
logger *zap.SugaredLogger
|
||||
kubeContext string
|
||||
extra []string
|
||||
decryptedSecretMutex sync.Mutex
|
||||
decryptedSecrets map[string]*decryptedSecret
|
||||
}
|
||||
|
||||
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
@@ -44,14 +50,13 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
}
|
||||
|
||||
// New for running helm commands
|
||||
func New(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
func New(logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
|
||||
return &execer{
|
||||
helmBinary: command,
|
||||
logger: logger,
|
||||
kubeContext: kubeContext,
|
||||
runner: &ShellRunner{
|
||||
logger: logger,
|
||||
},
|
||||
helmBinary: command,
|
||||
logger: logger,
|
||||
kubeContext: kubeContext,
|
||||
runner: runner,
|
||||
decryptedSecrets: make(map[string]*decryptedSecret),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +68,15 @@ func (helm *execer) SetHelmBinary(bin string) {
|
||||
helm.helmBinary = bin
|
||||
}
|
||||
|
||||
func (helm *execer) AddRepo(name, repository, certfile, keyfile, username, password string) error {
|
||||
func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
|
||||
var args []string
|
||||
args = append(args, "repo", "add", name, repository)
|
||||
if certfile != "" && keyfile != "" {
|
||||
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
|
||||
}
|
||||
if cafile != "" {
|
||||
args = append(args, "--ca-file", cafile)
|
||||
}
|
||||
if username != "" && password != "" {
|
||||
args = append(args, "--username", username, "--password", password)
|
||||
}
|
||||
@@ -85,6 +93,13 @@ func (helm *execer) UpdateRepo() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) BuildDeps(name, chart string) error {
|
||||
helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateDeps(chart string) error {
|
||||
helm.logger.Infof("Updating dependency %v", chart)
|
||||
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
|
||||
@@ -92,15 +107,8 @@ func (helm *execer) UpdateDeps(chart string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) BuildDeps(chart string) error {
|
||||
helm.logger.Infof("Building dependency %v", chart)
|
||||
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Upgrading %v", chart)
|
||||
helm.logger.Infof("Upgrading release=%v, chart=%v", name, chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
|
||||
@@ -121,78 +129,105 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s
|
||||
helm.logger.Infof("Listing releases matching %v", filter)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "list", filter), flags...), env)
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
args = []string{"list", "--filter", filter}
|
||||
} else {
|
||||
args = []string{"list", filter}
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
|
||||
helm.write(out)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) {
|
||||
// Prevents https://github.com/roboll/helmfile/issues/258
|
||||
helm.decryptionMutex.Lock()
|
||||
defer helm.decryptionMutex.Unlock()
|
||||
|
||||
absPath, err := filepath.Abs(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
helm.logger.Infof("Decrypting secret %v", absPath)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
|
||||
helm.write(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
helm.logger.Debugf("Preparing to decrypt secret %v", absPath)
|
||||
helm.decryptedSecretMutex.Lock()
|
||||
|
||||
secret, ok := helm.decryptedSecrets[absPath]
|
||||
|
||||
// Cache miss
|
||||
if !ok {
|
||||
|
||||
secret = &decryptedSecret{}
|
||||
helm.decryptedSecrets[absPath] = secret
|
||||
|
||||
secret.mutex.Lock()
|
||||
defer secret.mutex.Unlock()
|
||||
helm.decryptedSecretMutex.Unlock()
|
||||
|
||||
helm.logger.Infof("Decrypting secret %v", absPath)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
|
||||
helm.info(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// HELM_SECRETS_DEC_SUFFIX is used by the helm-secrets plugin to define the output file
|
||||
decSuffix := os.Getenv("HELM_SECRETS_DEC_SUFFIX")
|
||||
if len(decSuffix) == 0 {
|
||||
decSuffix = ".yaml.dec"
|
||||
}
|
||||
decFilename := strings.Replace(absPath, ".yaml", decSuffix, 1)
|
||||
|
||||
secretBytes, err := ioutil.ReadFile(decFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret.bytes = secretBytes
|
||||
|
||||
if err := os.Remove(decFilename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
} else {
|
||||
// Cache hit
|
||||
helm.logger.Debugf("Found secret in cache %v", absPath)
|
||||
|
||||
secret.mutex.RLock()
|
||||
helm.decryptedSecretMutex.Unlock()
|
||||
defer secret.mutex.RUnlock()
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
// HELM_SECRETS_DEC_SUFFIX is used by the helm-secrets plugin to define the output file
|
||||
decSuffix := os.Getenv("HELM_SECRETS_DEC_SUFFIX")
|
||||
if len(decSuffix) == 0 {
|
||||
decSuffix = ".yaml.dec"
|
||||
}
|
||||
decFilename := strings.Replace(absPath, ".yaml", decSuffix, 1)
|
||||
|
||||
// os.Rename seems to results in "cross-device link` errors in some cases
|
||||
// Instead of moving, copy it to the destination temp file as a work-around
|
||||
// See https://github.com/roboll/helmfile/issues/251#issuecomment-417166296f
|
||||
decFile, err := os.Open(decFilename)
|
||||
_, err = tmpFile.Write(secret.bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer decFile.Close()
|
||||
|
||||
_, err = io.Copy(tmpFile, decFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := decFile.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.Remove(decFilename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile.Name(), err
|
||||
}
|
||||
|
||||
func (helm *execer) TemplateRelease(chart string, flags ...string) error {
|
||||
out, err := helm.exec(append([]string{"template", chart}, flags...), map[string]string{})
|
||||
func (helm *execer) TemplateRelease(name string, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Templating release=%v, chart=%v", name, chart)
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
args = []string{"template", name, chart}
|
||||
} else {
|
||||
args = []string{"template", chart, "--name", name}
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(args, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Comparing %v %v", name, chart)
|
||||
helm.logger.Infof("Comparing release=%v, chart=%v", name, chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
|
||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env)
|
||||
// Do our best to write STDOUT only when diff existed
|
||||
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
|
||||
detailedExitcodeEnabled := false
|
||||
@@ -216,8 +251,8 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags .
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Lint(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Linting %v", chart)
|
||||
func (helm *execer) Lint(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
@@ -243,7 +278,13 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin
|
||||
helm.logger.Infof("Testing %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "test", name), flags...), env)
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
args = []string{"test", "run", name}
|
||||
} else {
|
||||
args = []string{"test", name}
|
||||
}
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
@@ -274,3 +315,7 @@ func (helm *execer) write(out []byte) {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) isHelm3() bool {
|
||||
return os.Getenv("HELMFILE_HELM3") != ""
|
||||
}
|
||||
|
||||
+68
-29
@@ -24,8 +24,7 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
|
||||
}
|
||||
|
||||
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
execer := New(logger, kubeContext)
|
||||
execer.runner = &mockRunner{}
|
||||
execer := New(logger, kubeContext, &mockRunner{})
|
||||
return execer
|
||||
}
|
||||
|
||||
@@ -34,7 +33,9 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
func TestNewHelmExec(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(logger, "dev")
|
||||
helm := New(logger, "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
if helm.kubeContext != "dev" {
|
||||
t.Error("helmexec.New() - kubeContext")
|
||||
}
|
||||
@@ -47,7 +48,11 @@ func TestNewHelmExec(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetExtraArgs(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
helm.SetExtraArgs()
|
||||
if len(helm.extra) != 0 {
|
||||
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
|
||||
@@ -63,7 +68,11 @@ func Test_SetExtraArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetHelmBinary(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
if helm.helmBinary != "helm" {
|
||||
t.Error("helmexec.command - default command is not helm")
|
||||
}
|
||||
@@ -77,7 +86,7 @@ func Test_AddRepo(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "cert.pem", "key.pem", "", "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "")
|
||||
expected := `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev:
|
||||
@@ -87,7 +96,17 @@ exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
|
||||
@@ -97,7 +116,7 @@ exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev:
|
||||
@@ -126,7 +145,7 @@ func Test_SyncRelease(t *testing.T) {
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Upgrading chart
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||
`
|
||||
@@ -136,7 +155,7 @@ exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --
|
||||
|
||||
buffer.Reset()
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart")
|
||||
expected = `Upgrading chart
|
||||
expected = `Upgrading release=release, chart=chart
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev:
|
||||
`
|
||||
@@ -151,7 +170,7 @@ func Test_SyncReleaseTillerless(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
|
||||
"--timeout 10", "--wait")
|
||||
expected := `Upgrading chart
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||
`
|
||||
@@ -189,8 +208,8 @@ func Test_BuildDeps(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.BuildDeps("./chart/foo")
|
||||
expected := `Building dependency ./chart/foo
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected := `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm dependency build ./chart/foo --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
`
|
||||
@@ -200,8 +219,8 @@ exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.BuildDeps("./chart/foo")
|
||||
expected = `Building dependency ./chart/foo
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev:
|
||||
`
|
||||
@@ -219,10 +238,16 @@ func Test_DecryptSecret(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
}
|
||||
expected := fmt.Sprintf(`Decrypting secret %s/secretName
|
||||
// Run again for caching
|
||||
helm.DecryptSecret(HelmContext{}, "secretName")
|
||||
|
||||
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
|
||||
Decrypting secret %s/secretName
|
||||
exec: helm secrets dec %s/secretName --kube-context dev
|
||||
exec: helm secrets dec %s/secretName --kube-context dev:
|
||||
`, cwd, cwd, cwd)
|
||||
Preparing to decrypt secret %s/secretName
|
||||
Found secret in cache %s/secretName
|
||||
`, cwd, cwd, cwd, cwd, cwd, cwd)
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
@@ -233,9 +258,9 @@ func Test_DiffRelease(t *testing.T) {
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
expected := `Comparing release=release, chart=chart
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -243,9 +268,9 @@ exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --k
|
||||
|
||||
buffer.Reset()
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart")
|
||||
expected = `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
|
||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev:
|
||||
expected = `Comparing release=release, chart=chart
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -257,9 +282,9 @@ func Test_DiffReleaseTillerless(t *testing.T) {
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Comparing release chart
|
||||
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
expected := `Comparing release=release, chart=chart
|
||||
exec: helm tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -398,8 +423,8 @@ func Test_Lint(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.Lint("path/to/chart", "--values", "file.yml")
|
||||
expected := `Linting path/to/chart
|
||||
helm.Lint("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Linting release=release, chart=path/to/chart
|
||||
exec: helm lint path/to/chart --values file.yml --kube-context dev
|
||||
exec: helm lint path/to/chart --values file.yml --kube-context dev:
|
||||
`
|
||||
@@ -438,7 +463,7 @@ func Test_LogLevels(t *testing.T) {
|
||||
buffer.Reset()
|
||||
logger := NewLogger(&buffer, logLevel)
|
||||
helm := MockExecer(logger, "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password")
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
@@ -478,3 +503,17 @@ func Test_mergeEnv(t *testing.T) {
|
||||
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Template(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Templating release=release, chart=path/to/chart
|
||||
exec: helm template path/to/chart --name release --values file.yml --kube-context dev
|
||||
exec: helm template path/to/chart --name release --values file.yml --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ type Interface interface {
|
||||
SetExtraArgs(args ...string)
|
||||
SetHelmBinary(bin string)
|
||||
|
||||
AddRepo(name, repository, certfile, keyfile, username, password string) error
|
||||
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
|
||||
UpdateRepo() error
|
||||
BuildDeps(chart string) error
|
||||
BuildDeps(name, chart string) error
|
||||
UpdateDeps(chart string) error
|
||||
SyncRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
DiffRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
TemplateRelease(chart string, flags ...string) error
|
||||
TemplateRelease(name, chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
Lint(chart string, flags ...string) error
|
||||
Lint(name, chart string, flags ...string) error
|
||||
ReleaseStatus(context HelmContext, name string, flags ...string) error
|
||||
DeleteRelease(context HelmContext, name string, flags ...string) error
|
||||
TestRelease(context HelmContext, name string, flags ...string) error
|
||||
|
||||
@@ -25,7 +25,7 @@ type Runner interface {
|
||||
type ShellRunner struct {
|
||||
Dir string
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
// Execute a shell command
|
||||
@@ -33,7 +33,7 @@ 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 combinedOutput(preparedCmd, shell.logger)
|
||||
return combinedOutput(preparedCmd, shell.Logger)
|
||||
}
|
||||
|
||||
func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {
|
||||
|
||||
@@ -76,8 +76,13 @@ func Set(m map[string]interface{}, key []string, value string) map[string]interf
|
||||
|
||||
nested, ok := m[k]
|
||||
if !ok {
|
||||
new_m := map[string]interface{}{}
|
||||
nested = Set(new_m, remain, value)
|
||||
nested = map[string]interface{}{}
|
||||
}
|
||||
switch t := nested.(type) {
|
||||
case map[string]interface{}:
|
||||
nested = Set(t, remain, value)
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
||||
}
|
||||
|
||||
m[k] = nested
|
||||
|
||||
+26
-6
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-getter/helper/url"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
@@ -80,6 +82,9 @@ func (r *Remote) GetBytes(goGetterSrc string) ([]byte, error) {
|
||||
// If the argument was an URL, it fetches the remote directory contained within the URL,
|
||||
// and returns the path to the file in the fetched directory
|
||||
func (r *Remote) Locate(urlOrPath string) (string, error) {
|
||||
if r.FileExists(urlOrPath) || r.DirExists(urlOrPath) {
|
||||
return urlOrPath, nil
|
||||
}
|
||||
fetched, err := r.Fetch(urlOrPath)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
@@ -100,7 +105,7 @@ func (e InvalidURLError) Error() string {
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Getter, Scheme, Host, Dir, File, RawQuery string
|
||||
Getter, Scheme, User, Host, Dir, File, RawQuery string
|
||||
}
|
||||
|
||||
func IsRemote(goGetterSrc string) bool {
|
||||
@@ -135,6 +140,7 @@ func Parse(goGetterSrc string) (*Source, error) {
|
||||
|
||||
return &Source{
|
||||
Getter: getter,
|
||||
User: u.User.String(),
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Dir: pathComponents[0],
|
||||
@@ -154,6 +160,7 @@ func (r *Remote) Fetch(goGetterSrc string) (string, error) {
|
||||
|
||||
r.Logger.Debugf("getter: %s", u.Getter)
|
||||
r.Logger.Debugf("scheme: %s", u.Scheme)
|
||||
r.Logger.Debugf("user: %s", u.User)
|
||||
r.Logger.Debugf("host: %s", u.Host)
|
||||
r.Logger.Debugf("dir: %s", u.Dir)
|
||||
r.Logger.Debugf("file: %s", u.File)
|
||||
@@ -175,10 +182,16 @@ func (r *Remote) Fetch(goGetterSrc string) (string, error) {
|
||||
|
||||
cached := false
|
||||
|
||||
// e.g. .helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
|
||||
getterDst := filepath.Join(cacheBaseDir, cacheKey)
|
||||
|
||||
// e.g. $PWD/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
|
||||
cacheDirPath := filepath.Join(r.Home, getterDst)
|
||||
|
||||
r.Logger.Debugf("home: %s", r.Home)
|
||||
r.Logger.Debugf("getter dest: %s", getterDst)
|
||||
r.Logger.Debugf("cached dir: %s", cacheDirPath)
|
||||
|
||||
{
|
||||
if r.FileExists(cacheDirPath) {
|
||||
return "", fmt.Errorf("%s is not directory. please remove it so that variant could use it for dependency caching", getterDst)
|
||||
@@ -191,11 +204,14 @@ func (r *Remote) Fetch(goGetterSrc string) (string, error) {
|
||||
|
||||
if !cached {
|
||||
var getterSrc string
|
||||
|
||||
if len(query) == 0 {
|
||||
getterSrc = srcDir
|
||||
if u.User != "" {
|
||||
getterSrc = fmt.Sprintf("%s://%s@%s%s", u.Scheme, u.User, u.Host, u.Dir)
|
||||
} else {
|
||||
getterSrc = strings.Join([]string{srcDir, query}, "?")
|
||||
getterSrc = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
|
||||
}
|
||||
|
||||
if len(query) > 0 {
|
||||
getterSrc = strings.Join([]string{getterSrc, query}, "?")
|
||||
}
|
||||
|
||||
if u.Getter != "" {
|
||||
@@ -204,7 +220,11 @@ func (r *Remote) Fetch(goGetterSrc string) (string, error) {
|
||||
|
||||
r.Logger.Debugf("downloading %s to %s", getterSrc, getterDst)
|
||||
|
||||
if err := r.Getter.Get(r.Home, getterSrc, getterDst); err != nil {
|
||||
if err := r.Getter.Get(r.Home, getterSrc, cacheDirPath); err != nil {
|
||||
rmerr := os.RemoveAll(cacheDirPath)
|
||||
if rmerr != nil {
|
||||
return "", multierr.Append(err, rmerr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemote(t *testing.T) {
|
||||
func TestRemote_HttpsGitHub(t *testing.T) {
|
||||
cleanfs := map[string]string{
|
||||
"path/to/home": "",
|
||||
}
|
||||
@@ -85,6 +85,77 @@ func TestRemote(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_SShGitHub(t *testing.T) {
|
||||
cleanfs := map[string]string{
|
||||
"path/to/home": "",
|
||||
}
|
||||
cachefs := map[string]string{
|
||||
"path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
files map[string]string
|
||||
expectCacheHit bool
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{files: cleanfs, expectCacheHit: false},
|
||||
{files: cachefs, expectCacheHit: true},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
testfs := testhelper.NewTestFs(testcase.files)
|
||||
|
||||
hit := true
|
||||
|
||||
get := func(wd, src, dst string) error {
|
||||
if wd != "path/to/home" {
|
||||
return fmt.Errorf("unexpected wd: %s", wd)
|
||||
}
|
||||
if src != "git::ssh://git@github.com/cloudposse/helmfiles.git?ref=0.40.0" {
|
||||
return fmt.Errorf("unexpected src: %s", src)
|
||||
}
|
||||
|
||||
hit = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
getter := &testGetter{
|
||||
get: get,
|
||||
}
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Home: "path/to/home",
|
||||
Getter: getter,
|
||||
ReadFile: testfs.ReadFile,
|
||||
FileExists: testfs.FileExistsAt,
|
||||
DirExists: testfs.DirectoryExistsAt,
|
||||
}
|
||||
|
||||
url := "git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
|
||||
file, err := remote.Locate(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if file != "path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
|
||||
t.Errorf("unexpected file located: %s", file)
|
||||
}
|
||||
|
||||
if testcase.expectCacheHit && !hit {
|
||||
t.Errorf("unexpected result: unexpected cache miss")
|
||||
}
|
||||
if !testcase.expectCacheHit && hit {
|
||||
t.Errorf("unexpected result: unexpected cache hit")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testGetter struct {
|
||||
get func(wd, src, dst string) error
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package state
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/r3labs/diff"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -128,21 +129,6 @@ func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, err
|
||||
return "", fmt.Errorf("no resolved dependency found for \"%s\"", chart)
|
||||
}
|
||||
|
||||
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
parts := strings.Split(repoAndChart, "/")
|
||||
if isLocalChart(repoAndChart) {
|
||||
return "", "", false
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
panic(fmt.Sprintf("unsupported format of chart name: %s", repoAndChart))
|
||||
}
|
||||
|
||||
repo := parts[0]
|
||||
chart := parts[1]
|
||||
|
||||
return repo, chart, true
|
||||
}
|
||||
|
||||
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
|
||||
filename, unresolved, err := getUnresolvedDependenciess(st)
|
||||
if err != nil {
|
||||
@@ -293,8 +279,31 @@ func (m *chartDependencyManager) lockFileName() string {
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
if isHelm3() {
|
||||
return m.updateHelm3(shell, wd, unresolved)
|
||||
}
|
||||
return m.updateHelm2(shell, wd, unresolved)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) updateHelm3(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
// Generate `Chart.yaml` of the temporary local chart
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\n", m.Name))); err != nil {
|
||||
chartMetaContent := fmt.Sprintf("name: %s\nversion: 1.0.0\n", m.Name)
|
||||
|
||||
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
|
||||
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(chartMetaContent+string(reqsContent))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.doUpdate("Chart.lock", unresolved, shell, wd)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) updateHelm2(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
// Generate `Chart.yaml` of the temporary local chart
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\nversion: 1.0.0\n", m.Name))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -307,16 +316,21 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.doUpdate("requirements.lock", unresolved, shell, wd)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *UnresolvedDependencies, shell helmexec.DependencyUpdater, wd string) (*ResolvedDependencies, error) {
|
||||
|
||||
// Generate `requirements.lock` of the temporary local chart by coping `<basename>.lock`
|
||||
lockFile := m.lockFileName()
|
||||
|
||||
lockFileContent, err := m.readBytes(lockFile)
|
||||
originalLockFileContent, err := m.readBytes(lockFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lockFileContent != nil {
|
||||
if err := m.writeBytes(filepath.Join(wd, "requirements.lock"), lockFileContent); err != nil {
|
||||
if isHelm3() && originalLockFileContent != nil {
|
||||
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -326,7 +340,7 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, "requirements.lock"))
|
||||
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, chartLockFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -341,6 +355,24 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
|
||||
return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName
|
||||
})
|
||||
|
||||
// Don't update lock file if no dependency updated.
|
||||
if !isHelm3() && originalLockFileContent != nil {
|
||||
originalLockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes, err := diff.Diff(originalLockedReqs.ResolvedDependencies, lockedReqs.ResolvedDependencies)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
lockedReqs.Generated = originalLockedReqs.Generated
|
||||
}
|
||||
}
|
||||
|
||||
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
|
||||
|
||||
if err != nil {
|
||||
|
||||
+90
-35
@@ -4,13 +4,15 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type StateLoadError struct {
|
||||
@@ -36,13 +38,14 @@ type StateCreator struct {
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
helm helmexec.Interface
|
||||
|
||||
Strict bool
|
||||
|
||||
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
|
||||
}
|
||||
|
||||
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator {
|
||||
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error), helm helmexec.Interface) *StateCreator {
|
||||
return &StateCreator{
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
@@ -50,6 +53,7 @@ func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error)
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
helm: helm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
|
||||
|
||||
state.FilePath = file
|
||||
state.basePath = baseDir
|
||||
state.helm = c.helm
|
||||
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(content))
|
||||
if !c.Strict {
|
||||
@@ -148,6 +153,8 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.FilePath = file
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
@@ -182,7 +189,6 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
}
|
||||
|
||||
if len(envSpec.Secrets) > 0 {
|
||||
helm := helmexec.New(st.logger, "")
|
||||
|
||||
var envSecretFiles []string
|
||||
for _, urlOrPath := range envSpec.Secrets {
|
||||
@@ -196,33 +202,8 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
|
||||
envSecretFiles = append(envSecretFiles, resolved...)
|
||||
}
|
||||
|
||||
for _, path := range envSecretFiles {
|
||||
// Work-around to allow decrypting environment secrets
|
||||
//
|
||||
// We don't have releases loaded yet and therefore unable to decide whether
|
||||
// helmfile should use helm-tiller to call helm-secrets or not.
|
||||
//
|
||||
// This means that, when you use environment secrets + tillerless setup, you still need a tiller
|
||||
// installed on the cluster, just for decrypting secrets!
|
||||
// Related: https://github.com/futuresimple/helm-secrets/issues/83
|
||||
release := &ReleaseSpec{}
|
||||
flags := st.appendTillerFlags([]string{}, release)
|
||||
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := readFile(decFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err)
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err)
|
||||
}
|
||||
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to load \"%s\": %v", path, err)
|
||||
}
|
||||
if err = st.scatterGatherEnvSecretFiles(envSecretFiles, envVals, readFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else if ctxEnv == nil && name != DefaultEnv {
|
||||
@@ -244,14 +225,88 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
return newEnv, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
|
||||
var errs []error
|
||||
|
||||
inputs := envSecretFiles
|
||||
inputsSize := len(inputs)
|
||||
|
||||
type secretResult struct {
|
||||
result map[string]interface{}
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
secrets := make(chan string, inputsSize)
|
||||
results := make(chan secretResult, inputsSize)
|
||||
helm := st.helm
|
||||
|
||||
st.scatterGather(0, inputsSize,
|
||||
func() {
|
||||
for _, secretFile := range envSecretFiles {
|
||||
secrets <- secretFile
|
||||
}
|
||||
close(secrets)
|
||||
},
|
||||
func(id int) {
|
||||
for path := range secrets {
|
||||
release := &ReleaseSpec{}
|
||||
flags := st.appendConnectionFlags([]string{}, release)
|
||||
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, err, path}
|
||||
continue
|
||||
}
|
||||
bytes, err := readFile(decFile)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
continue
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
continue
|
||||
}
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
|
||||
// See https://github.com/roboll/helmfile/issues/677
|
||||
vals, err := maputil.CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
continue
|
||||
}
|
||||
results <- secretResult{vals, nil, path}
|
||||
}
|
||||
},
|
||||
func() {
|
||||
for i := 0; i < inputsSize; i++ {
|
||||
result := <-results
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
} else {
|
||||
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to load environment secrets file \"%s\": %v", result.path, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
close(results)
|
||||
},
|
||||
)
|
||||
|
||||
if len(errs) > 1 {
|
||||
for _, err := range errs {
|
||||
st.logger.Error(err)
|
||||
}
|
||||
return fmt.Errorf("Failed loading environment secrets with %d errors", len(errs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}) (map[string]interface{}, error) {
|
||||
envVals := map[string]interface{}{}
|
||||
|
||||
valuesEntries := append([]interface{}{}, entries...)
|
||||
ld := &EnvironmentValuesLoader{
|
||||
storage: st.storage(),
|
||||
readFile: st.readFile,
|
||||
}
|
||||
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger)
|
||||
var err error
|
||||
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"go.uber.org/zap"
|
||||
|
||||
. "gotest.tools/assert"
|
||||
"gotest.tools/assert/cmp"
|
||||
)
|
||||
@@ -107,7 +108,7 @@ bar: {{ readFile "bar.txt" }}
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package state
|
||||
|
||||
type EnvironmentSpec struct {
|
||||
Values []interface{} `yaml:"values"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
Values []interface{} `yaml:"values,omitempty"`
|
||||
Secrets []string `yaml:"secrets,omitempty"`
|
||||
|
||||
// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
|
||||
// under `environments.NAME.values`.
|
||||
@@ -11,5 +11,5 @@ type EnvironmentSpec struct {
|
||||
//
|
||||
// Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
|
||||
// a message about the missing file at the log-level.
|
||||
MissingFileHandler *string `yaml:"missingFileHandler"`
|
||||
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type EnvironmentValuesLoader struct {
|
||||
storage *Storage
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error)) *EnvironmentValuesLoader {
|
||||
return &EnvironmentValuesLoader{
|
||||
storage: storage,
|
||||
readFile: readFile,
|
||||
}
|
||||
}
|
||||
|
||||
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, envValues []interface{}) (map[string]interface{}, error) {
|
||||
envVals := map[string]interface{}{}
|
||||
|
||||
for _, v := range envValues {
|
||||
switch typedValue := v.(type) {
|
||||
case string:
|
||||
urlOrPath := typedValue
|
||||
resolved, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, envvalFullPath := range resolved {
|
||||
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
|
||||
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(envvalFullPath), tmplData)
|
||||
bytes, err := r.RenderToBytes(envvalFullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
|
||||
}
|
||||
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFullPath, err)
|
||||
}
|
||||
}
|
||||
case map[interface{}]interface{}:
|
||||
m, err := maputil.CastKeysToStrings(typedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", typedValue, err)
|
||||
}
|
||||
continue
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return envVals, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type EnvironmentValuesLoader struct {
|
||||
storage *Storage
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger) *EnvironmentValuesLoader {
|
||||
return &EnvironmentValuesLoader{
|
||||
storage: storage,
|
||||
readFile: readFile,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []interface{}) (map[string]interface{}, error) {
|
||||
result := map[string]interface{}{}
|
||||
|
||||
for _, entry := range valuesEntries {
|
||||
maps := []interface{}{}
|
||||
|
||||
switch strOrMap := entry.(type) {
|
||||
case string:
|
||||
urlOrPath := strOrMap
|
||||
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
|
||||
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
|
||||
bytes, err := r.RenderToBytes(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
|
||||
}
|
||||
maps = append(maps, m)
|
||||
if ld.logger != nil {
|
||||
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
|
||||
}
|
||||
}
|
||||
case map[interface{}]interface{}:
|
||||
maps = append(maps, strOrMap)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
|
||||
}
|
||||
for _, m := range maps {
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
|
||||
// See https://github.com/roboll/helmfile/issues/677
|
||||
vals, err := maputil.CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&result, &vals, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package state
|
||||
|
||||
type Dependency struct {
|
||||
Chart string `yaml:"chart"`
|
||||
Version string `yaml:"version"`
|
||||
Alias string `yaml:"alias"`
|
||||
}
|
||||
|
||||
func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) ([]string, error) {
|
||||
for _, d := range release.Dependencies {
|
||||
var dep string
|
||||
if d.Alias != "" {
|
||||
dep += d.Alias + "="
|
||||
}
|
||||
dep += d.Chart
|
||||
if d.Version != "" {
|
||||
dep += ":" + d.Version
|
||||
}
|
||||
flags = append(flags, "--dependency", dep)
|
||||
}
|
||||
|
||||
for _, adopt := range release.Adopt {
|
||||
flags = append(flags, "--adopt", adopt)
|
||||
}
|
||||
|
||||
jsonPatches := release.JSONPatches
|
||||
if len(jsonPatches) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
flags = append(flags, "--json-patch", f)
|
||||
}
|
||||
|
||||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
}
|
||||
|
||||
strategicMergePatches := release.StrategicMergePatches
|
||||
if len(strategicMergePatches) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
flags = append(flags, "--strategic-merge-patch", f)
|
||||
}
|
||||
|
||||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
@@ -39,6 +40,86 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ts := result.Version
|
||||
result.Version, err = renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
}
|
||||
|
||||
if result.WaitTemplate != nil {
|
||||
ts := *result.WaitTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.WaitTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.InstalledTemplate != nil {
|
||||
ts := *result.InstalledTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.InstalledTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.TillerlessTemplate != nil {
|
||||
ts := *result.TillerlessTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.TillerlessTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.VerifyTemplate != nil {
|
||||
ts := *result.VerifyTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.VerifyTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
for key, val := range result.Labels {
|
||||
ts := val
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".labels[%s] = \"%s\": %v", r.Name, key, ts, err)
|
||||
}
|
||||
result.Labels[key] = s.String()
|
||||
}
|
||||
|
||||
if result.ValuesTemplate != nil && len(result.ValuesTemplate) > 0 {
|
||||
for i, t := range result.ValuesTemplate {
|
||||
switch ts := t.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
serialized, err := yaml.Marshal(ts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(serialized))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, serialized, err)
|
||||
}
|
||||
|
||||
var deserialized map[interface{}]interface{}
|
||||
|
||||
if err := yaml.Unmarshal(s.Bytes(), &deserialized); err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
|
||||
result.ValuesTemplate[i] = deserialized
|
||||
}
|
||||
}
|
||||
|
||||
result.Values = result.ValuesTemplate
|
||||
}
|
||||
|
||||
for i, t := range result.Values {
|
||||
switch ts := t.(type) {
|
||||
case string:
|
||||
@@ -58,6 +139,48 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
result.Secrets[i] = s.String()
|
||||
}
|
||||
|
||||
if result.SetValuesTemplate != nil && len(result.SetValuesTemplate) > 0 {
|
||||
for i, val := range result.SetValuesTemplate {
|
||||
{
|
||||
// name
|
||||
ts := val.Name
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].name = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Name = s.String()
|
||||
}
|
||||
{
|
||||
// value
|
||||
ts := val.Value
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].value = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Value = s.String()
|
||||
}
|
||||
{
|
||||
// file
|
||||
ts := val.File
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].file = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].File = s.String()
|
||||
}
|
||||
for j, ts := range val.Values {
|
||||
// values
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Values[j] = s.String()
|
||||
}
|
||||
}
|
||||
|
||||
result.SetValues = result.SetValuesTemplate
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
+332
-113
@@ -1,8 +1,11 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
@@ -10,6 +13,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/event"
|
||||
@@ -30,23 +34,23 @@ type HelmState struct {
|
||||
FilePath string
|
||||
|
||||
// DefaultValues is the default values to be overrode by environment values and command-line overrides
|
||||
DefaultValues []interface{} `yaml:"values"`
|
||||
DefaultValues []interface{} `yaml:"values,omitempty"`
|
||||
|
||||
Environments map[string]EnvironmentSpec `yaml:"environments"`
|
||||
Environments map[string]EnvironmentSpec `yaml:"environments,omitempty"`
|
||||
|
||||
Bases []string `yaml:"bases"`
|
||||
HelmDefaults HelmSpec `yaml:"helmDefaults"`
|
||||
Helmfiles []SubHelmfileSpec `yaml:"helmfiles"`
|
||||
DeprecatedContext string `yaml:"context"`
|
||||
DeprecatedReleases []ReleaseSpec `yaml:"charts"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Repositories []RepositorySpec `yaml:"repositories"`
|
||||
Releases []ReleaseSpec `yaml:"releases"`
|
||||
Selectors []string
|
||||
Bases []string `yaml:"bases,omitempty"`
|
||||
HelmDefaults HelmSpec `yaml:"helmDefaults,omitempty"`
|
||||
Helmfiles []SubHelmfileSpec `yaml:"helmfiles,omitempty"`
|
||||
DeprecatedContext string `yaml:"context,omitempty"`
|
||||
DeprecatedReleases []ReleaseSpec `yaml:"charts,omitempty"`
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
Repositories []RepositorySpec `yaml:"repositories,omitempty"`
|
||||
Releases []ReleaseSpec `yaml:"releases,omitempty"`
|
||||
Selectors []string `yaml:"-"`
|
||||
|
||||
Templates map[string]TemplateSpec `yaml:"templates"`
|
||||
|
||||
Env environment.Environment
|
||||
Env environment.Environment `yaml:"-"`
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
@@ -58,27 +62,31 @@ type HelmState struct {
|
||||
tempDir func(string, string) (string, error)
|
||||
|
||||
runner helmexec.Runner
|
||||
helm helmexec.Interface
|
||||
}
|
||||
|
||||
// SubHelmfileSpec defines the subhelmfile path and options
|
||||
type SubHelmfileSpec struct {
|
||||
Path string //path or glob pattern for the sub helmfiles
|
||||
Selectors []string //chosen selectors for the sub helmfiles
|
||||
SelectorsInherited bool //do the sub helmfiles inherits from parent selectors
|
||||
//path or glob pattern for the sub helmfiles
|
||||
Path string `yaml:"path,omitempty"`
|
||||
//chosen selectors for the sub helmfiles
|
||||
Selectors []string `yaml:"selectors,omitempty"`
|
||||
//do the sub helmfiles inherits from parent selectors
|
||||
SelectorsInherited bool `yaml:"selectorsInherited,omitempty"`
|
||||
|
||||
Environment SubhelmfileEnvironmentSpec
|
||||
}
|
||||
|
||||
type SubhelmfileEnvironmentSpec struct {
|
||||
OverrideValues []interface{} `yaml:"values"`
|
||||
OverrideValues []interface{} `yaml:"values,omitempty"`
|
||||
}
|
||||
|
||||
// HelmSpec to defines helmDefault values
|
||||
type HelmSpec struct {
|
||||
KubeContext string `yaml:"kubeContext"`
|
||||
TillerNamespace string `yaml:"tillerNamespace"`
|
||||
KubeContext string `yaml:"kubeContext,omitempty"`
|
||||
TillerNamespace string `yaml:"tillerNamespace,omitempty"`
|
||||
Tillerless bool `yaml:"tillerless"`
|
||||
Args []string `yaml:"args"`
|
||||
Args []string `yaml:"args,omitempty"`
|
||||
Verify bool `yaml:"verify"`
|
||||
// Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0'
|
||||
Devel bool `yaml:"devel"`
|
||||
@@ -94,69 +102,87 @@ type HelmSpec struct {
|
||||
Atomic bool `yaml:"atomic"`
|
||||
|
||||
TLS bool `yaml:"tls"`
|
||||
TLSCACert string `yaml:"tlsCACert"`
|
||||
TLSKey string `yaml:"tlsKey"`
|
||||
TLSCert string `yaml:"tlsCert"`
|
||||
TLSCACert string `yaml:"tlsCACert,omitempty"`
|
||||
TLSKey string `yaml:"tlsKey,omitempty"`
|
||||
TLSCert string `yaml:"tlsCert,omitempty"`
|
||||
}
|
||||
|
||||
// RepositorySpec that defines values for a helm repo
|
||||
type RepositorySpec struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
CertFile string `yaml:"certFile"`
|
||||
KeyFile string `yaml:"keyFile"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
CaFile string `yaml:"caFile,omitempty"`
|
||||
CertFile string `yaml:"certFile,omitempty"`
|
||||
KeyFile string `yaml:"keyFile,omitempty"`
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
// ReleaseSpec defines the structure of a helm release
|
||||
type ReleaseSpec struct {
|
||||
// Chart is the name of the chart being installed to create this release
|
||||
Chart string `yaml:"chart"`
|
||||
Version string `yaml:"version"`
|
||||
Verify *bool `yaml:"verify"`
|
||||
Chart string `yaml:"chart,omitempty"`
|
||||
Version string `yaml:"version,omitempty"`
|
||||
Verify *bool `yaml:"verify,omitempty"`
|
||||
// Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0'
|
||||
Devel *bool `yaml:"devel"`
|
||||
Devel *bool `yaml:"devel,omitempty"`
|
||||
// Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful
|
||||
Wait *bool `yaml:"wait"`
|
||||
Wait *bool `yaml:"wait,omitempty"`
|
||||
// Timeout is the time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
|
||||
Timeout *int `yaml:"timeout"`
|
||||
Timeout *int `yaml:"timeout,omitempty"`
|
||||
// RecreatePods, when set to true, instruct helmfile to perform pods restart for the resource if applicable
|
||||
RecreatePods *bool `yaml:"recreatePods"`
|
||||
RecreatePods *bool `yaml:"recreatePods,omitempty"`
|
||||
// Force, when set to true, forces resource update through delete/recreate if needed
|
||||
Force *bool `yaml:"force"`
|
||||
Force *bool `yaml:"force,omitempty"`
|
||||
// Installed, when set to true, `delete --purge` the release
|
||||
Installed *bool `yaml:"installed"`
|
||||
Installed *bool `yaml:"installed,omitempty"`
|
||||
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
|
||||
Atomic *bool `yaml:"atomic"`
|
||||
Atomic *bool `yaml:"atomic,omitempty"`
|
||||
|
||||
// MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
|
||||
// The default value for MissingFileHandler is "Error".
|
||||
MissingFileHandler *string `yaml:"missingFileHandler"`
|
||||
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
|
||||
|
||||
// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
|
||||
Hooks []event.Hook `yaml:"hooks"`
|
||||
Hooks []event.Hook `yaml:"hooks,omitempty"`
|
||||
|
||||
// Name is the name of this release
|
||||
Name string `yaml:"name"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
Values []interface{} `yaml:"values"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
SetValues []SetValue `yaml:"set"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty"`
|
||||
Values []interface{} `yaml:"values,omitempty"`
|
||||
Secrets []string `yaml:"secrets,omitempty"`
|
||||
SetValues []SetValue `yaml:"set,omitempty"`
|
||||
|
||||
ValuesTemplate []interface{} `yaml:"valuesTemplate,omitempty"`
|
||||
SetValuesTemplate []SetValue `yaml:"setTemplate,omitempty"`
|
||||
|
||||
// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
|
||||
EnvValues []SetValue `yaml:"env"`
|
||||
EnvValues []SetValue `yaml:"env,omitempty"`
|
||||
|
||||
ValuesPathPrefix string `yaml:"valuesPathPrefix"`
|
||||
ValuesPathPrefix string `yaml:"valuesPathPrefix,omitempty"`
|
||||
|
||||
TillerNamespace string `yaml:"tillerNamespace"`
|
||||
Tillerless *bool `yaml:"tillerless"`
|
||||
TillerNamespace string `yaml:"tillerNamespace,omitempty"`
|
||||
Tillerless *bool `yaml:"tillerless,omitempty"`
|
||||
|
||||
TLS *bool `yaml:"tls"`
|
||||
TLSCACert string `yaml:"tlsCACert"`
|
||||
TLSKey string `yaml:"tlsKey"`
|
||||
TLSCert string `yaml:"tlsCert"`
|
||||
KubeContext string `yaml:"kubeContext,omitempty"`
|
||||
|
||||
TLS *bool `yaml:"tls,omitempty"`
|
||||
TLSCACert string `yaml:"tlsCACert,omitempty"`
|
||||
TLSKey string `yaml:"tlsKey,omitempty"`
|
||||
TLSCert string `yaml:"tlsCert,omitempty"`
|
||||
|
||||
// These values are used in templating
|
||||
TillerlessTemplate *string `yaml:"tillerlessTemplate,omitempty"`
|
||||
VerifyTemplate *string `yaml:"verifyTemplate,omitempty"`
|
||||
WaitTemplate *string `yaml:"waitTemplate,omitempty"`
|
||||
InstalledTemplate *string `yaml:"installedTemplate,omitempty"`
|
||||
|
||||
// These settings requires helm-x integration to work
|
||||
Dependencies []Dependency `yaml:"dependencies,omitempty"`
|
||||
JSONPatches []interface{} `yaml:"jsonPatches,omitempty"`
|
||||
StrategicMergePatches []interface{} `yaml:"strategicMergePatches,omitempty"`
|
||||
Adopt []string `yaml:"adopt,omitempty"`
|
||||
|
||||
// generatedValues are values that need cleaned up on exit
|
||||
generatedValues []string
|
||||
@@ -166,10 +192,10 @@ type ReleaseSpec struct {
|
||||
|
||||
// SetValue are the key values to set on a helm release
|
||||
type SetValue struct {
|
||||
Name string `yaml:"name"`
|
||||
Value string `yaml:"value"`
|
||||
File string `yaml:"file"`
|
||||
Values []string `yaml:"values"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Values []string `yaml:"values,omitempty"`
|
||||
}
|
||||
|
||||
// AffectedReleases hold the list of released that where updated, deleted, or in error
|
||||
@@ -193,7 +219,7 @@ func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
|
||||
}
|
||||
|
||||
type RepoUpdater interface {
|
||||
AddRepo(name, repository, certfile, keyfile, username, password string) error
|
||||
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
|
||||
UpdateRepo() error
|
||||
}
|
||||
|
||||
@@ -202,7 +228,7 @@ func (st *HelmState) SyncRepos(helm RepoUpdater) []error {
|
||||
errs := []error{}
|
||||
|
||||
for _, repo := range st.Repositories {
|
||||
if err := helm.AddRepo(repo.Name, repo.URL, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil {
|
||||
if err := helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, repo.Username, repo.Password); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
@@ -228,7 +254,12 @@ type syncPrepareResult struct {
|
||||
}
|
||||
|
||||
// SyncReleases wrapper for executing helm upgrade on the releases
|
||||
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
|
||||
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int, opt ...SyncOpt) ([]syncPrepareResult, []error) {
|
||||
opts := &SyncOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
releases := []*ReleaseSpec{}
|
||||
for i, _ := range st.Releases {
|
||||
releases = append(releases, &st.Releases[i])
|
||||
@@ -241,6 +272,8 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
res := []syncPrepareResult{}
|
||||
errs := []error{}
|
||||
|
||||
mut := sync.Mutex{}
|
||||
|
||||
st.scatterGather(
|
||||
concurrency,
|
||||
numReleases,
|
||||
@@ -265,7 +298,11 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO We need a long-term fix for this :)
|
||||
// See https://github.com/roboll/helmfile/issues/737
|
||||
mut.Lock()
|
||||
flags, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
|
||||
mut.Unlock()
|
||||
if flagsErr != nil {
|
||||
results <- syncPrepareResult{errors: []*ReleaseError{newReleaseError(release, flagsErr)}}
|
||||
continue
|
||||
@@ -287,6 +324,12 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
flags = append(flags, "--values", valfile)
|
||||
}
|
||||
|
||||
if opts.Set != nil {
|
||||
for _, s := range opts.Set {
|
||||
flags = append(flags, "--set", s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
results <- syncPrepareResult{errors: errs}
|
||||
continue
|
||||
@@ -313,7 +356,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
}
|
||||
|
||||
func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) {
|
||||
out, err := helm.List(context, "^"+release.Name+"$", st.tillerFlags(&release)...)
|
||||
out, err := st.listReleases(context, helm, &release)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if out != "" {
|
||||
@@ -341,9 +384,24 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
|
||||
return detected, nil
|
||||
}
|
||||
|
||||
type SyncOpts struct {
|
||||
Set []string
|
||||
}
|
||||
|
||||
type SyncOpt interface{ Apply(*SyncOpts) }
|
||||
|
||||
func (o *SyncOpts) Apply(opts *SyncOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
// SyncReleases wrapper for executing helm upgrade on the releases
|
||||
func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
|
||||
preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit)
|
||||
func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error {
|
||||
opts := &SyncOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts)
|
||||
if len(prepErrs) > 0 {
|
||||
return prepErrs
|
||||
}
|
||||
@@ -376,7 +434,8 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
|
||||
if err != nil {
|
||||
relErr = newReleaseError(release, err)
|
||||
} else if installed {
|
||||
if err := helm.DeleteRelease(context, release.Name, "--purge"); err != nil {
|
||||
deletionFlags := st.appendConnectionFlags([]string{"--purge"}, release)
|
||||
if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
|
||||
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||
relErr = newReleaseError(release, err)
|
||||
} else {
|
||||
@@ -431,9 +490,17 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
|
||||
flags := st.connectionFlags(release)
|
||||
if isHelm3() && release.Namespace != "" {
|
||||
flags = append(flags, "--namespace", release.Namespace)
|
||||
}
|
||||
return helm.List(context, "^"+release.Name+"$", flags...)
|
||||
}
|
||||
|
||||
func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
|
||||
//retrieve the version
|
||||
if out, err := helm.List(context, "^"+release.Name+"$", st.tillerFlags(release)...); err == nil {
|
||||
if out, err := st.listReleases(context, helm, release); err == nil {
|
||||
chartName := filepath.Base(release.Chart)
|
||||
//the regexp without escapes : .*\s.*\s.*\s.*\schartName-(.*?)\s
|
||||
pat := regexp.MustCompile(".*\\s.*\\s.*\\s.*\\s" + chartName + "-(.*?)\\s")
|
||||
@@ -518,8 +585,23 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
type TemplateOpts struct {
|
||||
Set []string
|
||||
}
|
||||
|
||||
type TemplateOpt interface{ Apply(*TemplateOpts) }
|
||||
|
||||
func (o *TemplateOpts) Apply(opts *TemplateOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
// TemplateReleases wrapper for executing helm template on the releases
|
||||
func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error {
|
||||
func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, additionalValues []string, args []string, workerLimit int, opt ...TemplateOpt) []error {
|
||||
opts := &TemplateOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
|
||||
helm.SetExtraArgs()
|
||||
|
||||
@@ -550,10 +632,13 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
|
||||
continue
|
||||
}
|
||||
|
||||
st.applyDefaultsTo(&release)
|
||||
|
||||
flags, err := st.flagsForTemplate(helm, &release, 0)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
for _, value := range additionalValues {
|
||||
valfile, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
@@ -566,8 +651,25 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
|
||||
flags = append(flags, "--values", valfile)
|
||||
}
|
||||
|
||||
if opts.Set != nil {
|
||||
for _, s := range opts.Set {
|
||||
flags = append(flags, "--set", s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outputDir) > 0 {
|
||||
releaseOutputDir, err := st.GenerateOutputDir(outputDir, release)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
flags = append(flags, "--output-dir", releaseOutputDir)
|
||||
st.logger.Debugf("Generating templates to : %s\n", releaseOutputDir)
|
||||
os.Mkdir(releaseOutputDir, 0755)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
if err := helm.TemplateRelease(temp[release.Name], flags...); err != nil {
|
||||
if err := helm.TemplateRelease(release.Name, temp[release.Name], flags...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
@@ -584,8 +686,23 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
|
||||
return nil
|
||||
}
|
||||
|
||||
type LintOpts struct {
|
||||
Set []string
|
||||
}
|
||||
|
||||
type LintOpt interface{ Apply(*LintOpts) }
|
||||
|
||||
func (o *LintOpts) Apply(opts *LintOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
// LintReleases wrapper for executing helm lint on the releases
|
||||
func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error {
|
||||
func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int, opt ...LintOpt) []error {
|
||||
opts := &LintOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
|
||||
helm.SetExtraArgs()
|
||||
|
||||
@@ -631,8 +748,14 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
|
||||
flags = append(flags, "--values", valfile)
|
||||
}
|
||||
|
||||
if opts.Set != nil {
|
||||
for _, s := range opts.Set {
|
||||
flags = append(flags, "--set", s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
if err := helm.Lint(temp[release.Name], flags...); err != nil {
|
||||
if err := helm.Lint(release.Name, temp[release.Name], flags...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
@@ -659,7 +782,12 @@ type diffPrepareResult struct {
|
||||
errors []*ReleaseError
|
||||
}
|
||||
|
||||
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) {
|
||||
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool, opt ...DiffOpt) ([]diffPrepareResult, []error) {
|
||||
opts := &DiffOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
releases := []*ReleaseSpec{}
|
||||
for i, _ := range st.Releases {
|
||||
if !st.Releases[i].Desired() {
|
||||
@@ -675,6 +803,8 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
|
||||
rs := []diffPrepareResult{}
|
||||
errs := []error{}
|
||||
|
||||
mut := sync.Mutex{}
|
||||
|
||||
st.scatterGather(
|
||||
concurrency,
|
||||
numReleases,
|
||||
@@ -690,7 +820,11 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
|
||||
|
||||
st.applyDefaultsTo(release)
|
||||
|
||||
// TODO We need a long-term fix for this :)
|
||||
// See https://github.com/roboll/helmfile/issues/737
|
||||
mut.Lock()
|
||||
flags, err := st.flagsForDiff(helm, release, workerIndex)
|
||||
mut.Unlock()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -715,6 +849,20 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
|
||||
flags = append(flags, "--suppress-secrets")
|
||||
}
|
||||
|
||||
if opts.NoColor {
|
||||
flags = append(flags, "--no-color")
|
||||
}
|
||||
|
||||
if opts.Context > 0 {
|
||||
flags = append(flags, "--context", fmt.Sprintf("%d", opts.Context))
|
||||
}
|
||||
|
||||
if opts.Set != nil {
|
||||
for _, s := range opts.Set {
|
||||
flags = append(flags, "--set", s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
rsErrs := make([]*ReleaseError, len(errs))
|
||||
for i, e := range errs {
|
||||
@@ -760,10 +908,27 @@ func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helme
|
||||
}
|
||||
}
|
||||
|
||||
type DiffOpts struct {
|
||||
Context int
|
||||
NoColor bool
|
||||
Set []string
|
||||
}
|
||||
|
||||
func (o *DiffOpts) Apply(opts *DiffOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
type DiffOpt interface{ Apply(*DiffOpts) }
|
||||
|
||||
// DiffReleases wrapper for executing helm diff on the releases
|
||||
// It returns releases that had any changes
|
||||
func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool) ([]*ReleaseSpec, []error) {
|
||||
preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets)
|
||||
func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool, triggerCleanupEvents bool, opt ...DiffOpt) ([]*ReleaseSpec, []error) {
|
||||
opts := &DiffOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets, opts)
|
||||
if len(prepErrs) > 0 {
|
||||
return []*ReleaseSpec{}, prepErrs
|
||||
}
|
||||
@@ -830,24 +995,24 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
flags = st.appendConnectionFlags(flags, &release)
|
||||
|
||||
return helm.ReleaseStatus(st.createHelmContext(&release, workerIndex), release.Name, flags...)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReleases wrapper for executing helm delete on the releases
|
||||
func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, purge bool) []error {
|
||||
return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec, workerIndex int) error {
|
||||
func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error {
|
||||
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
|
||||
if !release.Desired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
if purge {
|
||||
if purge && !isHelm3() {
|
||||
flags = append(flags, "--purge")
|
||||
}
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
flags = st.appendConnectionFlags(flags, &release)
|
||||
context := st.createHelmContext(&release, workerIndex)
|
||||
|
||||
installed, err := st.isReleaseInstalled(context, helm, release)
|
||||
@@ -878,13 +1043,21 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
|
||||
if cleanup {
|
||||
flags = append(flags, "--cleanup")
|
||||
}
|
||||
flags = append(flags, "--timeout", strconv.Itoa(timeout))
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
duration := strconv.Itoa(timeout)
|
||||
if isHelm3() {
|
||||
duration += "s"
|
||||
}
|
||||
flags = append(flags, "--timeout", duration)
|
||||
flags = st.appendConnectionFlags(flags, &release)
|
||||
|
||||
return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...)
|
||||
})
|
||||
}
|
||||
|
||||
func isHelm3() bool {
|
||||
return os.Getenv("HELMFILE_HELM3") != ""
|
||||
}
|
||||
|
||||
// Clean will remove any generated secrets
|
||||
func (st *HelmState) Clean() []error {
|
||||
errs := []error{}
|
||||
@@ -1011,7 +1184,7 @@ func (st *HelmState) ResolveDeps() (*HelmState, error) {
|
||||
|
||||
// UpdateDeps wrapper for updating dependencies on the releases
|
||||
func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
|
||||
errs := []error{}
|
||||
var errs []error
|
||||
|
||||
for _, release := range st.Releases {
|
||||
if isLocalChart(release.Chart) {
|
||||
@@ -1044,7 +1217,7 @@ func (st *HelmState) BuildDeps(helm helmexec.Interface) []error {
|
||||
|
||||
for _, release := range st.Releases {
|
||||
if isLocalChart(release.Chart) {
|
||||
if err := helm.BuildDeps(normalizeChart(st.basePath, release.Chart)); err != nil {
|
||||
if err := helm.BuildDeps(release.Name, normalizeChart(st.basePath, release.Chart)); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
@@ -1055,27 +1228,6 @@ func (st *HelmState) BuildDeps(helm helmexec.Interface) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeChart allows for the distinction between a file path reference and repository references.
|
||||
// - Any single (or double character) followed by a `/` will be considered a local file reference and
|
||||
// be constructed relative to the `base path`.
|
||||
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
|
||||
func normalizeChart(basePath, chart string) string {
|
||||
if !isLocalChart(chart) {
|
||||
return chart
|
||||
}
|
||||
return filepath.Join(basePath, chart)
|
||||
}
|
||||
|
||||
func isLocalChart(chart string) bool {
|
||||
regex, _ := regexp.Compile("^[.]?./")
|
||||
matched := regex.MatchString(chart)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
return chart == "" || chart[0] == '/' || len(strings.Split(chart, "/")) != 2
|
||||
}
|
||||
|
||||
func pathExists(chart string) bool {
|
||||
_, err := os.Stat(chart)
|
||||
return err == nil
|
||||
@@ -1111,15 +1263,16 @@ func findChartDirectory(topLevelDir string) (string, error) {
|
||||
return topLevelDir, errors.New("No Chart.yaml found")
|
||||
}
|
||||
|
||||
func (st *HelmState) appendTillerFlags(flags []string, release *ReleaseSpec) []string {
|
||||
adds := st.tillerFlags(release)
|
||||
// appendConnectionFlags append all the helm command-line flags related to K8s API and Tiller connection including the kubecontext
|
||||
func (st *HelmState) appendConnectionFlags(flags []string, release *ReleaseSpec) []string {
|
||||
adds := st.connectionFlags(release)
|
||||
for _, a := range adds {
|
||||
flags = append(flags, a)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (st *HelmState) tillerFlags(release *ReleaseSpec) []string {
|
||||
func (st *HelmState) connectionFlags(release *ReleaseSpec) []string {
|
||||
flags := []string{}
|
||||
tillerless := st.HelmDefaults.Tillerless
|
||||
if release.Tillerless != nil {
|
||||
@@ -1153,6 +1306,12 @@ func (st *HelmState) tillerFlags(release *ReleaseSpec) []string {
|
||||
} else if st.HelmDefaults.TLSCACert != "" {
|
||||
flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
|
||||
}
|
||||
|
||||
if release.KubeContext != "" {
|
||||
flags = append(flags, "--kube-context", release.KubeContext)
|
||||
} else if st.HelmDefaults.KubeContext != "" {
|
||||
flags = append(flags, "--kube-context", st.HelmDefaults.KubeContext)
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
@@ -1196,7 +1355,13 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
|
||||
flags = append(flags, "--atomic")
|
||||
}
|
||||
|
||||
flags = st.appendTillerFlags(flags, release)
|
||||
flags = st.appendConnectionFlags(flags, release)
|
||||
|
||||
var err error
|
||||
flags, err = st.appendHelmXFlags(flags, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
if err != nil {
|
||||
@@ -1206,9 +1371,14 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
|
||||
}
|
||||
|
||||
func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
|
||||
flags := []string{
|
||||
"--name", release.Name,
|
||||
flags := []string{}
|
||||
|
||||
var err error
|
||||
flags, err = st.appendHelmXFlags(flags, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1226,7 +1396,13 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
|
||||
flags = append(flags, "--devel")
|
||||
}
|
||||
|
||||
flags = st.appendTillerFlags(flags, release)
|
||||
flags = st.appendConnectionFlags(flags, release)
|
||||
|
||||
var err error
|
||||
flags, err = st.appendHelmXFlags(flags, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
if err != nil {
|
||||
@@ -1245,7 +1421,17 @@ func (st *HelmState) isDevelopment(release *ReleaseSpec) bool {
|
||||
}
|
||||
|
||||
func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
|
||||
return st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
flags, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flags, err = st.appendHelmXFlags(flags, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
|
||||
@@ -1275,7 +1461,7 @@ func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) {
|
||||
return nil, err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("no file matching %s found", hf.Path)
|
||||
continue
|
||||
}
|
||||
for _, match := range matches {
|
||||
newHelmfile := hf
|
||||
@@ -1383,7 +1569,7 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
|
||||
}
|
||||
path := paths[0]
|
||||
|
||||
decryptFlags := st.appendTillerFlags([]string{}, release)
|
||||
decryptFlags := st.appendConnectionFlags([]string{}, release)
|
||||
valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1514,3 +1700,36 @@ func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) GenerateOutputDir(outputDir string, release ReleaseSpec) (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)]
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(stateFileName)
|
||||
sb.WriteString("-")
|
||||
sb.WriteString(hex.EncodeToString(hasher.Sum(nil))[:8])
|
||||
sb.WriteString("-")
|
||||
sb.WriteString(release.Name)
|
||||
|
||||
return path.Join(outputDir, sb.String()), nil
|
||||
}
|
||||
|
||||
func (st *HelmState) ToYaml() (string, error) {
|
||||
if result, err := yaml.Marshal(st); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(result), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (st *HelmState) Values() (map[string]interface{}, error) {
|
||||
@@ -41,6 +44,55 @@ func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolRefFromStringTemplate(templateRef string) (*bool, error) {
|
||||
var result bool
|
||||
if err := yaml.Unmarshal([]byte(templateRef), &result); err != nil {
|
||||
return nil, fmt.Errorf("failed deserialising string %s: %v", templateRef, err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func updateBoolTemplatedValues(r *ReleaseSpec) error {
|
||||
|
||||
if r.InstalledTemplate != nil {
|
||||
if installed, err := getBoolRefFromStringTemplate(*r.InstalledTemplate); err != nil {
|
||||
return fmt.Errorf("installedTemplate: %v", err)
|
||||
} else {
|
||||
r.InstalledTemplate = nil
|
||||
r.Installed = installed
|
||||
}
|
||||
}
|
||||
|
||||
if r.WaitTemplate != nil {
|
||||
if wait, err := getBoolRefFromStringTemplate(*r.WaitTemplate); err != nil {
|
||||
return fmt.Errorf("waitTemplate: %v", err)
|
||||
} else {
|
||||
r.WaitTemplate = nil
|
||||
r.Wait = wait
|
||||
}
|
||||
}
|
||||
|
||||
if r.TillerlessTemplate != nil {
|
||||
if tillerless, err := getBoolRefFromStringTemplate(*r.TillerlessTemplate); err != nil {
|
||||
return fmt.Errorf("tillerlessTemplate: %v", err)
|
||||
} else {
|
||||
r.TillerlessTemplate = nil
|
||||
r.Tillerless = tillerless
|
||||
}
|
||||
}
|
||||
|
||||
if r.VerifyTemplate != nil {
|
||||
if verify, err := getBoolRefFromStringTemplate(*r.VerifyTemplate); err != nil {
|
||||
return fmt.Errorf("verifyTemplate: %v", err)
|
||||
} else {
|
||||
r.VerifyTemplate = nil
|
||||
r.Verify = verify
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
||||
r := *st
|
||||
|
||||
@@ -50,17 +102,32 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
||||
}
|
||||
|
||||
for i, rt := range st.Releases {
|
||||
tmplData := releaseTemplateData{
|
||||
Environment: st.Env,
|
||||
Release: rt,
|
||||
Values: vals,
|
||||
successFlag := false
|
||||
for it, prev := 0, &rt; it < 6; it++ {
|
||||
tmplData := releaseTemplateData{
|
||||
Environment: st.Env,
|
||||
Release: *prev,
|
||||
Values: vals,
|
||||
}
|
||||
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
|
||||
r, err := rt.ExecuteTemplateExpressions(renderer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
|
||||
}
|
||||
if reflect.DeepEqual(prev, r) {
|
||||
successFlag = true
|
||||
if err := updateBoolTemplatedValues(r); err != nil {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
|
||||
}
|
||||
st.Releases[i] = *r
|
||||
break
|
||||
}
|
||||
prev = r
|
||||
}
|
||||
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
|
||||
r, err := rt.ExecuteTemplateExpressions(renderer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
|
||||
if !successFlag {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %s", st.FilePath, rt.Name,
|
||||
"recursive references can't be resolved")
|
||||
}
|
||||
st.Releases[i] = *r
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
)
|
||||
|
||||
func boolPtrToString(ptr *bool) string {
|
||||
if ptr == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("&%t", *ptr)
|
||||
}
|
||||
|
||||
func ptr(v interface{}) interface{} {
|
||||
r := v
|
||||
return reflect.ValueOf(r).Addr().Interface()
|
||||
}
|
||||
|
||||
func TestHelmState_executeTemplates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -13,24 +29,103 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
want ReleaseSpec
|
||||
}{
|
||||
{
|
||||
name: "Has template expressions in chart, values, and secrets",
|
||||
name: "Has template expressions in chart, values, secrets, version, labels",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/{{ .Release.Name }}",
|
||||
Version: "0.1",
|
||||
Verify: nil,
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-{{ .Release.Name }}",
|
||||
Values: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
|
||||
Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
|
||||
Chart: "test-charts/{{ .Release.Name }}",
|
||||
Version: "{{ .Release.Name }}-0.1",
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-{{ .Release.Name }}",
|
||||
ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
|
||||
Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "{{ .Release.Name }}"},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/test-app",
|
||||
Version: "0.1",
|
||||
Verify: nil,
|
||||
Version: "test-app-0.1",
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-test-app",
|
||||
Values: []interface{}{"config/test_env/test-app/values.yaml"},
|
||||
Secrets: []string{"config/test_env/test-app/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "test-app"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template expressions in name with recursive refs",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "{{ .Release.Labels.id }}-{{ .Release.Namespace }}",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "{{ .Release.Chart }}"},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "test-chart-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "test-chart"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template expressions in boolean values",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "app-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app"},
|
||||
InstalledTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "yes" "no" }}`),
|
||||
VerifyTemplate: func(i string) *string { return &i }(`{{ true }}`),
|
||||
Verify: func(i bool) *bool { return &i }(false),
|
||||
WaitTemplate: func(i string) *string { return &i }(`{{ false }}`),
|
||||
TillerlessTemplate: func(i string) *string { return &i }(`yes`),
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "app-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app"},
|
||||
Installed: func(i bool) *bool { return &i }(true),
|
||||
Verify: func(i bool) *bool { return &i }(true),
|
||||
Wait: func(i bool) *bool { return &i }(false),
|
||||
Tillerless: func(i bool) *bool { return &i }(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template in set-values",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Name: "test-app",
|
||||
Namespace: "dev",
|
||||
SetValuesTemplate: []SetValue{
|
||||
SetValue{Name: "val1", Value: "{{ .Release.Name }}-val1"},
|
||||
SetValue{Name: "val2", File: "{{ .Release.Name }}.yml"},
|
||||
SetValue{Name: "val3", Values: []string{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
|
||||
},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Name: "test-app",
|
||||
Namespace: "dev",
|
||||
SetValues: []SetValue{
|
||||
SetValue{Name: "val1", Value: "test-app-val1"},
|
||||
SetValue{Name: "val2", File: "test-app.yml"},
|
||||
SetValue{Name: "val3", Values: []string{"test-app-val2", "test-app-val3"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template in values (map)",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Verify: nil,
|
||||
Name: "app",
|
||||
Namespace: "dev",
|
||||
ValuesTemplate: []interface{}{map[string]string{"key": "{{ .Release.Name }}-val0"}},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Verify: nil,
|
||||
Name: "app",
|
||||
Namespace: "dev",
|
||||
Values: []interface{}{map[interface{}]interface{}{"key": "app-val0"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -59,17 +154,102 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
|
||||
actual := r.Releases[0]
|
||||
|
||||
if !reflect.DeepEqual(actual.Name, tt.want.Name) {
|
||||
t.Errorf("expected Name %+v, got %+v", tt.want.Name, actual.Name)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Chart, tt.want.Chart) {
|
||||
t.Errorf("expected %+v, got %+v", tt.want.Chart, actual.Chart)
|
||||
t.Errorf("expected Chart %+v, got %+v", tt.want.Chart, actual.Chart)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) {
|
||||
t.Errorf("expected %+v, got %+v", tt.want.Namespace, actual.Namespace)
|
||||
t.Errorf("expected Namespace %+v, got %+v", tt.want.Namespace, actual.Namespace)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Values, tt.want.Values) {
|
||||
t.Errorf("expected %+v, got %+v", tt.want.Values, actual.Values)
|
||||
if diff := deep.Equal(actual.Values, tt.want.Values); diff != nil && len(actual.Values) > 0 {
|
||||
t.Errorf("Values differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Secrets, tt.want.Secrets) {
|
||||
t.Errorf("expected %+v, got %+v", tt.want.Secrets, actual.Secrets)
|
||||
if diff := deep.Equal(actual.Secrets, tt.want.Secrets); diff != nil && len(actual.Secrets) > 0 {
|
||||
t.Errorf("Secrets differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if diff := deep.Equal(actual.SetValues, tt.want.SetValues); diff != nil && len(actual.SetValues) > 0 {
|
||||
t.Errorf("SetValues differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if diff := deep.Equal(actual.Labels, tt.want.Labels); diff != nil && len(actual.Labels) > 0 {
|
||||
t.Errorf("Labels differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Version, tt.want.Version) {
|
||||
t.Errorf("expected Version %+v, got %+v", tt.want.Version, actual.Version)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Installed, tt.want.Installed) {
|
||||
t.Errorf("expected actual.Installed %+v, got %+v",
|
||||
boolPtrToString(tt.want.Installed), boolPtrToString(actual.Installed),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Tillerless, tt.want.Tillerless) {
|
||||
t.Errorf("expected actual.Tillerless %+v, got %+v",
|
||||
boolPtrToString(tt.want.Tillerless), boolPtrToString(actual.Tillerless),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Verify, tt.want.Verify) {
|
||||
t.Errorf("expected actual.Verify %+v, got %+v",
|
||||
boolPtrToString(tt.want.Verify), boolPtrToString(actual.Verify),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Wait, tt.want.Wait) {
|
||||
t.Errorf("expected actual.Wait %+v, got %+v",
|
||||
boolPtrToString(tt.want.Wait), boolPtrToString(actual.Wait),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_recursiveRefsTemplates(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input ReleaseSpec
|
||||
}{
|
||||
{
|
||||
name: "Has reqursive references",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/{{ .Release.Name }}",
|
||||
Verify: nil,
|
||||
Name: "{{ .Release.Labels.id }}",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app-{{ .Release.Name }}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has unresolvable boolean templates",
|
||||
input: ReleaseSpec{
|
||||
Name: "app-dev",
|
||||
Chart: "test-charts/app",
|
||||
Verify: nil,
|
||||
Namespace: "dev",
|
||||
WaitTemplate: func(i string) *string { return &i }("hi"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: ".",
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
Namespace: "test-namespace_",
|
||||
Repositories: nil,
|
||||
Releases: []ReleaseSpec{
|
||||
tt.input,
|
||||
},
|
||||
}
|
||||
|
||||
r, err := state.ExecuteTemplates()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got valid response: %v", r)
|
||||
t.FailNow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+15
-6
@@ -2,8 +2,9 @@ package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"sync"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
type result struct {
|
||||
@@ -12,11 +13,19 @@ type result struct {
|
||||
}
|
||||
|
||||
func (st *HelmState) scatterGather(concurrency int, items int, produceInputs func(), receiveInputsAndProduceIntermediates func(int), aggregateIntermediates func()) {
|
||||
numReleases := len(st.Releases)
|
||||
if concurrency < 1 {
|
||||
concurrency = numReleases
|
||||
} else if concurrency > numReleases {
|
||||
concurrency = numReleases
|
||||
|
||||
if concurrency < 1 || concurrency > items {
|
||||
concurrency = items
|
||||
}
|
||||
|
||||
for _, r := range st.Releases {
|
||||
if r.Tillerless != nil {
|
||||
if *r.Tillerless {
|
||||
concurrency = 1
|
||||
}
|
||||
} else if st.HelmDefaults.Tillerless {
|
||||
concurrency = 1
|
||||
}
|
||||
}
|
||||
|
||||
// WaitGroup is required to wait until goroutine per job in job queue cleanly stops.
|
||||
|
||||
+64
-11
@@ -524,7 +524,9 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
|
||||
Releases: []ReleaseSpec{*tt.release},
|
||||
HelmDefaults: tt.defaults,
|
||||
}
|
||||
helm := helmexec.New(logger, "default")
|
||||
helm := helmexec.New(logger, "default", &helmexec.ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
args, err := state.flagsForUpgrade(helm, tt.release, 0)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error flagsForUpgade: %v", err)
|
||||
@@ -704,7 +706,7 @@ func (helm *mockHelmExec) UpdateDeps(chart string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) BuildDeps(chart string) error {
|
||||
func (helm *mockHelmExec) BuildDeps(name, chart string) error {
|
||||
if strings.Contains(chart, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
@@ -718,8 +720,8 @@ func (helm *mockHelmExec) SetExtraArgs(args ...string) {
|
||||
func (helm *mockHelmExec) SetHelmBinary(bin string) {
|
||||
return
|
||||
}
|
||||
func (helm *mockHelmExec) AddRepo(name, repository, certfile, keyfile, username, password string) error {
|
||||
helm.repo = []string{name, repository, certfile, keyfile, username, password}
|
||||
func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
|
||||
helm.repo = []string{name, repository, cafile, certfile, keyfile, username, password}
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) UpdateRepo() error {
|
||||
@@ -767,10 +769,10 @@ func (helm *mockHelmExec) TestRelease(context helmexec.HelmContext, name string,
|
||||
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) Lint(chart string, flags ...string) error {
|
||||
func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) TemplateRelease(chart string, flags ...string) error {
|
||||
func (helm *mockHelmExec) TemplateRelease(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func TestHelmState_SyncRepos(t *testing.T) {
|
||||
@@ -794,7 +796,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []string{"name", "http://example.com/", "", "", "", ""},
|
||||
want: []string{"name", "http://example.com/", "", "", "", "", ""},
|
||||
},
|
||||
{
|
||||
name: "repository with cert and key",
|
||||
@@ -809,7 +811,21 @@ func TestHelmState_SyncRepos(t *testing.T) {
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []string{"name", "http://example.com/", "certfile", "keyfile", "", ""},
|
||||
want: []string{"name", "http://example.com/", "", "certfile", "keyfile", "", ""},
|
||||
},
|
||||
{
|
||||
name: "repository with ca file",
|
||||
repos: []RepositorySpec{
|
||||
{
|
||||
Name: "name",
|
||||
URL: "http://example.com/",
|
||||
CaFile: "cafile",
|
||||
Username: "",
|
||||
Password: "",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []string{"name", "http://example.com/", "cafile", "", "", "", ""},
|
||||
},
|
||||
{
|
||||
name: "repository with username and password",
|
||||
@@ -824,7 +840,7 @@ func TestHelmState_SyncRepos(t *testing.T) {
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []string{"name", "http://example.com/", "", "", "example_user", "example_password"},
|
||||
want: []string{"name", "http://example.com/", "", "", "", "example_user", "example_password"},
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
@@ -1917,6 +1933,8 @@ func TestHelmState_Delete(t *testing.T) {
|
||||
purge bool
|
||||
flags string
|
||||
tillerNamespace string
|
||||
kubeContext string
|
||||
defKubeContext string
|
||||
}{
|
||||
{
|
||||
name: "desired and installed (purge=false)",
|
||||
@@ -2008,6 +2026,37 @@ func TestHelmState_Delete(t *testing.T) {
|
||||
flags: "--tiller-namespacetillerns",
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge", "--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
{
|
||||
name: "with kubecontext",
|
||||
wantErr: false,
|
||||
desired: nil,
|
||||
installed: true,
|
||||
purge: true,
|
||||
kubeContext: "ctx",
|
||||
flags: "--kube-contextctx",
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "ctx"}}},
|
||||
},
|
||||
{
|
||||
name: "with default kubecontext",
|
||||
wantErr: false,
|
||||
desired: nil,
|
||||
installed: true,
|
||||
purge: true,
|
||||
defKubeContext: "defctx",
|
||||
flags: "--kube-contextdefctx",
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "defctx"}}},
|
||||
},
|
||||
{
|
||||
name: "with non-default and default kubecontexts",
|
||||
wantErr: false,
|
||||
desired: nil,
|
||||
installed: true,
|
||||
purge: true,
|
||||
kubeContext: "ctx",
|
||||
defKubeContext: "defctx",
|
||||
flags: "--kube-contextctx",
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge", "--kube-context", "ctx"}}},
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
@@ -2020,11 +2069,15 @@ func TestHelmState_Delete(t *testing.T) {
|
||||
Name: name,
|
||||
Installed: tt.desired,
|
||||
TillerNamespace: tt.tillerNamespace,
|
||||
KubeContext: tt.kubeContext,
|
||||
}
|
||||
releases := []ReleaseSpec{
|
||||
release,
|
||||
}
|
||||
state := &HelmState{
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: tt.defKubeContext,
|
||||
},
|
||||
Releases: releases,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -2036,14 +2089,14 @@ func TestHelmState_Delete(t *testing.T) {
|
||||
helm.lists[listKey{filter: "^" + name + "$", flags: tt.flags}] = name
|
||||
}
|
||||
affectedReleases := AffectedReleases{}
|
||||
errs := state.DeleteReleases(&affectedReleases, helm, tt.purge)
|
||||
errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge)
|
||||
if errs != nil {
|
||||
if !tt.wantErr || len(affectedReleases.Failed) != 1 || affectedReleases.Failed[0].Name != release.Name {
|
||||
t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
|
||||
return
|
||||
}
|
||||
} else if !(reflect.DeepEqual(tt.deleted, helm.deleted) && (len(affectedReleases.Deleted) == len(tt.deleted))) {
|
||||
t.Errorf("unexpected deletions happened: expected %v, got %v", &affectedReleases.Deleted, tt.deleted)
|
||||
t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.deleted)
|
||||
}
|
||||
}
|
||||
t.Run(tt.name, f)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isLocalChart(chart string) bool {
|
||||
regex, _ := regexp.Compile("^[.]?./")
|
||||
matched := regex.MatchString(chart)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
uriLike := strings.Index(chart, "://") > -1
|
||||
if uriLike {
|
||||
return false
|
||||
}
|
||||
|
||||
return chart == "" ||
|
||||
chart[0] == '/' ||
|
||||
strings.Index(chart, "/") == -1 ||
|
||||
len(strings.Split(chart, "/")) != 2
|
||||
}
|
||||
|
||||
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
if isLocalChart(repoAndChart) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
parts := strings.Split(repoAndChart, "/")
|
||||
if len(parts) != 2 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
repo := parts[0]
|
||||
chart := parts[1]
|
||||
|
||||
return repo, chart, true
|
||||
}
|
||||
|
||||
// normalizeChart allows for the distinction between a file path reference and repository references.
|
||||
// - Any single (or double character) followed by a `/` will be considered a local file reference and
|
||||
// be constructed relative to the `base path`.
|
||||
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
|
||||
func normalizeChart(basePath, chart string) string {
|
||||
if !isLocalChart(chart) || chart[0] == '/' {
|
||||
return chart
|
||||
}
|
||||
return filepath.Join(basePath, chart)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package state
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsLocalChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "stable/mysql",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "/charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
// Regression test case for:
|
||||
// * https://github.com/roboll/helmfile/issues/675
|
||||
// * https://github.com/roboll/helmfile/issues/687
|
||||
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "https://example.com/bar.tgz",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
actual := isLocalChart(testcase.input)
|
||||
|
||||
if testcase.expected != actual {
|
||||
t.Errorf("unexpected result: isLocalChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRemortChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
repo string
|
||||
chart string
|
||||
remote bool
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "stable/mysql",
|
||||
repo: "stable",
|
||||
chart: "mysql",
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
input: "./charts/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "/charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
// Regression test case for:
|
||||
// * https://github.com/roboll/helmfile/issues/675
|
||||
// * https://github.com/roboll/helmfile/issues/687
|
||||
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "https://example.com/bar.tgz",
|
||||
remote: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
repo, chart, actual := resolveRemoteChart(testcase.input)
|
||||
|
||||
if testcase.remote != actual {
|
||||
t.Fatalf("unexpected result: reolveRemoteChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.remote, actual)
|
||||
}
|
||||
|
||||
if testcase.repo != repo {
|
||||
t.Errorf("unexpected repo: %s: expected=%v, got=%v", testcase.input, testcase.repo, repo)
|
||||
}
|
||||
|
||||
if testcase.chart != chart {
|
||||
t.Errorf("unexpected chart: %s: expected=%v, got=%v", testcase.input, testcase.chart, chart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
expected: "/path/to/mychart",
|
||||
},
|
||||
{
|
||||
input: "/charts/mychart",
|
||||
expected: "/charts/mychart",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
actual := normalizeChart("/path/to", testcase.input)
|
||||
|
||||
if testcase.expected != actual {
|
||||
t.Fatalf("unexpected result: normalizeChart(\"/path/to\", \"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func FromYaml(str string) (Values, error) {
|
||||
m := Values{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s, offending yaml: %s", err, str)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ func (r *FileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, e
|
||||
return r.RenderTemplateContentToBuffer(content)
|
||||
}
|
||||
|
||||
// RenderToBytes loads the content of the file.
|
||||
// If its extension is `gotmpl` it treats the content as a go template and renders it.
|
||||
func (r *FileRenderer) RenderToBytes(path string) ([]byte, error) {
|
||||
var yamlBytes []byte
|
||||
splits := strings.Split(path, ".")
|
||||
|
||||
@@ -60,6 +60,12 @@ test_start "happypath - simple rollout of httpbin chart"
|
||||
info "Diffing ${dir}/happypath.yaml"
|
||||
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
|
||||
|
||||
info "Diffing ${dir}/happypath.yaml without color"
|
||||
bash -c "${helmfile} -f ${dir}/happypath.yaml --no-color diff --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
|
||||
|
||||
info "Diffing ${dir}/happypath.yaml with limited context"
|
||||
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --context 3 --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
|
||||
|
||||
info "Templating ${dir}/happypath.yaml"
|
||||
${helmfile} -f ${dir}/happypath.yaml template
|
||||
code=$?
|
||||
@@ -93,6 +99,9 @@ ${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release shou
|
||||
info "Ensuring \"helmfile delete\" doesn't fail when no releases installed"
|
||||
${helmfile} -f ${dir}/happypath.yaml delete || fail "\"helmfile delete\" shouldn't fail when there are no installed releases"
|
||||
|
||||
info "Ensuring \"helmfile template\" output does contain only YAML docs"
|
||||
(${helmfile} -f ${dir}/happypath.yaml template | kubectl apply -f -) || fail "\"helmfile template | kubectl apply -f -\" shouldn't fail"
|
||||
|
||||
test_pass "happypath"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user