Compare commits

...
49 Commits
Author SHA1 Message Date
KUOKA Yusuke b82f77ee50 fix: Absolute paths for -f not working on Windows (#721)
Fixes #718
2019-06-25 20:06:07 +09:00
KUOKA Yusuke e6bb8282db fix: remote helmfile from git::ssh source (#719)
The remote helmfile feature introduced by #648 was unable to be sourced from private git repositories due to URL parsing issue in helmfile. This fixes that.

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

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

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

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

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

* fixing the table

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

It only affects people using the `HELM_SECRETS_DEC_SUFFIX` env var

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #623

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

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

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

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

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

Resolves #523

* fix: multiple "bases" declarations yields duplicate releases

Fixes #615

* fix regression in double-rendering with env value overrides

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

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

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

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

Resolves #548

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

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

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

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

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

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

Resolves: #597
2019-05-27 09:53:24 +09:00
KUOKA Yusuke f6264bfa9d fix: do not complain on missing values files for undesired release (#617)
Fixes #616
2019-05-24 21:40:34 +09:00
KUOKA Yusuke 3b3d3092dd fix scopelint errors / possible races in hooks (#614)
Ref #612
Fixes #514
2019-05-22 18:50:38 +09:00
67 changed files with 3919 additions and 982 deletions
+3 -1
View File
@@ -22,7 +22,9 @@ RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
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/futuresimple/helm-secrets && \
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
helm plugin install https://github.com/aslafy-z/helm-git.git
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
+150 -26
View File
@@ -1,19 +1,19 @@
# helmfile [![CircleCI](https://circleci.com/gh/roboll/helmfile.svg?style=svg)](https://circleci.com/gh/roboll/helmfile)
# Helmfile [![CircleCI](https://circleci.com/gh/roboll/helmfile.svg?style=svg)](https://circleci.com/gh/roboll/helmfile)
Deploy Kubernetes Helm Charts
[![Docker Repository on Quay](https://quay.io/repository/roboll/helmfile/status "Docker Repository on Quay")](https://quay.io/repository/roboll/helmfile)
[![Slack Community #helmfile](https://slack.sweetops.com/badge.svg)](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,20 +23,44 @@ 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`:
```yaml
# Chart repositories used from within this state file
#
# Use `helm-s3` and `helm-git` and whatever Helm Downloader plugins
# to use repositories other than the official repository or one backend by chartmuseum.
repositories:
- name: roboll
url: http://roboll.io/charts
certFile: optional_client_cert
keyFile: optional_client_key
username: optional_username
password: optional_password
# To use official "stable" charts a.k.a https://github.com/helm/charts/tree/master/stable
- name: stable
url: https://kubernetes-charts.storage.googleapis.com
# To use official "incubator" charts a.k.a https://github.com/helm/charts/tree/master/incubator
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
# helm-git powered repository: You can treat any Git repository as a charts repository
- name: polaris
url: git+https://github.com/reactiveops/polaris@deploy/helm?ref=master
# Advanced configuration: You can setup basic or tls auth
- name: roboll
url: http://roboll.io/charts
certFile: optional_client_cert
keyFile: optional_client_key
username: optional_username
password: optional_password
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
@@ -63,6 +87,9 @@ helmDefaults:
# path to TLS key file (default "$HELM_HOME/key.pem")
tlsKey: "path/to/key.pem"
# The desired states of Helm releases.
#
# Helmfile runs various helm commands to converge the current state in the live cluster to the desired state defined here.
releases:
# Published chart example
- name: vault # name of this release
@@ -71,12 +98,21 @@ releases:
foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
version: ~1.24.1 # the semver of the chart. range constraint is supported
missingFileHandler: warn # 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.
missingFileHandler: Warn # 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.
# Values files used for rendering the chart
values:
# value files passed via --values
# Value files passed via --values
- vault.yaml
# inline values, passed via a temporary values file and --values
# Inline values, passed via a temporary values file and --values, so that it doesn't suffer from type issues like --set
- address: https://vault.example.com
# Go template available in inline values and values files.
- image:
# The end result is more or less YAML. So do `quote` to prevent number-like strings from accidentally parsed into numbers!
# See https://github.com/roboll/helmfile/issues/608
tag: {{ requiredEnv "IMAGE_TAG" | quote }}
# Otherwise:
# tag: "{{ requiredEnv "IMAGE_TAG" }}"
# tag: !!string {{ requiredEnv "IMAGE_TAG" }}
db:
username: {{ requiredEnv "DB_USERNAME" }}
# value taken from environment variable. Quotes are necessary. Will throw an error if the environment variable is not set. $DB_PASSWORD needs to be set in the calling environment ex: export DB_PASSWORD='password1'
@@ -85,6 +121,8 @@ releases:
# Interpolate environment variable with a fixed string
domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
scheme: {{ env "SCHEME" | default "https" }}
# Use `values` whenever possible!
# `set` translates to helm's `--set key=val`, that is known to suffer from type issues like https://github.com/roboll/helmfile/issues/608
set:
# single value loaded from a local file, translates to --set-file foo.config=path/to/file
- name: foo.config
@@ -100,6 +138,8 @@ releases:
# will attempt to decrypt it using helm-secrets plugin
secrets:
- vault_secret.yaml
# verify the chart before upgrading (only works with packaged charts not directories)
verify: true
# wait for k8s resources via --wait. Defaults to `false`
wait: true
# time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
@@ -124,6 +164,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
@@ -134,6 +178,80 @@ releases:
- ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment.
wait: true
#
# Advanced Configuration: Nested States
#
helmfiles:
- # Path to the helmfile state file being processed BEFORE releases in this state file
path: path/to/subhelmfile.yaml
# Label selector used for filtering releases in the nested state.
# For example, `name=prometheus` in this context is equivalent to processing the nested state like
# helmfile -f path/to/subhelmfile.yaml -l name=prometheus sync
selectors:
- name=prometheus
# Override state values
values:
# Values files merged into the nested state's values
- additiona.values.yaml
# Inline state values merged into the nested state's values
- key1: val1
- # All the nested state files under `helmfiles:` is processed in the order of definition.
# 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.
# Therefore all the local paths in the file are resolved relative to the file
path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
#
# Advanced Configuration: Environments
#
# The list of environments managed by helmfile.
#
# The default is `environments: {"default": {}}` which implies:
#
# - `{{ .Environment.Name }}` evaluates to "default"
# - `{{ .Environment.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 }}`.
# Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below,
# `{{ .Environment.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`
- 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`.
production:
values:
- environment/production/values.yaml
- myChartVer: 1.0.0
## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Secrets.KEY }}`
secrets:
- environment/production/secrets.yaml
# Instructs helmfile to fail when unable to find a environment values file listed under `environments.NAME.values`.
#
# Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
#
# Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
# a message about the missing file at the log-level.
missingFileHandler: Error
#
# Advanced Configuration: Layering
#
# Helmfile merges all the "base" state files and this state file before processing.
#
# Assuming this state file is named `helmfile.yaml`, all the files are merged in the order of:
# environments.yaml <- defaults.yaml <- templates.yaml <- helmfile.yaml
bases:
- environments.yaml
- defaults.yaml
- templates.yaml
```
## Templating
@@ -220,9 +338,10 @@ USAGE:
helmfile [global options] command [command options] [arguments...]
VERSION:
v0.52.0
v0.70.0
COMMANDS:
deps update charts based on the contents of requirements.yaml
repos sync repositories from state file (helm repo add && helm repo update)
charts DEPRECATED: sync releases from state file (helm upgrade --install)
diff diff releases from state file against env (helm diff)
@@ -239,6 +358,8 @@ GLOBAL OPTIONS:
--helm-binary value, -b value path to helm binary
--file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference
--environment default, -e default specify the environment name. defaults to default
--state-values-set value set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--state-values-file value specify state values in a YAML file
--quiet, -q Silence output. Equivalent to log-level warn
--kube-context value Set kubectl context. Uses current context by default
--log-level value Set log level, default info
@@ -247,6 +368,7 @@ GLOBAL OPTIONS:
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease
--allow-no-matching-release Do not exit with an error code if the provided selector has no matching releases.
--interactive, -i Request confirmation before attempting to modify clusters
--help, -h show help
--version, -v print the version
@@ -254,7 +376,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.
@@ -585,9 +707,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,
@@ -659,19 +778,22 @@ Just run `helmfile sync` inside `myteam/`, and you are done.
All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too.
#### selectors
When composing helmfiles you can use selectors from the command line as well as explicit selectors inside the parent helmfile to filter the releases to be used.
```yaml
helmfiles:
- apps/*/helmfile.yaml
- path: apps/a-helmfile.yaml
selectors: # list of selectors
- name=prometheus
- tier=frontend
selectors: # list of selectors
- name=prometheus
- tier=frontend
- path: apps/b-helmfile.yaml # no selector, so all releases are used
selectors: []
selectors: []
- path: apps/c-helmfile.yaml # parent selector to be used or cli selector for the initial helmfile
selectorsInherited: true
selectorsInherited: true
```
* When a selector is specified, only this selector applies and the parents or CLI selectors are ignored.
* When not selector is specified there are 2 modes for the selector inheritance because we would like to change the current inheritance behavior (see [issue #344](https://github.com/roboll/helmfile/issues/344) ).
* Legacy mode, sub-helmfiles without selectors inherit selectors from their parent helmfile. The initial helmfiles inherit from the command line selectors.
@@ -707,9 +829,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:
@@ -735,6 +858,7 @@ releases:
# *snip*
hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
+14 -13
View File
@@ -1,18 +1,19 @@
# 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 | Repos are private, but [here's a blog post](https://medium.com/@naseem_60378/helmfile-its-like-a-helm-for-your-helm-74a908581599). We're using `helmfile.d` with 1 helmfile per namespace. Multiple environments, but not all charts on all environments. Using installed: `{{ eq .Environment.Name "env1" "env2" "env5" }}`. Loving it so far! Thanks! | Montreal, Canada | March 2019 |
| [uniqkey](https://uniqkey.eu/) | production | [Wiki Page](https://ocd-scm.github.io/ocd-meta/) | Copenhagen, Denmark | April 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).
-94
View File
@@ -1,94 +0,0 @@
package cmd
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"strings"
)
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
a, fileOrDir, err := InitAppEntry(c, false)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStates(fileOrDir, a.Selectors, convergeWithHelmBinary)
return toCliError(err)
}
func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
fileOrDir := c.GlobalString("file")
kubeContext := c.GlobalString("kube-context")
namespace := c.GlobalString("namespace")
selectors := c.GlobalStringSlice("selector")
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
env := c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
app := app.Init(&app.App{
KubeContext: kubeContext,
Logger: logger,
Reverse: reverse,
Env: env,
Namespace: namespace,
Selectors: selectors,
})
return app, fileOrDir, nil
}
func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
a, fileOrDir, err := InitAppEntry(c, reverse)
if err != nil {
return err
}
ctx := app.NewContext()
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
if c.GlobalString("helm-binary") != "" {
helm.SetHelmBinary(c.GlobalString("helm-binary"))
}
return converge(st, helm, ctx)
}
err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
return toCliError(err)
}
func toCliError(err error) error {
if err != nil {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
return cli.NewExitError(e.Error(), 2)
case *app.Error:
return cli.NewExitError(e.Error(), e.Code())
default:
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
}
}
return err
}
-37
View File
@@ -1,37 +0,0 @@
package cmd
import (
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/urfave/cli"
)
func Deps() cli.Command {
return cli.Command{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
errs := state.UpdateDeps(helm)
ok := len(errs) == 0
return ok, errs
})
},
}
}
+50 -14
View File
@@ -103,23 +103,16 @@ Let's assume that your `helmfile.yaml` looks like:
```
bases:
- commons.yaml
- environments.yaml
releases:
- name: metricbaet
chart: stable/metricbeat
- name: myapp
chart: mychart
```
Whereas `commons.yaml` contained a monitoring agent:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
```
And `environments.yaml` contained well-known environments:
Whereas `environments.yaml` contained well-known environments:
```yaml
environments:
@@ -130,10 +123,6 @@ environments:
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
```yaml
# commons.yaml
releases:
- name: metricbaet
chart: stable/metricbeat
---
# environments.yaml
environments:
@@ -144,6 +133,8 @@ environments:
releases:
- name: myapp
chart: mychart
- name: metricbaet
chart: stable/metricbeat
```
Finally the resulting YAML documents are merged in the order of occurrence,
@@ -167,6 +158,51 @@ Now, repeat the above steps for each your `helmfile.yaml`, so that all your helm
Please also see [the discussion in the issue 388](https://github.com/roboll/helmfile/issues/388#issuecomment-491710348) for more advanced layering examples.
## Merging Arrays in Layers
Helmfile doesn't merge arrays across layers. That is, the below example doesn't work as you might have expected:
```yaml
releases:
- name: metricbaet
chart: stable/metricbeat
---
releases:
- name: myapp
chart: mychart
```
Helmfile overrides the `releases` array with the latest layer so the resulting state file will be:
```yaml
releases:
# metricbeat release disappeared! but that's how helmfiel works
- name: myapp
chart: mychart
```
A work-around is to treat the state file as a go template and use `readFile` template function to import the common part of your state file as a plain text:
`common.yaml`:
```yaml
templates:
metricbeat: &metricbeat
- name: metricbeat
chart: stable/metricbeat
```
`helmfile.yaml`:
```yaml
{{ readFile "common.yaml" }}
releases:
- <<: *metricbeat
- name: myapp
chart: mychart
```
## Layering State Template Files
Do you need to make your state file even more DRY?
-8
View File
@@ -1,8 +0,0 @@
package environment
type Environment struct {
Name string
Values map[string]interface{}
}
var EmptyEnvironment Environment
+3 -4
View File
@@ -4,20 +4,19 @@ go 1.12
require (
github.com/Masterminds/semver v1.4.1
github.com/Masterminds/sprig v2.15.0+incompatible
github.com/Masterminds/sprig v2.16.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/google/go-cmp v0.3.0
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
github.com/hashicorp/go-getter v1.3.0
github.com/huandu/xstrings v1.0.0 // indirect
github.com/imdario/mergo v0.3.6
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/pkg/errors v0.8.1 // indirect
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/zap v1.8.0
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect
gopkg.in/yaml.v2 v2.2.1
gotest.tools v2.2.0+incompatible
)
+169
View File
@@ -1,35 +1,204 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.36.0 h1:+aCSj7tOo2LODWVEuZDZeGCckdt6MlSF+X/rB3wUiS8=
cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
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/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/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/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=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
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/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/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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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/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=
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3 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/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=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-getter v1.3.0 h1:pFMSFlI9l5NaeuzkpE3L7BYk9qQ9juTAgXW/H0cqxcU=
github.com/hashicorp/go-getter v1.3.0/go.mod h1:/O1k/AizTN0QmfEKknCYGvICeyKUDqCYA8vvWtGWDeQ=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
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/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=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
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=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
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/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/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=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
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/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=
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939/go.mod h1:omGxs4/6hNjxPKUTjmaNkPzehSnNJOJN6pMEbrlYIT4=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28 h1:Yf7/DcGM+61oLBGXQV2Q+ztEGBcOe3EUnbKSOn4fQuE=
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
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=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
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/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/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=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
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/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=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 h1:mBVYJnbrXLA/ZCBTCe7PtEgAUP+1bg92qTaFoPHdz+8=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
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/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=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
+225 -295
View File
@@ -2,11 +2,10 @@ package main
import (
"fmt"
"github.com/roboll/helmfile/args"
"github.com/roboll/helmfile/cmd"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/state"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -59,6 +58,14 @@ func main() {
Name: "environment, e",
Usage: "specify the environment name. defaults to `default`",
},
cli.StringSliceFlag{
Name: "state-values-set",
Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)",
},
cli.StringSliceFlag{
Name: "state-values-file",
Usage: "specify state values in a YAML file",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "Silence output. Equivalent to log-level warn",
@@ -82,6 +89,10 @@ func main() {
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label. --selector name=myrelease`,
},
cli.BoolFlag{
Name: "allow-no-matching-release",
Usage: `Do not exit with an error code if the provided selector has no matching releases.`,
},
cli.BoolFlag{
Name: "interactive, i",
Usage: "Request confirmation before attempting to modify clusters",
@@ -90,7 +101,20 @@ func main() {
cliApp.Before = configureLogging
cliApp.Commands = []cli.Command{
cmd.Deps(),
{
Name: "deps",
Usage: "update charts based on the contents of requirements.yaml",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: action(func(run *app.App, c configImpl) error {
return run.Deps(c)
}),
},
{
Name: "repos",
Usage: "sync repositories from state file (helm repo add && helm repo update)",
@@ -101,20 +125,9 @@ func main() {
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return cmd.VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
errs := ctx.SyncReposOnce(state, helm)
ok := len(state.Repositories) > 0 && len(errs) == 0
return ok, errs
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Repos(c)
}),
},
{
Name: "charts",
@@ -135,14 +148,9 @@ func main() {
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
return executeSyncCommand(c, &affectedReleases, st, helm)
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
Action: action(func(run *app.App, c configImpl) error {
return run.DeprecatedSyncCharts(c)
}),
},
{
Name: "diff",
@@ -175,24 +183,9 @@ func main() {
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
return errs
}
_, errs := executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets"))
return errs
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Diff(c)
}),
},
{
Name: "template",
@@ -217,22 +210,9 @@ func main() {
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
return errs
}
return executeTemplateCommand(c, state, helm)
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Template(c)
}),
},
{
Name: "lint",
@@ -257,25 +237,9 @@ func main() {
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
values := c.StringSlice("values")
args := args.GetArgs(c.String("args"), state)
workers := c.Int("concurrency")
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
return state.LintReleases(helm, values, args, workers)
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Lint(c)
}),
},
{
Name: "sync",
@@ -300,25 +264,9 @@ func main() {
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
return errs
}
return executeSyncCommand(c, &affectedReleases, st, helm)
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
Action: action(func(run *app.App, c configImpl) error {
return run.Sync(c)
}),
},
{
Name: "apply",
@@ -347,88 +295,9 @@ func main() {
Usage: "skip running `helm repo update` and `helm dependency build`",
},
},
Action: func(c *cli.Context) error {
affectedReleases := state.AffectedReleases{}
errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error {
if !c.Bool("skip-deps") {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
return errs
}
releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets"))
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
if err != nil {
errs = append(errs, err)
}
fatalErrs := []error{}
noError := true
for _, e := range errs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
noError = false
fatalErrs = append(fatalErrs, e)
}
default:
noError = false
fatalErrs = append(fatalErrs, e)
}
}
// sync only when there are changes
if noError {
if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
// TODO better way to get the logger
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
logger.Infof("")
logger.Infof("No affected releases")
} else {
names := []string{}
for _, r := range releases {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
rs := []state.ReleaseSpec{}
for _, r := range releases {
rs = append(rs, *r)
}
for _, r := range releasesToBeDeleted {
rs = append(rs, *r)
}
st.Releases = rs
return executeSyncCommand(c, &affectedReleases, st, helm)
}
}
}
return fatalErrs
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
Action: action(func(run *app.App, c configImpl) error {
return run.Apply(c)
}),
},
{
Name: "status",
@@ -445,23 +314,19 @@ Do you really want to apply?
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
workers := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
return state.ReleaseStatuses(helm, workers)
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Status(c)
}),
},
{
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: "",
@@ -472,77 +337,28 @@ Do you really want to apply?
Usage: "purge releases i.e. free release names and histories",
},
},
Action: func(c *cli.Context) error {
affectedReleases := state.AffectedReleases{}
errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
purge := c.Bool("purge")
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
names := make([]string, len(state.Releases))
for i, r := range state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(&affectedReleases, helm, purge)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
Action: action(func(run *app.App, c configImpl) error {
return run.Delete(c)
}),
},
{
Name: "destroy",
Usage: "deletes and then purges releases",
Flags: []cli.Flag{
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
},
Action: func(c *cli.Context) error {
affectedReleases := state.AffectedReleases{}
errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
names := make([]string, len(state.Releases))
for i, r := range state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.GlobalBool("interactive")
if !interactive || interactive && askForConfirmation(msg) {
return state.DeleteReleases(&affectedReleases, helm, true)
}
return nil
})
affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger))
return errs
},
Action: action(func(run *app.App, c configImpl) error {
return run.Destroy(c)
}),
},
{
Name: "test",
@@ -568,20 +384,9 @@ Do you really want to delete?
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error {
cleanup := c.Bool("cleanup")
timeout := c.Int("timeout")
concurrency := c.Int("concurrency")
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
}
return state.TestReleases(helm, cleanup, timeout, concurrency)
})
},
Action: action(func(run *app.App, c configImpl) error {
return run.Test(c)
}),
},
}
@@ -592,39 +397,164 @@ Do you really want to delete?
}
}
func executeSyncCommand(c *cli.Context, affectedReleases *state.AffectedReleases, state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
if len(args) > 0 {
helm.SetExtraArgs(args...)
type configImpl struct {
c *cli.Context
set map[string]interface{}
}
func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
if c.NArg() > 0 {
cli.ShowAppHelp(c)
return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.SyncReleases(affectedReleases, helm, values, workers)
}
func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state)
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.TemplateReleases(helm, values, args, workers)
}
func executeDiffCommand(c *cli.Context, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) {
args := args.GetArgs(c.String("args"), st)
if len(args) > 0 {
helm.SetExtraArgs(args...)
conf := configImpl{
c: c,
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
triggerCleanupEvents := c.Command.HasName("diff")
optsSet := c.GlobalStringSlice("state-values-set")
if len(optsSet) > 0 {
set := map[string]interface{}{}
for i := range optsSet {
ops := strings.Split(optsSet[i], ",")
for j := range ops {
op := strings.Split(ops[j], "=")
k := strings.Split(op[0], ".")
v := op[1]
return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets, triggerCleanupEvents)
set = maputil.Set(set, k, v)
}
}
conf.set = set
}
return conf, nil
}
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge)
func (c configImpl) Values() []string {
return c.c.StringSlice("values")
}
func (c configImpl) Args() string {
return c.c.String("args")
}
func (c configImpl) Concurrency() int {
return c.c.Int("concurrency")
}
func (c configImpl) HasCommandName(name string) bool {
return c.c.Command.HasName(name)
}
// DiffConfig
func (c configImpl) SkipDeps() bool {
return c.c.Bool("skip-deps")
}
func (c configImpl) DetailedExitcode() bool {
return c.c.Bool("detailed-exitcode")
}
func (c configImpl) SuppressSecrets() bool {
return c.c.Bool("suppress-secrets")
}
// DeleteConfig
func (c configImpl) Purge() bool {
return c.c.Bool("purge")
}
// TestConfig
func (c configImpl) Cleanup() bool {
return c.c.Bool("cleanup")
}
func (c configImpl) Timeout() int {
return c.c.Int("timeout")
}
// GlobalConfig
func (c configImpl) HelmBinary() string {
return c.c.GlobalString("helm-binary")
}
func (c configImpl) KubeContext() string {
return c.c.GlobalString("kube-context")
}
func (c configImpl) Namespace() string {
return c.c.GlobalString("namespace")
}
func (c configImpl) FileOrDir() string {
return c.c.GlobalString("file")
}
func (c configImpl) Selectors() []string {
return c.c.GlobalStringSlice("selector")
}
func (c configImpl) Set() map[string]interface{} {
return c.set
}
func (c configImpl) ValuesFiles() []string {
return c.c.GlobalStringSlice("state-values-file")
}
func (c configImpl) Interactive() bool {
return c.c.GlobalBool("interactive")
}
func (c configImpl) Logger() *zap.SugaredLogger {
return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
}
func (c configImpl) Env() string {
env := c.c.GlobalString("environment")
if env == "" {
env = state.DefaultEnv
}
return env
}
func action(do func(*app.App, configImpl) error) func(*cli.Context) error {
return func(implCtx *cli.Context) error {
conf, err := NewUrfaveCliConfigImpl(implCtx)
if err != nil {
return err
}
a := app.New(conf)
a.ErrorHandler = func(err error) error {
return toCliError(implCtx, err)
}
return do(a, conf)
}
}
func toCliError(c *cli.Context, err error) error {
if err != nil {
switch e := err.(type) {
case *app.NoMatchingHelmfileError:
noMatchingExitCode := 3
if c.GlobalBool("allow-no-matching-release") {
noMatchingExitCode = 0
}
return cli.NewExitError(e.Error(), noMatchingExitCode)
case *app.Error:
return cli.NewExitError(e.Error(), e.Code())
default:
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
}
}
return err
}
+229 -23
View File
@@ -2,6 +2,9 @@ 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"
@@ -9,8 +12,6 @@ import (
"strings"
"syscall"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"go.uber.org/zap"
"path/filepath"
@@ -24,8 +25,17 @@ type App struct {
Env string
Namespace string
Selectors []string
HelmBinary string
Args string
ValuesFiles []string
Set map[string]interface{}
FileOrDir string
ErrorHandler func(error) error
readFile func(string) ([]byte, error)
fileExists func(string) (bool, error)
glob func(string) ([]string, error)
abs func(string) (string, error)
fileExistsAt func(string) bool
@@ -33,6 +43,23 @@ type App struct {
getwd func() (string, error)
chdir func(string) error
remote *remote.Remote
}
func New(conf ConfigProvider) *App {
return Init(&App{
KubeContext: conf.KubeContext(),
Logger: conf.Logger(),
Env: conf.Env(),
Namespace: conf.Namespace(),
Selectors: conf.Selectors(),
HelmBinary: conf.HelmBinary(),
Args: conf.Args(),
FileOrDir: conf.FileOrDir(),
ValuesFiles: conf.ValuesFiles(),
Set: conf.Set(),
})
}
func Init(app *App) *App {
@@ -42,10 +69,89 @@ func Init(app *App) *App {
app.getwd = os.Getwd
app.chdir = os.Chdir
app.fileExistsAt = fileExistsAt
app.fileExists = fileExists
app.directoryExistsAt = directoryExistsAt
return app
}
func (a *App) Deps(c DepsConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Deps(c)
})
}
func (a *App) Repos(c ReposConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Repos(c)
})
}
func (a *App) reverse() *App {
new := *a
new.Reverse = true
return &new
}
func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.DeprecatedSyncCharts(c)
})
}
func (a *App) Diff(c DiffConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Diff(c)
})
}
func (a *App) Template(c TemplateConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Template(c)
})
}
func (a *App) Lint(c LintConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Lint(c)
})
}
func (a *App) Sync(c SyncConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Sync(c)
})
}
func (a *App) Apply(c ApplyConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Apply(c)
})
}
func (a *App) Status(c StatusesConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Status(c)
})
}
func (a *App) Delete(c DeleteConfigProvider) error {
return a.reverse().ForEachState(func(run *Run) []error {
return run.Delete(c)
})
}
func (a *App) Destroy(c DestroyConfigProvider) error {
return a.reverse().ForEachState(func(run *Run) []error {
return run.Destroy(c)
})
}
func (a *App) Test(c TestConfigProvider) error {
return a.ForEachState(func(run *Run) []error {
return run.Test(c)
})
}
func (a *App) within(dir string, do func() error) error {
if dir == "." {
return do()
@@ -81,7 +187,7 @@ func (a *App) within(dir string, do func() error) error {
return appErr
}
func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
func (a *App) visitStateFiles(fileOrDir string, do func(string, string) error) error {
desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir)
if err != nil {
return appError("", err)
@@ -101,7 +207,12 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
a.Logger.Debugf("processing file \"%s\" in directory \"%s\"", file, dir)
err := a.within(dir, func() error {
return do(file)
absd, err := a.abs(dir)
if err != nil {
return err
}
return do(file, absd)
})
if err != nil {
return appError(fmt.Sprintf("in %s/%s", dir, file), err)
@@ -111,26 +222,39 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
return nil
}
func (a *App) loadDesiredStateFromYaml(file string) (*state.HelmState, error) {
func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.HelmState, error) {
ld := &desiredStateLoader{
readFile: a.readFile,
env: a.Env,
namespace: a.Namespace,
logger: a.Logger,
abs: a.abs,
readFile: a.readFile,
fileExists: a.fileExists,
env: a.Env,
namespace: a.Namespace,
logger: a.Logger,
abs: a.abs,
Reverse: a.Reverse,
KubeContext: a.KubeContext,
glob: a.glob,
}
return ld.Load(file)
var op LoadOpts
if len(opts) > 0 {
op = opts[0]
}
return ld.Load(file, op)
}
func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
noMatchInHelmfiles := true
err := a.visitStateFiles(fileOrDir, func(f string) error {
st, err := a.loadDesiredStateFromYaml(f)
err := a.visitStateFiles(fileOrDir, func(f, d string) error {
opts := defOpts.DeepCopy()
if opts.CalleePath == "" {
opts.CalleePath = f
}
st, err := a.loadDesiredStateFromYaml(f, opts)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@@ -166,16 +290,23 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
return ctx.wrapErrs(err)
}
}
st.Selectors = selector
st.Selectors = opts.Selectors
if len(st.Helmfiles) > 0 {
noMatchInSubHelmfiles := true
for i, m := range st.Helmfiles {
optsForNestedState := LoadOpts{
CalleePath: filepath.Join(d, f),
Environment: m.Environment,
}
//assign parent selector to sub helm selector in legacy mode or do not inherit in experimental mode
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
m.Selectors = selector
optsForNestedState.Selectors = opts.Selectors
} else {
optsForNestedState.Selectors = m.Selectors
}
if err := a.VisitDesiredStates(m.Path, m.Selectors, converge); err != nil {
if err := a.visitStates(m.Path, optsForNestedState, converge); err != nil {
switch err.(type) {
case *NoMatchingHelmfileError:
@@ -209,9 +340,62 @@ func (a *App) VisitDesiredStates(fileOrDir string, selector []string, converge f
return nil
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
func (a *App) ForEachState(do func(*Run) []error) error {
err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error {
ctx := NewContext()
err := a.VisitDesiredStates(fileOrDir, a.Selectors, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
run := NewRun(st, helm, ctx)
return do(run)
})
if err != nil && a.ErrorHandler != nil {
return a.ErrorHandler(err)
}
return err
}
func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error {
opts := LoadOpts{
Selectors: a.Selectors,
}
envvals := []interface{}{}
if a.ValuesFiles != nil {
for i := range a.ValuesFiles {
envvals = append(envvals, a.ValuesFiles[i])
}
}
if a.Set != nil {
envvals = append(envvals, a.Set)
}
if len(envvals) > 0 {
opts.Environment.OverrideValues = envvals
}
dir, err := a.getwd()
if err != nil {
return err
}
getter := &remote.GoGetter{Logger: a.Logger}
remote := &remote.Remote{
Logger: a.Logger,
Home: dir,
Getter: getter,
ReadFile: a.readFile,
DirExists: a.directoryExistsAt,
FileExists: a.fileExistsAt,
}
a.remote = remote
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 {
@@ -219,9 +403,14 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
}
}
if a.HelmBinary != "" {
helm.SetHelmBinary(a.HelmBinary)
}
type Key struct {
TillerNamespace, Name string
}
releaseNameCounts := map[Key]int{}
for _, r := range st.Releases {
tillerNamespace := st.HelmDefaults.TillerNamespace
@@ -242,10 +431,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) {
@@ -265,6 +450,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) {
@@ -316,6 +510,18 @@ func fileExistsAt(path string) bool {
return err == nil && fileInfo.Mode().IsRegular()
}
func fileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func directoryExistsAt(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.Mode().IsDir()
+731 -67
View File
@@ -2,28 +2,30 @@ package app
import (
"fmt"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/testhelper"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/state"
"gotest.tools/env"
)
func appWithFs(app *App, files map[string]string) *App {
fs := state.NewTestFs(files)
fs := testhelper.NewTestFs(files)
return injectFs(app, fs)
}
func injectFs(app *App, fs *state.TestFs) *App {
func injectFs(app *App, fs *testhelper.TestFs) *App {
app.readFile = fs.ReadFile
app.glob = fs.Glob
app.abs = fs.Abs
app.getwd = fs.Getwd
app.chdir = fs.Chdir
app.fileExistsAt = fs.FileExistsAt
app.fileExists = fs.FileExists
app.directoryExistsAt = fs.DirectoryExistsAt
return app
}
@@ -51,7 +53,7 @@ releases:
chart: stable/grafana
`,
}
fs := state.NewTestFs(files)
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
KubeContext: "default",
@@ -97,7 +99,7 @@ BAR: 2
BAZ: 4
`,
}
fs := state.NewTestFs(files)
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"}
app := &App{
KubeContext: "default",
@@ -136,7 +138,7 @@ releases:
chart: stable/zipkin
`,
}
fs := state.NewTestFs(files)
fs := testhelper.NewTestFs(files)
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
@@ -155,12 +157,66 @@ releases:
t.Fatal("expected error did not occur")
}
expected := "in ./helmfile.yaml: failed to read helmfile.yaml: no file matching env.*.yaml found"
expected := "in ./helmfile.yaml: failed to read helmfile.yaml: environment values file matching \"env.*.yaml\" does not exist in \".\""
if err.Error() != expected {
t.Errorf("unexpected error: expected=%s, got=%v", expected, err)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFileHandler(t *testing.T) {
testcases := []struct {
name string
handler string
filePattern string
expectErr bool
}{
{name: "error handler with no files matching glob", handler: "Error", filePattern: "env.*.yaml", expectErr: true},
{name: "warn handler with no files matching glob", handler: "Warn", filePattern: "env.*.yaml", expectErr: false},
{name: "info handler with no files matching glob", handler: "Info", filePattern: "env.*.yaml", expectErr: false},
{name: "debug handler with no files matching glob", handler: "Debug", filePattern: "env.*.yaml", expectErr: false},
}
for i := range testcases {
testcase := testcases[i]
t.Run(testcase.name, func(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": fmt.Sprintf(`
environments:
default:
missingFileHandler: %s
values:
- %s
releases:
- name: zipkin
chart: stable/zipkin
`, testcase.handler, testcase.filePattern),
}
fs := testhelper.NewTestFs(files)
app := &App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
}
app = injectFs(app, fs)
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", noop,
)
if testcase.expectErr && err == nil {
t.Fatal("expected error did not occur")
}
if !testcase.expectErr && err != nil {
t.Errorf("not error expected, but got: %v", err)
}
})
}
}
// See https://github.com/roboll/helmfile/issues/193
func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) {
files := map[string]string{
@@ -196,7 +252,7 @@ releases:
}
for _, testcase := range testcases {
fs := state.NewTestFs(files)
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
KubeContext: "default",
@@ -604,6 +660,120 @@ func runFilterSubHelmFilesTests(testcases []struct {
}
func TestVisitDesiredStatesWithReleasesFiltered_EmbeddedNestedStateAdditionalEnvValues(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- path: helmfile.d/a*.yaml
values:
- env.values.yaml
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
values:
- env.values.yaml
- tillerNs: INLINE_TILLER_NS_3
`,
"/path/to/helmfile.d/a1.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: foo
chart: stable/zipkin
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/helmfile.d/b.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: bar
chart: stable/grafana
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/helmfile.d/c.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: baz
chart: stable/envoy
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/env.values.yaml": `
tillerNs: INLINE_TILLER_NS_2
`,
}
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: "default",
}, files)
processed := []state.ReleaseSpec{}
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
for _, r := range st.Releases {
processed = append(processed, r)
}
return []error{}
}
err := app.VisitDesiredStatesWithReleasesFiltered(
"helmfile.yaml", collectReleases,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
type release struct {
chart string
tillerNs string
ns string
}
expectedReleases := map[string]release{
"foo": {"stable/zipkin", "INLINE_TILLER_NS_2", "INLINE_NS"},
"bar": {"stable/grafana", "INLINE_TILLER_NS", "INLINE_NS"},
"baz": {"stable/envoy", "INLINE_TILLER_NS_3", "INLINE_NS"},
}
for name := range processed {
actual := processed[name]
t.Run(actual.Name, func(t *testing.T) {
expected, ok := expectedReleases[actual.Name]
if !ok {
t.Fatalf("unexpected release processed: %v", actual)
}
if expected.chart != actual.Chart {
t.Errorf("unexpected chart: expected=%s, got=%s", expected.chart, actual.Chart)
}
if expected.tillerNs != actual.TillerNamespace {
t.Errorf("unexpected tiller namespace: expected=%s, got=%s", expected.tillerNs, actual.TillerNamespace)
}
if expected.ns != actual.Namespace {
t.Errorf("unexpected namespace: expected=%s, got=%s", expected.ns, actual.Namespace)
}
})
}
}
// See https://github.com/roboll/helmfile/issues/312
func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) {
files := map[string]string{
@@ -669,6 +839,243 @@ releases:
}
}
func TestVisitDesiredStatesWithReleasesFiltered_EnvironmentValueOverrides(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- values.yaml
---
releases:
- name: {{ .Environment.Values.foo }}-{{ .Environment.Values.bar }}-{{ .Environment.Values.baz }}
chart: stable/zipkin
`,
"/path/to/values.yaml": `
foo: foo
bar: bar
baz: baz
`,
"/path/to/overrides.yaml": `
foo: "foo1"
bar: "bar1"
`,
}
testcases := []struct {
expected string
}{
{expected: "foo1-bar2-baz1"},
}
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
for _, r := range st.Releases {
actual = append(actual, r.Name)
}
return []error{}
}
app := appWithFs(&App{
KubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: false,
Namespace: "",
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]interface{}{"bar": "bar2", "baz": "baz1"},
}, 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] != testcase.expected {
t.Errorf("unexpected result: expected=%s, got=%s", testcase.expected, actual[0])
}
}
}
func TestVisitDesiredStatesWithReleasesFiltered_StateValueOverrides(t *testing.T) {
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
}{
{
expr: envTmplExpr,
env: "default",
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
},
{
expr: envTmplExpr,
env: "production",
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
},
{
expr: relTmplExpr,
env: "default",
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
},
{
expr: relTmplExpr,
env: "production",
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
},
}
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": fmt.Sprintf(`
# The top-level "values" are "base" values has inherited to state values with the lowest priority.
# The lowest priority results in environment-specific values to override values defined in the base.
values:
- values.yaml
environments:
default:
values:
- default.yaml
production:
values:
- production.yaml
---
releases:
- name: %s
chart: %s
namespace: %s
`, testcase.expr, testcase.expr, testcase.expr),
"/path/to/values.yaml": `
x:
foo: foo
bar: bar
baz: baz
hoge: hoge
fuga: fuga
a: []
`,
"/path/to/default.yaml": `
x:
bar: "bar_default"
baz: "baz_default"
a:
- b: []
`,
"/path/to/production.yaml": `
x:
bar: "bar_production"
baz: "baz_production"
a:
- b: []
`,
"/path/to/overrides.yaml": `
x:
baz: baz_override
hoge: hoge_override
a:
- b:
- c: C
`,
}
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: testcase.env,
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]interface{}{"x": map[string]interface{}{"hoge": "hoge_set", "fuga": "fuga_set"}},
}, 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 != testcase.expected {
t.Errorf("unexpected name: expected=%s, got=%s", testcase.expected, actual[0].Name)
}
if actual[0].Chart != testcase.expected {
t.Errorf("unexpected chart: expected=%s, got=%s", testcase.expected, actual[0].Chart)
}
if actual[0].Namespace != testcase.expected {
t.Errorf("unexpected namespace: expected=%s, got=%s", testcase.expected, actual[0].Namespace)
}
})
}
}
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:
@@ -722,7 +1129,7 @@ releases:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
@@ -746,16 +1153,18 @@ helmDefaults:
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
KubeContext: "default",
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
fileExistsAt: testFs.FileExistsAt,
fileExists: testFs.FileExists,
KubeContext: "default",
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fatalf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
@@ -769,6 +1178,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) {
@@ -801,7 +1214,7 @@ releases:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
@@ -825,11 +1238,12 @@ helmDefaults:
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
@@ -839,25 +1253,23 @@ helmDefaults:
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
firstRelease := st.Releases[0]
if firstRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", firstRelease.Name)
}
if st.Releases[1].Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", st.Releases[1].Name)
secondRelease := st.Releases[1]
if secondRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", secondRelease.Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", st.Releases[2].Name)
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if *st.Releases[2].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
if *secondRelease.MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *firstRelease.MissingFileHandler)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
@@ -879,7 +1291,7 @@ releases:
- name: myrelease0
chart: mychart0
`
testFs := state.NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
@@ -900,11 +1312,12 @@ foo: FOO
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
@@ -924,6 +1337,67 @@ foo: FOO
}
}
func TestLoadDesiredStateFromYaml_InlineEnvVals(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
# "envvals inheritance"
# base.gotmpl should be able to reference environment values defined in the base.yaml and default/1.yaml
- ../base.gotmpl
---
releases:
- name: myrelease0
chart: mychart0
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
- tillerNs: INLINE_TILLER_NS
`,
"/path/to/base.gotmpl": `helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/1.yaml": `tillerNs: TILLER_NS
foo: FOO
`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "INLINE_TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
@@ -954,7 +1428,7 @@ releases:
stage: post
<<: *default
`
testFs := state.NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
test:
@@ -978,11 +1452,12 @@ helmDefaults:
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
@@ -993,24 +1468,23 @@ helmDefaults:
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
firstRelease := st.Releases[0]
if firstRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", firstRelease.Name)
}
if st.Releases[1].Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", st.Releases[1].Name)
secondRelease := st.Releases[1]
if secondRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", secondRelease.Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", st.Releases[2].Name)
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if *st.Releases[2].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
if *secondRelease.MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *firstRelease.MissingFileHandler)
}
if st.Releases[2].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
@@ -1040,7 +1514,7 @@ releases:
chart: mychart3
<<: *default
`
testFs := state.NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/yaml/templates.yaml": `templates:
default: &default
@@ -1067,10 +1541,200 @@ releases:
if st.Releases[1].Name != "myrelease2" {
t.Errorf("unexpected releases[0].name: expected=myrelease2, got=%s", st.Releases[1].Name)
}
if st.Releases[2].Name != "myrelease1" {
t.Errorf("unexpected releases[0].name: expected=myrelease1, got=%s", st.Releases[2].Name)
if len(st.Releases) != 2 {
t.Errorf("unexpected number of releases: expected=2, got=%d", len(st.Releases))
}
if st.Releases[3].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[3].Name)
}
// See https://github.com/roboll/helmfile/issues/615
func TestLoadDesiredStateFromYaml_MultiPartTemplate_NoMergeArrayInEnvVal(t *testing.T) {
statePath := "/path/to/helmfile.yaml"
stateContent := `
environments:
default:
values:
- foo: ["foo"]
---
environments:
default:
values:
- foo: ["FOO"]
- 1.yaml
---
environments:
default:
values:
- 2.yaml
---
releases:
- name: {{ .Environment.Values.foo | quote }}
chart: {{ .Environment.Values.bar | quote }}
`
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `bar: ["bar"]`,
"/path/to/2.yaml": `bar: ["BAR"]`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: true,
}
st, err := app.loadDesiredStateFromYaml(statePath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != "[FOO]" {
t.Errorf("unexpected releases[0].name: expected=FOO, got=%s", st.Releases[0].Name)
}
if st.Releases[0].Chart != "[BAR]" {
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
}
}
// See https://github.com/roboll/helmfile/issues/623
func TestLoadDesiredStateFromYaml_MultiPartTemplate_MergeMapsVariousKeys(t *testing.T) {
type testcase struct {
overrideValues interface{}
expected string
}
testcases := []testcase{
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
{map[interface{}]interface{}{"foo": map[interface{}]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
{map[interface{}]interface{}{"foo": map[string]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
{map[interface{}]interface{}{"foo": []interface{}{"foo"}}, `[foo]`},
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
}
for i := range testcases {
tc := testcases[i]
statePath := "/path/to/helmfile.yaml"
stateContent := `
environments:
default:
values:
- 1.yaml
- 2.yaml
---
releases:
- name: {{ .Environment.Values.foo | quote }}
chart: {{ .Environment.Values.bar | quote }}
`
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `bar: ["bar"]`,
"/path/to/2.yaml": `bar: ["BAR"]`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: true,
}
opts := LoadOpts{
CalleePath: statePath,
Environment: state.SubhelmfileEnvironmentSpec{
OverrideValues: []interface{}{tc.overrideValues},
},
}
st, err := app.loadDesiredStateFromYaml(statePath, opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != tc.expected {
t.Errorf("unexpected releases[0].name: expected=%s, got=%s", tc.expected, st.Releases[0].Name)
}
if st.Releases[0].Chart != "[BAR]" {
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
}
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_SprigDictFuncs(t *testing.T) {
type testcase struct {
state string
expr string
expected string
}
stateInline := `
environments:
default:
values:
- foo: FOO
bar: { "baz": "BAZ" }
---
releases:
- name: %s
chart: stable/nginx
`
stateExternal := `
environments:
default:
values:
- 1.yaml
- 2.yaml
---
releases:
- name: %s
chart: stable/nginx
`
testcases := []testcase{
{stateInline, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
{stateInline, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
{stateInline, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
{stateInline, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
{stateInline, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
// This fails when .Environment.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 .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
{stateExternal, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
{stateExternal, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
{stateExternal, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
{stateExternal, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
{stateExternal, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
{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`},
}
for i := range testcases {
tc := testcases[i]
statePath := "/path/to/helmfile.yaml"
stateContent := fmt.Sprintf(tc.state, tc.expr)
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `foo: FOO`,
"/path/to/2.yaml": `bar: { "baz": "BAZ" }
services:
- name: "x"
- name: "y"
something: false
`,
})
app := &App{
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Reverse: true,
}
st, err := app.loadDesiredStateFromYaml(statePath)
if err != nil {
t.Fatalf("unexpected error at %d: %v", i, err)
}
if st.Releases[0].Name != tc.expected {
t.Errorf("unexpected releases[0].name at %d: expected=%s, got=%s", i, tc.expected, st.Releases[0].Name)
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
package main
package app
import (
"bufio"
@@ -11,7 +11,7 @@ import (
// Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
//
// Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
func askForConfirmation(s string) bool {
func AskForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
+132
View File
@@ -0,0 +1,132 @@
package app
import "go.uber.org/zap"
type ConfigProvider interface {
Args() string
HelmBinary() string
FileOrDir() string
KubeContext() string
Namespace() string
Selectors() []string
Set() map[string]interface{}
ValuesFiles() []string
Env() string
loggingConfig
}
type DeprecatedChartsConfigProvider interface {
Values() []string
concurrencyConfig
loggingConfig
}
type DepsConfigProvider interface {
Args() string
}
type ReposConfigProvider interface {
Args() string
}
type ApplyConfigProvider interface {
Args() string
Values() []string
SkipDeps() bool
SuppressSecrets() bool
concurrencyConfig
interactive
loggingConfig
}
type SyncConfigProvider interface {
Args() string
Values() []string
SkipDeps() bool
concurrencyConfig
loggingConfig
}
type DiffConfigProvider interface {
Args() string
Values() []string
SkipDeps() bool
SuppressSecrets() bool
DetailedExitcode() bool
concurrencyConfig
}
type DeleteConfigProvider interface {
Args() string
Purge() bool
interactive
loggingConfig
concurrencyConfig
}
type DestroyConfigProvider interface {
Args() string
interactive
loggingConfig
concurrencyConfig
}
type TestConfigProvider interface {
Args() string
Timeout() int
Cleanup() bool
concurrencyConfig
}
type LintConfigProvider interface {
Args() string
Values() []string
SkipDeps() bool
concurrencyConfig
}
type TemplateConfigProvider interface {
Args() string
Values() []string
SkipDeps() bool
concurrencyConfig
}
type StatusesConfigProvider interface {
Args() string
concurrencyConfig
}
type concurrencyConfig interface {
Concurrency() int
}
type loggingConfig interface {
Logger() *zap.SugaredLogger
}
type interactive interface {
Interactive() bool
}
+3 -1
View File
@@ -1,6 +1,8 @@
package app
import "github.com/roboll/helmfile/state"
import (
"github.com/roboll/helmfile/pkg/state"
)
type Context struct {
updatedRepos map[string]struct{}
+52 -25
View File
@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/state"
"go.uber.org/zap"
"path/filepath"
"sort"
@@ -19,15 +19,38 @@ type desiredStateLoader struct {
env string
namespace string
readFile func(string) ([]byte, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
readFile func(string) ([]byte, error)
fileExists func(string) (bool, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger
}
func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
st, err := ld.loadFile(nil, filepath.Dir(f), filepath.Base(f), true)
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
var overrodeEnv *environment.Environment
args := opts.Environment.OverrideValues
if len(args) > 0 {
if opts.CalleePath == "" {
return nil, fmt.Errorf("bug: opts.CalleePath was nil: f=%s, opts=%v", f, opts)
}
storage := state.NewStorage(opts.CalleePath, ld.logger, ld.glob)
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile, ld.logger)
handler := state.MissingFileHandlerError
vals, err := envld.LoadEnvironmentValues(&handler, args)
if err != nil {
return nil, err
}
overrodeEnv = &environment.Environment{
Name: ld.env,
Values: vals,
}
}
st, err := ld.loadFileWithOverrides(nil, overrodeEnv, filepath.Dir(f), filepath.Base(f), true)
if err != nil {
return nil, err
}
@@ -58,6 +81,10 @@ func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
}
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
return ld.loadFileWithOverrides(inheritedEnv, nil, baseDir, file, evaluateBases)
}
func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
var f string
if filepath.IsAbs(file) {
f = file
@@ -77,6 +104,7 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba
if !experimentalModeEnabled() || ext == ".gotmpl" {
self, err = ld.renderAndLoad(
inheritedEnv,
overrodeEnv,
baseDir,
f,
fileBytes,
@@ -89,6 +117,7 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba
file,
evaluateBases,
inheritedEnv,
overrodeEnv,
)
}
@@ -96,35 +125,32 @@ func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, ba
}
func (a *desiredStateLoader) underlying() *state.StateCreator {
c := state.NewCreator(a.logger, a.readFile, a.abs, a.glob)
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob)
c.LoadFile = a.loadFile
return c
}
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool, env *environment.Environment) (*state.HelmState, error) {
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, env)
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool, env, overrodeEnv *environment.Environment) (*state.HelmState, error) {
merged, err := env.Merge(overrodeEnv)
if err != nil {
return nil, err
}
helmfiles := []state.SubHelmfileSpec{}
for _, hf := range st.Helmfiles {
matches, err := st.ExpandPaths([]string{hf.Path}, a.glob)
if err != nil {
return nil, err
}
for _, match := range matches {
newHelmfile := hf
newHelmfile.Path = match
helmfiles = append(helmfiles, newHelmfile)
}
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, merged)
if err != nil {
return nil, err
}
helmfiles, err := st.ExpandedHelmfiles()
if err != nil {
return nil, err
}
st.Helmfiles = helmfiles
return st, nil
}
func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
parts := bytes.Split(content, []byte("\n---\n"))
var finalState *state.HelmState
@@ -135,13 +161,13 @@ func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDi
id := fmt.Sprintf("%s.part.%d", filename, i)
if env == nil {
if env == nil && overrodeEnv == nil {
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
} else {
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part, *env)
yamlBuf, err = ld.renderTemplatesToYamlWithEnv(baseDir, id, part, env, overrodeEnv)
if err != nil {
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
}
@@ -153,6 +179,7 @@ func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDi
filename,
evaluateBases,
env,
overrodeEnv,
)
if err != nil {
return nil, err
@@ -161,7 +188,7 @@ func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDi
if finalState == nil {
finalState = currentState
} else {
if err := mergo.Merge(finalState, currentState, mergo.WithAppendSlice); err != nil {
if err := mergo.Merge(finalState, currentState, mergo.WithOverride); err != nil {
return nil, err
}
}
+28
View File
@@ -0,0 +1,28 @@
package app
import (
"github.com/roboll/helmfile/pkg/state"
"gopkg.in/yaml.v2"
)
type LoadOpts struct {
Selectors []string
Environment state.SubhelmfileEnvironmentSpec
// CalleePath is the absolute path to the file being loaded
CalleePath string
}
func (o LoadOpts) DeepCopy() LoadOpts {
bytes, err := yaml.Marshal(o)
if err != nil {
panic(err)
}
new := LoadOpts{}
if err := yaml.Unmarshal(bytes, &new); err != nil {
panic(err)
}
return new
}
+306
View File
@@ -0,0 +1,306 @@
package app
import (
"fmt"
"github.com/roboll/helmfile/pkg/argparser"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"strings"
)
type Run struct {
state *state.HelmState
helm helmexec.Interface
ctx Context
Ask func(string) bool
}
func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run {
return &Run{state: st, helm: helm, ctx: ctx}
}
func (r *Run) askForConfirmation(msg string) bool {
if r.Ask != nil {
return r.Ask(msg)
}
return AskForConfirmation(msg)
}
func (r *Run) Deps(c DepsConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
return errs
}
return r.state.UpdateDeps(r.helm)
}
func (r *Run) Repos(c ReposConfigProvider) []error {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.ctx.SyncReposOnce(r.state, r.helm)
}
func (r *Run) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) []error {
st := r.state
helm := r.helm
affectedReleases := state.AffectedReleases{}
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Status(c StatusesConfigProvider) []error {
workers := c.Concurrency()
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.ReleaseStatuses(r.helm, workers)
}
func (r *Run) Delete(c DeleteConfigProvider) []error {
affectedReleases := state.AffectedReleases{}
purge := c.Purge()
errs := []error{}
names := make([]string, len(r.state.Releases))
for i, r := range r.state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), purge)
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Destroy(c DestroyConfigProvider) []error {
errs := []error{}
affectedReleases := state.AffectedReleases{}
names := make([]string, len(r.state.Releases))
for i, r := range r.state.Releases {
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to delete?
Helmfile will delete all your releases, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), true)
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Apply(c ApplyConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
affectedReleases := state.AffectedReleases{}
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
return errs
}
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
detailedExitCode := true
releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false)
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
if err != nil {
errs = append(errs, err)
}
fatalErrs := []error{}
noError := true
for _, e := range errs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
noError = false
fatalErrs = append(fatalErrs, e)
}
default:
noError = false
fatalErrs = append(fatalErrs, e)
}
}
// sync only when there are changes
if noError {
if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
// TODO better way to get the logger
logger := c.Logger()
logger.Infof("")
logger.Infof("No affected releases")
} else {
names := []string{}
for _, r := range releases {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
}
for _, r := range releasesToBeDeleted {
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
}
msg := fmt.Sprintf(`Affected releases are:
%s
Do you really want to apply?
Helmfile will apply all your changes, as shown above.
`, strings.Join(names, "\n"))
interactive := c.Interactive()
if !interactive || interactive && r.askForConfirmation(msg) {
rs := []state.ReleaseSpec{}
for _, r := range releases {
rs = append(rs, *r)
}
for _, r := range releasesToBeDeleted {
rs = append(rs, *r)
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
st.Releases = rs
return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
}
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return fatalErrs
}
func (r *Run) Diff(c DiffConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
return errs
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true)
return errs
}
func (r *Run) Sync(c SyncConfigProvider) []error {
st := r.state
helm := r.helm
ctx := r.ctx
affectedReleases := state.AffectedReleases{}
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
return errs
}
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
affectedReleases.DisplayAffectedReleases(c.Logger())
return errs
}
func (r *Run) Template(c TemplateConfigProvider) []error {
state := r.state
helm := r.helm
ctx := r.ctx
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
return errs
}
args := argparser.GetArgs(c.Args(), state)
return state.TemplateReleases(helm, c.Values(), args, c.Concurrency())
}
func (r *Run) Test(c TestConfigProvider) []error {
cleanup := c.Cleanup()
timeout := c.Timeout()
concurrency := c.Concurrency()
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
return r.state.TestReleases(r.helm, cleanup, timeout, concurrency)
}
func (r *Run) Lint(c LintConfigProvider) []error {
state := r.state
helm := r.helm
ctx := r.ctx
values := c.Values()
args := argparser.GetArgs(c.Args(), state)
workers := c.Concurrency()
if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
return errs
}
if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 {
return errs
}
}
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
return errs
}
return state.LintReleases(helm, values, args, workers)
}
+81 -25
View File
@@ -3,9 +3,10 @@ package app
import (
"bytes"
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/state"
"github.com/roboll/helmfile/tmpl"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/tmpl"
"strings"
)
@@ -18,8 +19,12 @@ func prependLineNumbers(text string) string {
return buf.String()
}
func (r *desiredStateLoader) renderEnvironment(firstPassEnv environment.Environment, baseDir, filename string, content []byte) environment.Environment {
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
tmplData := state.EnvironmentTemplateData{
Environment: *firstPassEnv,
Namespace: r.namespace,
Values: map[string]interface{}{},
}
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
// parse as much as we can, tolerate errors, this is a preparse
@@ -28,13 +33,23 @@ func (r *desiredStateLoader) renderEnvironment(firstPassEnv environment.Environm
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
if yamlBuf == nil { // we have a template syntax error, let the second parse report
r.logger.Debugf("template syntax error: %v", err)
return firstPassEnv
return firstPassEnv, nil
}
}
yamlData := yamlBuf.String()
// Work-around for https://github.com/golang/go/issues/24963
sanitized := strings.ReplaceAll(yamlData, "<no value>", "")
if len(yamlData) != len(sanitized) {
msg := "replaced <no value>s to workaround https://github.com/golang/go/issues/24963 to address https://github.com/roboll/helmfile/issues/553:\n%s"
r.logger.Debugf(msg, cmp.Diff(yamlData, sanitized))
}
c := r.underlying()
c.Strict = false
// create preliminary state, as we may have an environment. Tolerate errors.
prestate, err := c.ParseAndLoad(yamlBuf.Bytes(), baseDir, filename, r.env, false, &firstPassEnv)
prestate, err := c.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
if err != nil && r.logger != nil {
switch err.(type) {
case *state.StateLoadError:
@@ -44,36 +59,77 @@ func (r *desiredStateLoader) renderEnvironment(firstPassEnv environment.Environm
}
if prestate != nil {
firstPassEnv = prestate.Env
}
return firstPassEnv
}
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte, context ...environment.Environment) (*bytes.Buffer, error) {
var env environment.Environment
if len(context) > 0 {
env = context[0]
} else {
env = environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
firstPassEnv = &prestate.Env
}
return r.twoPassRenderTemplateToYaml(env, baseDir, filename, content)
return firstPassEnv, prestate
}
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(initEnv environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
type RenderOpts struct {
}
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte) (*bytes.Buffer, error) {
env := &environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
return r.renderTemplatesToYamlWithEnv(baseDir, filename, content, env, nil)
}
func (r *desiredStateLoader) renderTemplatesToYamlWithEnv(baseDir, filename string, content []byte, inherited, overrode *environment.Environment) (*bytes.Buffer, error) {
return r.twoPassRenderTemplateToYaml(inherited, overrode, baseDir, filename, content)
}
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
// try a first pass render. This will always succeed, but can produce a limited env
if r.logger != nil {
r.logger.Debugf("first-pass rendering input of \"%s\": %v", filename, initEnv)
r.logger.Debugf("first-pass rendering starting for \"%s\": inherited=%v, overrode=%v", filename, inherited, overrode)
}
firstPassEnv := r.renderEnvironment(initEnv, baseDir, filename, content)
initEnv, err := inherited.Merge(overrode)
if err != nil {
return nil, err
}
if r.logger != nil {
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, firstPassEnv)
r.logger.Debugf("first-pass uses: %v", initEnv)
}
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
renderedEnv, prestate := r.renderPrestate(initEnv, baseDir, filename, content)
if r.logger != nil {
r.logger.Debugf("first-pass produced: %v", renderedEnv)
}
finalEnv, err := inherited.Merge(renderedEnv)
if err != nil {
return nil, err
}
finalEnv, err = finalEnv.Merge(overrode)
if err != nil {
return nil, err
}
if r.logger != nil {
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, *finalEnv)
}
vals := map[string]interface{}{}
if prestate != nil {
prestate.Env = *finalEnv
vals, err = prestate.Values()
if err != nil {
return nil, err
}
}
if prestate != nil {
r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
}
tmplData := state.EnvironmentTemplateData{
Environment: *finalEnv,
Namespace: r.namespace,
Values: vals,
}
secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
if err != nil {
+12 -10
View File
@@ -1,24 +1,26 @@
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/helmexec"
"github.com/roboll/helmfile/state"
"gopkg.in/yaml.v2"
)
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) {
testfs := state.NewTestFs(files)
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
testfs := testhelper.NewTestFs(files)
return &desiredStateLoader{
env: env,
namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"),
readFile: testfs.ReadFile,
abs: testfs.Abs,
glob: testfs.Glob,
env: env,
namespace: "namespace",
logger: helmexec.NewLogger(os.Stdout, "debug"),
readFile: testfs.ReadFile,
fileExists: testfs.FileExists,
abs: testfs.Abs,
glob: testfs.Glob,
}, testfs
}
+2 -7
View File
@@ -1,10 +1,9 @@
package args
package argparser
import (
"fmt"
"github.com/roboll/helmfile/pkg/state"
"strings"
"github.com/roboll/helmfile/state"
)
type keyVal struct {
@@ -83,10 +82,6 @@ func GetArgs(args string, state *state.HelmState) []string {
}
}
if state.HelmDefaults.KubeContext != "" {
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
}
var argArr []string
for _, flag := range argsMap.flags {
@@ -1,10 +1,9 @@
package args
package argparser
import (
"github.com/roboll/helmfile/pkg/state"
"strings"
"testing"
"github.com/roboll/helmfile/state"
)
func TestGetArgs(t *testing.T) {
@@ -14,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, " "))
@@ -28,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, " "))
+66
View File
@@ -0,0 +1,66 @@
package environment
import (
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/maputil"
"gopkg.in/yaml.v2"
)
type Environment struct {
Name string
Values map[string]interface{}
Defaults map[string]interface{}
}
var EmptyEnvironment Environment
func (e Environment) DeepCopy() Environment {
valuesBytes, err := yaml.Marshal(e.Values)
if err != nil {
panic(err)
}
var values map[string]interface{}
if err := yaml.Unmarshal(valuesBytes, &values); err != nil {
panic(err)
}
values, err = maputil.CastKeysToStrings(values)
if err != nil {
panic(err)
}
defaultsBytes, err := yaml.Marshal(e.Defaults)
if err != nil {
panic(err)
}
var defaults map[string]interface{}
if err := yaml.Unmarshal(defaultsBytes, &defaults); err != nil {
panic(err)
}
defaults, err = maputil.CastKeysToStrings(defaults)
if err != nil {
panic(err)
}
return Environment{
Name: e.Name,
Values: values,
Defaults: defaults,
}
}
func (e *Environment) Merge(other *Environment) (*Environment, error) {
if e == nil {
if other != nil {
copy := other.DeepCopy()
return &copy, nil
}
return nil, nil
}
copy := e.DeepCopy()
if other != nil {
if err := mergo.Merge(&copy, other, mergo.WithOverride); err != nil {
return nil, err
}
}
return &copy, nil
}
+15 -8
View File
@@ -2,18 +2,21 @@ package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
"go.uber.org/zap"
"os"
"strings"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/tmpl"
"go.uber.org/zap"
)
type Hook struct {
Name string `yaml:"name"`
Events []string `yaml:"events"`
Command string `yaml:"command"`
Args []string `yaml:"args"`
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
View File
@@ -2,10 +2,13 @@ package event
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/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())
}
}
}
+17 -6
View File
@@ -5,6 +5,7 @@ import (
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
@@ -94,7 +95,7 @@ func (helm *execer) UpdateDeps(chart string) error {
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)
helm.info(out)
return err
}
@@ -130,11 +131,15 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
helm.decryptionMutex.Lock()
defer helm.decryptionMutex.Unlock()
helm.logger.Infof("Decrypting secret %v", name)
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", name), flags...), env)
helm.write(out)
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
helm.info(out)
if err != nil {
return "", err
}
@@ -145,10 +150,16 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
}
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
decFilename := name + ".dec"
decFile, err := os.Open(decFilename)
if err != nil {
return "", err
@@ -181,7 +192,7 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags .
helm.logger.Infof("Comparing %v %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
@@ -2,8 +2,10 @@ package helmexec
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"reflect"
"testing"
@@ -213,10 +215,14 @@ func Test_DecryptSecret(t *testing.T) {
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "dev")
helm.DecryptSecret(HelmContext{}, "secretName")
expected := `Decrypting secret secretName
exec: helm secrets dec secretName --kube-context dev
exec: helm secrets dec secretName --kube-context dev:
`
cwd, err := filepath.Abs(".")
if err != nil {
t.Errorf("Error: %v", err)
}
expected := fmt.Sprintf(`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)
if buffer.String() != expected {
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
@@ -228,8 +234,8 @@ func Test_DiffRelease(t *testing.T) {
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:
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)
@@ -238,8 +244,8 @@ 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:
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)
@@ -252,8 +258,8 @@ func Test_DiffReleaseTillerless(t *testing.T) {
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:
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)
+86
View File
@@ -0,0 +1,86 @@
package maputil
import "fmt"
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
new := map[string]interface{}{}
switch src := s.(type) {
case map[interface{}]interface{}:
for k, v := range src {
var str_k string
switch typed_k := k.(type) {
case string:
str_k = typed_k
default:
return nil, fmt.Errorf("unexpected type of key in map: expected string, got %T: value=%v, map=%v", typed_k, typed_k, src)
}
casted_v, err := recursivelyStringifyMapKey(v)
if err != nil {
return nil, err
}
new[str_k] = casted_v
}
case map[string]interface{}:
for k, v := range src {
casted_v, err := recursivelyStringifyMapKey(v)
if err != nil {
return nil, err
}
new[k] = casted_v
}
}
return new, nil
}
func recursivelyStringifyMapKey(v interface{}) (interface{}, error) {
var casted_v interface{}
switch typed_v := v.(type) {
case map[interface{}]interface{}, map[string]interface{}:
tmp, err := CastKeysToStrings(typed_v)
if err != nil {
return nil, err
}
casted_v = tmp
case []interface{}:
a := []interface{}{}
for i := range typed_v {
res, err := recursivelyStringifyMapKey(typed_v[i])
if err != nil {
return nil, err
}
a = append(a, res)
}
casted_v = a
default:
casted_v = typed_v
}
return casted_v, nil
}
func Set(m map[string]interface{}, key []string, value string) map[string]interface{} {
if len(key) == 0 {
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
}
k := key[0]
if len(key) == 1 {
m[k] = value
return m
}
remain := key[1:]
nested, ok := m[k]
if !ok {
new_m := map[string]interface{}{}
nested = Set(new_m, remain, value)
}
m[k] = nested
return m
}
+61
View File
@@ -0,0 +1,61 @@
package maputil
import "testing"
func TestMapUtil_StrKeys(t *testing.T) {
m := map[string]interface{}{
"a": []interface{}{
map[string]interface{}{
"b": []interface{}{
map[string]interface{}{
"c": "C",
},
},
},
},
}
r, err := CastKeysToStrings(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
a := r["a"].([]interface{})
a0 := a[0].(map[string]interface{})
b := a0["b"].([]interface{})
b0 := b[0].(map[string]interface{})
c := b0["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
func TestMapUtil_IFKeys(t *testing.T) {
m := map[interface{}]interface{}{
"a": []interface{}{
map[interface{}]interface{}{
"b": []interface{}{
map[interface{}]interface{}{
"c": "C",
},
},
},
},
}
r, err := CastKeysToStrings(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
a := r["a"].([]interface{})
a0 := a[0].(map[string]interface{})
b := a0["b"].([]interface{})
b0 := b[0].(map[string]interface{})
c := b0["c"]
if c != "C" {
t.Errorf("unexpected c: expected=C, got=%s", c)
}
}
+254
View File
@@ -0,0 +1,254 @@
package remote
import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/helper/url"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"path/filepath"
"strings"
)
const DefaultCacheDir = ".helmfile/cache"
type Remote struct {
Logger *zap.SugaredLogger
// Home is the home directory for helmfile. Usually this points to $HOME of the user running helmfile.
// Helmfile saves fetched remote files into .helmfile/cache under home
Home string
// Getter is the underlying implementation of getter used for fetching remote files
Getter Getter
// ReadFile is the implementation of the file reader that reads a local file from the specified path.
// Inject any implementation of your choice, like an im-memory impl for testing, ioutil.ReadFile for the real-world use.
ReadFile func(string) ([]byte, error)
DirExists func(string) bool
FileExists func(string) bool
}
func (r *Remote) Unmarshal(src string, dst interface{}) error {
bytes, err := r.GetBytes(src)
if err != nil {
return err
}
strs := strings.Split(src, "/")
file := strs[len(strs)-1]
ext := filepath.Ext(file)
{
r.Logger.Debugf("unmarshalling %s", string(bytes))
var err error
switch ext {
case "json":
err = json.Unmarshal(bytes, dst)
default:
err = yaml.Unmarshal(bytes, dst)
}
r.Logger.Debugf("unmarshalled to %v", dst)
if err != nil {
return err
}
}
return nil
}
func (r *Remote) GetBytes(goGetterSrc string) ([]byte, error) {
f, err := r.Fetch(goGetterSrc)
if err != nil {
return nil, err
}
bytes, err := r.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("read file: %v", err)
}
return bytes, nil
}
// Locate takes an URL to a remote file or a path to a local file.
// If the argument was an URL, it fetches the remote directory contained within the URL,
// and returns the path to the file in the fetched directory
func (r *Remote) Locate(urlOrPath string) (string, error) {
if r.FileExists(urlOrPath) || r.DirExists(urlOrPath) {
return urlOrPath, nil
}
fetched, err := r.Fetch(urlOrPath)
if err != nil {
switch err.(type) {
case InvalidURLError:
return urlOrPath, nil
}
return "", err
}
return fetched, nil
}
type InvalidURLError struct {
err string
}
func (e InvalidURLError) Error() string {
return e.err
}
type Source struct {
Getter, Scheme, User, Host, Dir, File, RawQuery string
}
func IsRemote(goGetterSrc string) bool {
if _, err := Parse(goGetterSrc); err != nil {
return false
}
return true
}
func Parse(goGetterSrc string) (*Source, error) {
items := strings.Split(goGetterSrc, "::")
var getter string
switch len(items) {
case 2:
getter = items[0]
goGetterSrc = items[1]
}
u, err := url.Parse(goGetterSrc)
if err != nil {
return nil, InvalidURLError{err: fmt.Sprintf("parse url: %v", err)}
}
if u.Scheme == "" {
return nil, InvalidURLError{err: fmt.Sprintf("parse url: missing scheme - probably this is a local file path? %s", goGetterSrc)}
}
pathComponents := strings.Split(u.Path, "@")
if len(pathComponents) != 2 {
return nil, fmt.Errorf("invalid src format: it must be `[<getter>::]<scheme>://<host>/<path/to/dir>@<path/to/file>?key1=val1&key2=val2: got %s", goGetterSrc)
}
return &Source{
Getter: getter,
User: u.User.String(),
Scheme: u.Scheme,
Host: u.Host,
Dir: pathComponents[0],
File: pathComponents[1],
RawQuery: u.RawQuery,
}, nil
}
func (r *Remote) Fetch(goGetterSrc string) (string, error) {
u, err := Parse(goGetterSrc)
if err != nil {
return "", err
}
srcDir := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
file := u.File
r.Logger.Debugf("getter: %s", u.Getter)
r.Logger.Debugf("scheme: %s", u.Scheme)
r.Logger.Debugf("user: %s", u.User)
r.Logger.Debugf("host: %s", u.Host)
r.Logger.Debugf("dir: %s", u.Dir)
r.Logger.Debugf("file: %s", u.File)
// This should be shared across variant commands, so that they can share cache for the shared imports
cacheBaseDir := DefaultCacheDir
query := u.RawQuery
var cacheKey string
replacer := strings.NewReplacer(":", "", "//", "_", "/", "_", ".", "_")
dirKey := replacer.Replace(srcDir)
if len(query) > 0 {
paramsKey := strings.Replace(query, "&", "_", -1)
cacheKey = fmt.Sprintf("%s.%s", dirKey, paramsKey)
} else {
cacheKey = dirKey
}
cached := false
getterDst := filepath.Join(cacheBaseDir, cacheKey)
cacheDirPath := filepath.Join(r.Home, getterDst)
r.Logger.Debugf("home: %s", r.Home)
r.Logger.Debugf("getter dest: %s", getterDst)
r.Logger.Debugf("cached dir: %s", cacheDirPath)
{
if r.FileExists(cacheDirPath) {
return "", fmt.Errorf("%s is not directory. please remove it so that variant could use it for dependency caching", getterDst)
}
if r.DirExists(cacheDirPath) {
cached = true
}
}
if !cached {
var getterSrc string
if u.User != "" {
getterSrc = fmt.Sprintf("%s://%s@%s%s", u.Scheme, u.User, u.Host, u.Dir)
} else {
getterSrc = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
}
if len(query) > 0 {
getterSrc = strings.Join([]string{getterSrc, query}, "?")
}
if u.Getter != "" {
getterSrc = u.Getter + "::" + getterSrc
}
r.Logger.Debugf("downloading %s to %s", getterSrc, getterDst)
if err := r.Getter.Get(r.Home, getterSrc, getterDst); err != nil {
return "", err
}
}
return filepath.Join(cacheDirPath, file), nil
}
type Getter interface {
Get(wd, src, dst string) error
}
type GoGetter struct {
Logger *zap.SugaredLogger
}
func (g *GoGetter) Get(wd, src, dst string) error {
ctx := context.Background()
get := &getter.Client{
Ctx: ctx,
Src: src,
Dst: dst,
Pwd: wd,
Mode: getter.ClientModeDir,
Options: []getter.ClientOption{},
}
g.Logger.Debugf("client: %+v", *get)
if err := get.Get(); err != nil {
return fmt.Errorf("get: %v", err)
}
return nil
}
+165
View File
@@ -0,0 +1,165 @@
package remote
import (
"fmt"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"os"
"testing"
)
func TestRemote_HttpsGitHub(t *testing.T) {
cleanfs := map[string]string{
"path/to/home": "",
}
cachefs := map[string]string{
"path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
}
type testcase struct {
files map[string]string
expectCacheHit bool
}
testcases := []testcase{
{files: cleanfs, expectCacheHit: false},
{files: cachefs, expectCacheHit: true},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
testfs := testhelper.NewTestFs(testcase.files)
hit := true
get := func(wd, src, dst string) error {
if wd != "path/to/home" {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0" {
return fmt.Errorf("unexpected src: %s", src)
}
hit = false
return nil
}
getter := &testGetter{
get: get,
}
remote := &Remote{
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Home: "path/to/home",
Getter: getter,
ReadFile: testfs.ReadFile,
FileExists: testfs.FileExistsAt,
DirExists: testfs.DirectoryExistsAt,
}
// FYI, go-getter in the `dir` mode accepts URL like the below. So helmfile expects URLs similar to it:
// go-getter -mode dir git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0 gettertest1/b
// We use `@` to separate dir and the file path. This is a good idea borrowed from helm-git:
// https://github.com/aslafy-z/helm-git
url := "git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
file, err := remote.Locate(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if file != "path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
t.Errorf("unexpected file located: %s", file)
}
if testcase.expectCacheHit && !hit {
t.Errorf("unexpected result: unexpected cache miss")
}
if !testcase.expectCacheHit && hit {
t.Errorf("unexpected result: unexpected cache hit")
}
})
}
}
func TestRemote_SShGitHub(t *testing.T) {
cleanfs := map[string]string{
"path/to/home": "",
}
cachefs := map[string]string{
"path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
}
type testcase struct {
files map[string]string
expectCacheHit bool
}
testcases := []testcase{
{files: cleanfs, expectCacheHit: false},
{files: cachefs, expectCacheHit: true},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
testfs := testhelper.NewTestFs(testcase.files)
hit := true
get := func(wd, src, dst string) error {
if wd != "path/to/home" {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::ssh://git@github.com/cloudposse/helmfiles.git?ref=0.40.0" {
return fmt.Errorf("unexpected src: %s", src)
}
hit = false
return nil
}
getter := &testGetter{
get: get,
}
remote := &Remote{
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Home: "path/to/home",
Getter: getter,
ReadFile: testfs.ReadFile,
FileExists: testfs.FileExistsAt,
DirExists: testfs.DirectoryExistsAt,
}
url := "git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
file, err := remote.Locate(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if file != "path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
t.Errorf("unexpected file located: %s", file)
}
if testcase.expectCacheHit && !hit {
t.Errorf("unexpected result: unexpected cache miss")
}
if !testcase.expectCacheHit && hit {
t.Errorf("unexpected result: unexpected cache hit")
}
})
}
}
type testGetter struct {
get func(wd, src, dst string) error
}
func (t *testGetter) Get(wd, src, dst string) error {
return t.get(wd, src, dst)
}
@@ -3,12 +3,13 @@ package state
import (
"fmt"
"github.com/Masterminds/semver"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/helmexec"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
)
@@ -49,6 +50,8 @@ type ChartRequirements struct {
type ChartLockedRequirements struct {
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
Digest string `yaml:"digest"`
Generated string `yaml:"generated"`
}
func (d *UnresolvedDependencies) Add(chart, url, versionConstraint string) error {
@@ -125,21 +128,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 {
@@ -328,6 +316,22 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
return nil, err
}
// Sort requirements alphabetically by name.
lockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
return nil, err
}
sort.Slice(lockedReqs.ResolvedDependencies, func(i, j int) bool {
return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName
})
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
if err != nil {
return nil, err
}
// Commit the lock file if and only if everything looks ok
if err := m.writeBytes(lockFile, updatedLockFileContent); err != nil {
return nil, err
+72 -82
View File
@@ -4,17 +4,14 @@ import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/tmpl"
"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 {
@@ -35,23 +32,25 @@ func (e *UndefinedEnvError) Error() string {
}
type StateCreator struct {
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
logger *zap.SugaredLogger
readFile func(string) ([]byte, error)
fileExists func(string) (bool, error)
abs func(string) (string, error)
glob func(string) ([]string, error)
Strict bool
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
}
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, 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)) *StateCreator {
return &StateCreator{
logger: logger,
readFile: readFile,
abs: abs,
glob: glob,
Strict: true,
logger: logger,
readFile: readFile,
fileExists: fileExists,
abs: abs,
glob: glob,
Strict: true,
}
}
@@ -102,17 +101,8 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
state.readFile = c.readFile
state.removeFile = os.Remove
state.fileExists = func(path string) (bool, error) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
state.fileExists = c.fileExists
state.glob = c.glob
return &state, nil
}
@@ -125,6 +115,12 @@ func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *envi
if err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
e.Defaults, err = state.loadValuesEntries(nil, state.DefaultValues)
if err != nil {
return nil, err
}
state.Env = *e
return &state, nil
@@ -148,7 +144,14 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
return nil, err
}
return c.LoadEnvValues(state, envName, envValues)
state, err = c.LoadEnvValues(state, envName, envValues)
if err != nil {
return nil, err
}
state.FilePath = file
return state, nil
}
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
@@ -171,67 +174,33 @@ func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmSta
return layers[0], nil
}
func (st *HelmState) ExpandPaths(patterns []string, glob func(string) ([]string, error)) ([]string, error) {
result := []string{}
for _, globPattern := range patterns {
var absPathPattern string
if filepath.IsAbs(globPattern) {
absPathPattern = globPattern
} else {
absPathPattern = st.JoinBase(globPattern)
}
matches, err := glob(absPathPattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("no file matching %s found", globPattern)
}
sort.Strings(matches)
result = append(result, matches...)
}
return result, nil
}
func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name]
if ok {
valuesFiles, err := st.ExpandPaths(envSpec.Values, glob)
var err error
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values)
if err != nil {
return nil, err
}
for _, envvalFullPath := range valuesFiles {
tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
r := tmpl.NewFileRenderer(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)
}
}
if len(envSpec.Secrets) > 0 {
secretsFiles, err := st.ExpandPaths(envSpec.Secrets, glob)
if err != nil {
return nil, err
}
helm := helmexec.New(st.logger, "")
for _, path := range secretsFiles {
if _, err := os.Stat(path); os.IsNotExist(err) {
var envSecretFiles []string
for _, urlOrPath := range envSpec.Secrets {
resolved, skipped, err := st.storage().resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
continue
}
envSecretFiles = append(envSecretFiles, resolved...)
}
for _, path := range envSecretFiles {
// Work-around to allow decrypting environment secrets
//
// We don't have releases loaded yet and therefore unable to decide whether
@@ -241,7 +210,7 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
// installed on the cluster, just for decrypting secrets!
// Related: https://github.com/futuresimple/helm-secrets/issues/83
release := &ReleaseSpec{}
flags := st.appendTillerFlags([]string{}, release)
flags := st.appendConnectionFlags([]string{}, release)
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
if err != nil {
return nil, err
@@ -254,7 +223,14 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
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 {
// 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(&envVals, &vals, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", path, err)
}
}
@@ -268,7 +244,7 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
if ctxEnv != nil {
intEnv := *ctxEnv
if err := mergo.Merge(&intEnv, newEnv, mergo.WithAppendSlice); err != nil {
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
}
@@ -277,3 +253,17 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
return newEnv, nil
}
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}) (map[string]interface{}, error) {
envVals := map[string]interface{}{}
valuesEntries := append([]interface{}{}, entries...)
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger)
var err error
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
if err != nil {
return nil, err
}
return envVals, nil
}
@@ -1,6 +1,7 @@
package state
import (
"github.com/roboll/helmfile/pkg/testhelper"
"go.uber.org/zap"
"io/ioutil"
"path/filepath"
@@ -98,7 +99,7 @@ bar: {{ readFile "bar.txt" }}
expectedValues := `env: production`
testFs := NewTestFs(map[string]string{
testFs := testhelper.NewTestFs(map[string]string{
fooYamlFile: string(fooYamlContent),
barYamlFile: string(barYamlContent),
barTextFile: string(barTextContent),
@@ -106,7 +107,7 @@ bar: {{ readFile "bar.txt" }}
})
testFs.Cwd = "/example/path/to"
state, err := NewCreator(logger, testFs.ReadFile, 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).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
+15
View File
@@ -0,0 +1,15 @@
package state
type EnvironmentSpec struct {
Values []interface{} `yaml:"values"`
Secrets []string `yaml:"secrets"`
// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
// under `environments.NAME.values`.
//
// Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
//
// Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
// a message about the missing file at the log-level.
MissingFileHandler *string `yaml:"missingFileHandler"`
}
+83
View File
@@ -0,0 +1,83 @@
package state
import (
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/tmpl"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"path/filepath"
)
type EnvironmentValuesLoader struct {
storage *Storage
readFile func(string) ([]byte, error)
logger *zap.SugaredLogger
}
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger) *EnvironmentValuesLoader {
return &EnvironmentValuesLoader{
storage: storage,
readFile: readFile,
logger: logger,
}
}
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []interface{}) (map[string]interface{}, error) {
result := map[string]interface{}{}
for _, entry := range valuesEntries {
maps := []interface{}{}
switch strOrMap := entry.(type) {
case string:
urlOrPath := strOrMap
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
continue
}
for _, f := range files {
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
bytes, err := r.RenderToBytes(f)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
}
maps = append(maps, m)
if ld.logger != nil {
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
}
}
case map[interface{}]interface{}:
maps = append(maps, strOrMap)
default:
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
}
for _, m := range maps {
// All the nested map key should be string. Otherwise we get strange errors due to that
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
// See https://github.com/roboll/helmfile/issues/677
vals, err := maputil.CastKeysToStrings(m)
if err != nil {
return nil, err
}
if err := mergo.Merge(&result, &vals, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
}
}
}
return result, nil
}
+51
View File
@@ -0,0 +1,51 @@
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)
}
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
}
+18 -1
View File
@@ -2,7 +2,8 @@ package state
import (
"fmt"
"github.com/roboll/helmfile/tmpl"
"github.com/roboll/helmfile/pkg/tmpl"
"gopkg.in/yaml.v2"
)
@@ -15,6 +16,14 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
return nil, fmt.Errorf("failed executing template expressions in release \"%s\": %v", r.Name, err)
}
{
ts := result.Name
result.Name, err = renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".name = \"%s\": %v", r.Name, ts, err)
}
}
{
ts := result.Chart
result.Chart, err = renderer.RenderTemplateContentToString([]byte(ts))
@@ -31,6 +40,14 @@ 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)
}
}
for i, t := range result.Values {
switch ts := t.(type) {
case string:
@@ -1,6 +1,8 @@
package state
import "fmt"
import (
"fmt"
)
const ReleaseErrorCodeFailure = 1
+171 -90
View File
@@ -11,15 +11,14 @@ import (
"strconv"
"strings"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/environment"
"github.com/roboll/helmfile/pkg/event"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/tmpl"
"regexp"
"net/url"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/event"
"github.com/roboll/helmfile/tmpl"
"github.com/tatsushid/go-prettytable"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
@@ -27,9 +26,13 @@ import (
// HelmState structure for the helmfile
type HelmState struct {
basePath string
Environments map[string]EnvironmentSpec
FilePath string
basePath string
FilePath string
// DefaultValues is the default values to be overrode by environment values and command-line overrides
DefaultValues []interface{} `yaml:"values"`
Environments map[string]EnvironmentSpec `yaml:"environments"`
Bases []string `yaml:"bases"`
HelmDefaults HelmSpec `yaml:"helmDefaults"`
@@ -51,6 +54,7 @@ type HelmState struct {
removeFile func(string) error
fileExists func(string) (bool, error)
glob func(string) ([]string, error)
tempDir func(string, string) (string, error)
runner helmexec.Runner
@@ -61,6 +65,12 @@ 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
Environment SubhelmfileEnvironmentSpec
}
type SubhelmfileEnvironmentSpec struct {
OverrideValues []interface{} `yaml:"values"`
}
// HelmSpec to defines helmDefault values
@@ -143,11 +153,18 @@ type ReleaseSpec struct {
TillerNamespace string `yaml:"tillerNamespace"`
Tillerless *bool `yaml:"tillerless"`
KubeContext string `yaml:"kubeContext"`
TLS *bool `yaml:"tls"`
TLSCACert string `yaml:"tlsCACert"`
TLSKey string `yaml:"tlsKey"`
TLSCert string `yaml:"tlsCert"`
// These settings requires helm-x integration to work
Dependencies []Dependency `yaml:"dependencies"`
JSONPatches []interface{} `yaml:"jsonPatches"`
StrategicMergePatches []interface{} `yaml:"strategicMergePatches"`
// generatedValues are values that need cleaned up on exit
generatedValues []string
//version of the chart that has really been installed cause desired version may be fuzzy (~2.0.0)
@@ -171,6 +188,11 @@ type AffectedReleases struct {
const DefaultEnv = "default"
const MissingFileHandlerError = "Error"
const MissingFileHandlerInfo = "Info"
const MissingFileHandlerWarn = "Warn"
const MissingFileHandlerDebug = "Debug"
func (st *HelmState) applyDefaultsTo(spec *ReleaseSpec) {
if st.Namespace != "" {
spec.Namespace = st.Namespace
@@ -239,6 +261,17 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
for release := range jobs {
st.applyDefaultsTo(release)
// If `installed: false`, the only potential operation on this release would be uninstalling.
// We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those.
// The values files are for `helm upgrade -f values.yaml` calls that happens when the release has `installed: true`.
// This logic addresses:
// - https://github.com/roboll/helmfile/issues/519
// - https://github.com/roboll/helmfile/issues/616
if !release.Desired() {
results <- syncPrepareResult{release: release, flags: []string{}, errors: []*ReleaseError{}}
continue
}
flags, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
if flagsErr != nil {
results <- syncPrepareResult{errors: []*ReleaseError{newReleaseError(release, flagsErr)}}
@@ -287,7 +320,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 := helm.List(context, "^"+release.Name+"$", st.connectionFlags(&release)...)
if err != nil {
return false, err
} else if out != "" {
@@ -298,7 +331,9 @@ func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helme
func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*ReleaseSpec, error) {
detected := []*ReleaseSpec{}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
if err != nil {
@@ -348,7 +383,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 {
@@ -405,9 +441,10 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
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 := helm.List(context, "^"+release.Name+"$", st.connectionFlags(release)...); err == nil {
chartName := filepath.Base(release.Chart)
pat := regexp.MustCompile(chartName + "-(.*?)\\s")
//the regexp without escapes : .*\s.*\s.*\s.*\schartName-(.*?)\s
pat := regexp.MustCompile(".*\\s.*\\s.*\\s.*\\s" + chartName + "-(.*?)\\s")
versions := pat.FindStringSubmatch(out)
if len(versions) > 0 {
return versions[1], nil
@@ -491,6 +528,9 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr
// TemplateReleases wrapper for executing helm template on the releases
func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error {
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
helm.SetExtraArgs()
errs := []error{}
// Create tmp directory and bail immediately if it fails
dir, err := ioutil.TempDir("", "")
@@ -511,7 +551,9 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
helm.SetExtraArgs(args...)
}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
continue
}
@@ -552,6 +594,9 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
// LintReleases wrapper for executing helm lint on the releases
func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error {
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
helm.SetExtraArgs()
errs := []error{}
// Create tmp directory and bail immediately if it fails
dir, err := ioutil.TempDir("", "")
@@ -571,7 +616,9 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
helm.SetExtraArgs(args...)
}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if !release.Desired() {
continue
}
@@ -791,15 +838,15 @@ 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
}
@@ -808,7 +855,7 @@ func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm hel
if purge {
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)
@@ -840,7 +887,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
flags = append(flags, "--cleanup")
}
flags = append(flags, "--timeout", strconv.Itoa(timeout))
flags = st.appendTillerFlags(flags, &release)
flags = st.appendConnectionFlags(flags, &release)
return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...)
})
@@ -910,7 +957,9 @@ func (st *HelmState) FilterReleases() error {
func (st *HelmState) PrepareReleases(helm helmexec.Interface, helmfileCommand string) []error {
errs := []error{}
for _, release := range st.Releases {
for i := range st.Releases {
release := st.Releases[i]
if _, err := st.triggerPrepareEvent(&release, helmfileCommand); err != nil {
errs = append(errs, newReleaseError(&release, err))
continue
@@ -1014,21 +1063,6 @@ func (st *HelmState) BuildDeps(helm helmexec.Interface) []error {
return nil
}
// JoinBase returns an absolute path in the form basePath/relative
func (st *HelmState) JoinBase(relPath string) string {
return filepath.Join(st.basePath, relPath)
}
// normalizes relative path to absolute one
func (st *HelmState) normalizePath(path string) string {
u, _ := url.Parse(path)
if u.Scheme != "" || filepath.IsAbs(path) {
return path
} else {
return st.JoinBase(path)
}
}
// 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`.
@@ -1039,17 +1073,6 @@ func normalizeChart(basePath, chart string) string {
}
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
@@ -1085,15 +1108,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 {
@@ -1127,6 +1151,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
@@ -1170,7 +1200,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 {
@@ -1183,6 +1219,13 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
flags := []string{
"--name", release.Name,
}
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
@@ -1200,7 +1243,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 {
@@ -1219,41 +1268,77 @@ 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) {
r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.envTemplateData())
r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.valuesFileTemplateData())
return r.RenderToBytes(path)
}
func (st *HelmState) storage() *Storage {
return &Storage{
FilePath: st.FilePath,
basePath: st.basePath,
glob: st.glob,
logger: st.logger,
}
}
func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) {
helmfiles := []SubHelmfileSpec{}
for _, hf := range st.Helmfiles {
if remote.IsRemote(hf.Path) {
helmfiles = append(helmfiles, hf)
continue
}
matches, err := st.storage().ExpandPaths(hf.Path)
if err != nil {
return nil, err
}
if len(matches) == 0 {
return nil, fmt.Errorf("no file matching %s found", hf.Path)
}
for _, match := range matches {
newHelmfile := hf
newHelmfile.Path = match
helmfiles = append(helmfiles, newHelmfile)
}
}
return helmfiles, nil
}
func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingFileHandler *string) ([]string, error) {
generatedFiles := []string{}
for _, value := range values {
switch typedValue := value.(type) {
case string:
path := st.normalizePath(typedValue)
ok, err := st.fileExists(path)
paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", typedValue)
if err != nil {
return nil, err
}
if !ok {
if missingFileHandler == nil || *missingFileHandler == "Error" {
return nil, fmt.Errorf("file does not exist: %s", path)
} else if *missingFileHandler == "Warn" {
st.logger.Warnf("skipping missing values file \"%s\"", path)
continue
} else if *missingFileHandler == "Info" {
st.logger.Infof("skipping missing values file \"%s\"", path)
continue
} else {
st.logger.Debugf("skipping missing values file \"%s\"", path)
continue
}
if skip {
continue
}
if len(paths) > 1 {
return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary")
}
path := paths[0]
yamlBytes, err := st.RenderValuesFileToBytes(path)
if err != nil {
return nil, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
@@ -1283,7 +1368,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
}
generatedFiles = append(generatedFiles, valfile.Name())
default:
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
}
}
return generatedFiles, nil
@@ -1299,7 +1384,7 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
for _, v := range release.Values {
switch typedValue := v.(type) {
case string:
path := st.normalizePath(release.ValuesPathPrefix + typedValue)
path := st.storage().normalizePath(release.ValuesPathPrefix + typedValue)
values = append(values, path)
default:
values = append(values, v)
@@ -1318,27 +1403,20 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
release.generatedValues = append(release.generatedValues, generatedFiles...)
for _, value := range release.Secrets {
path := st.normalizePath(release.ValuesPathPrefix + value)
ok, err := st.fileExists(path)
paths, skip, err := st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value)
if err != nil {
return nil, err
}
if !ok {
if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" {
return nil, err
} else if *release.MissingFileHandler == "Warn" {
st.logger.Warnf("skipping missing secrets file \"%s\"", path)
continue
} else if *release.MissingFileHandler == "Info" {
st.logger.Infof("skipping missing secrets file \"%s\"", path)
continue
} else {
st.logger.Debugf("skipping missing secrets file \"%s\"", path)
continue
}
if skip {
continue
}
decryptFlags := st.appendTillerFlags([]string{}, release)
if len(paths) > 1 {
return nil, fmt.Errorf("glob patterns in release secret file is not supported yet. please submit a feature request if necessary")
}
path := paths[0]
decryptFlags := st.appendConnectionFlags([]string{}, release)
valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
if err != nil {
return nil, err
@@ -1352,7 +1430,7 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
if set.Value != "" {
flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(set.Value)))
} else if set.File != "" {
flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.normalizePath(set.File)))
flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizePath(set.File)))
} else if len(set.Values) > 0 {
items := make([]string, len(set.Values))
for i, raw := range set.Values {
@@ -1447,6 +1525,8 @@ func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) erro
Path string `yaml:"path"`
Selectors []string `yaml:"selectors"`
SelectorsInherited bool `yaml:"selectorsInherited"`
Environment SubhelmfileEnvironmentSpec `yaml:",inline"`
}
if err := unmarshal(&subHelmfileSpecTmp); err != nil {
return err
@@ -1454,6 +1534,7 @@ func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) erro
hf.Path = subHelmfileSpecTmp.Path
hf.Selectors = subHelmfileSpecTmp.Selectors
hf.SelectorsInherited = subHelmfileSpecTmp.SelectorsInherited
hf.Environment = subHelmfileSpecTmp.Environment
}
//since we cannot make sur the "console" string can be red after the "path" we must check we don't have
//a SubHelmfileSpec with only selector and no path
+67
View File
@@ -0,0 +1,67 @@
package state
import (
"fmt"
"github.com/imdario/mergo"
"github.com/roboll/helmfile/pkg/maputil"
"github.com/roboll/helmfile/pkg/tmpl"
)
func (st *HelmState) Values() (map[string]interface{}, error) {
vals := map[string]interface{}{}
if err := mergo.Merge(&vals, st.Env.Defaults, mergo.WithOverride); err != nil {
return nil, err
}
if err := mergo.Merge(&vals, st.Env.Values, mergo.WithOverride); err != nil {
return nil, err
}
vals, err := maputil.CastKeysToStrings(vals)
if err != nil {
return nil, err
}
return vals, nil
}
func (st *HelmState) mustLoadVals() map[string]interface{} {
vals, err := st.Values()
if err != nil {
panic(err)
}
return vals
}
func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
Environment: st.Env,
Namespace: st.Namespace,
Values: st.mustLoadVals(),
}
}
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
r := *st
vals, err := st.Values()
if err != nil {
return nil, err
}
for i, rt := range st.Releases {
tmplData := releaseTemplateData{
Environment: st.Env,
Release: rt,
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)
}
st.Releases[i] = *r
}
return &r, nil
}
@@ -1,10 +1,9 @@
package state
import (
"github.com/roboll/helmfile/pkg/environment"
"reflect"
"testing"
"github.com/roboll/helmfile/environment"
)
func TestHelmState_executeTemplates(t *testing.T) {
@@ -17,7 +16,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
name: "Has template expressions in chart, values, and secrets",
input: ReleaseSpec{
Chart: "test-charts/{{ .Release.Name }}",
Version: "0.1",
Version: "{{ .Release.Name }}-0.1",
Verify: nil,
Name: "test-app",
Namespace: "test-namespace-{{ .Release.Name }}",
@@ -26,7 +25,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
},
want: ReleaseSpec{
Chart: "test-charts/test-app",
Version: "0.1",
Version: "test-app-0.1",
Verify: nil,
Name: "test-app",
Namespace: "test-namespace-test-app",
@@ -36,7 +35,8 @@ func TestHelmState_executeTemplates(t *testing.T) {
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
@@ -71,6 +71,9 @@ func TestHelmState_executeTemplates(t *testing.T) {
if !reflect.DeepEqual(actual.Secrets, tt.want.Secrets) {
t.Errorf("expected %+v, got %+v", tt.want.Secrets, actual.Secrets)
}
if !reflect.DeepEqual(actual.Version, tt.want.Version) {
t.Errorf("expected %+v, got %+v", tt.want.Version, actual.Version)
}
})
}
}
+11 -1
View File
@@ -2,7 +2,7 @@ package state
import (
"fmt"
"github.com/roboll/helmfile/helmexec"
"github.com/roboll/helmfile/pkg/helmexec"
"sync"
)
@@ -19,6 +19,16 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun
concurrency = numReleases
}
for _, r := range st.Releases {
if r.Tillerless != nil {
if *r.Tillerless {
concurrency = 1
}
} else if st.HelmDefaults.Tillerless {
concurrency = 1
}
}
// WaitGroup is required to wait until goroutine per job in job queue cleanly stops.
var waitGroup sync.WaitGroup
waitGroup.Add(concurrency)
+218 -48
View File
@@ -7,15 +7,24 @@ import (
"reflect"
"testing"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/testhelper"
"errors"
"strings"
"fmt"
"github.com/roboll/helmfile/helmexec"
)
var logger = helmexec.NewLogger(os.Stdout, "warn")
func injectFs(st *HelmState, fs *testhelper.TestFs) *HelmState {
st.glob = fs.Glob
st.readFile = fs.ReadFile
st.fileExists = fs.FileExists
return st
}
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
@@ -115,7 +124,8 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
want: specWithNamespaceFromFields,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: tt.fields.BaseChartPath,
@@ -505,7 +515,8 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: "./",
@@ -584,7 +595,8 @@ func Test_isLocalChart(t *testing.T) {
want: true,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
if got := isLocalChart(tt.args.chart); got != tt.want {
t.Errorf("%s(\"%s\") isLocalChart(): got %v, want %v", tt.name, tt.args.chart, got, tt.want)
@@ -636,7 +648,8 @@ func Test_normalizeChart(t *testing.T) {
want: "/app",
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
if got := normalizeChart(tt.args.basePath, tt.args.chart); got != tt.want {
t.Errorf("normalizeChart() = %v, want %v", got, tt.want)
@@ -814,7 +827,8 @@ func TestHelmState_SyncRepos(t *testing.T) {
want: []string{"name", "http://example.com/", "", "", "example_user", "example_password"},
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envs {
err := os.Setenv(k, v)
@@ -929,7 +943,8 @@ func TestHelmState_SyncReleases(t *testing.T) {
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
@@ -942,6 +957,118 @@ func TestHelmState_SyncReleases(t *testing.T) {
}
}
func TestHelmState_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.T) {
no := false
tests := []struct {
name string
release ReleaseSpec
listResult string
expectedError string
}{
{
name: "should install",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: ``,
expectedError: ``,
},
{
name: "should upgrade",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
{
name: "should uninstall",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Installed: &no,
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
{
name: "should fail installing due to missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []interface{}{"noexistent.values.yaml"},
},
listResult: ``,
expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist in "."`,
},
{
name: "should fail upgrading due to missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []interface{}{"noexistent.values.yaml"},
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist in "."`,
},
{
name: "should uninstall even when there is a missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []interface{}{"noexistent.values.yaml"},
Installed: &no,
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
Releases: []ReleaseSpec{tt.release},
logger: logger,
}
fs := testhelper.NewTestFs(map[string]string{})
state = injectFs(state, fs)
helm := &mockHelmExec{
lists: map[listKey]string{},
}
//simulate the helm.list call result
helm.lists[listKey{filter: "^" + tt.release.Name + "$"}] = tt.listResult
affectedReleases := AffectedReleases{}
errs := state.SyncReleases(&affectedReleases, helm, []string{}, 1)
if tt.expectedError != "" {
if len(errs) == 0 {
t.Fatalf("expected error not occurred: expected=%s, got none", tt.expectedError)
}
if len(errs) != 1 {
t.Fatalf("too many errors: expected %d, got %d: %v", 1, len(errs), errs)
}
err := errs[0]
if err.Error() != tt.expectedError {
t.Fatalf("unexpected error: expected=%s, got=%v", tt.expectedError, err)
}
} else {
if len(errs) > 0 {
t.Fatalf("unexpected error(s): expected=0, got=%d: %v", len(errs), errs)
}
}
})
}
}
func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
no := false
tests := []struct {
@@ -1016,7 +1143,8 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
wantAffected: mockAffected{[]*mockRelease{{"releaseNameFoo", []string{}}}, []*mockRelease{{"releaseNameBar", []string{}}}, []*mockRelease{{"releaseNameFoo-error", []string{}}}},
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
@@ -1105,8 +1233,19 @@ func TestGetDeployedVersion(t *testing.T) {
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-1.0.0-alpha+001 0.1.0 default`,
installedVersion: "1.0.0-alpha+001",
},
{
name: "chart version with dash and release with dash",
release: ReleaseSpec{
Name: "foo-bar",
Chart: "registry/foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo-bar-release 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-1.0.0-alpha+001 0.1.0 default`,
installedVersion: "1.0.0-alpha+001",
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: []ReleaseSpec{tt.release},
@@ -1225,7 +1364,8 @@ func TestHelmState_DiffReleases(t *testing.T) {
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
@@ -1294,28 +1434,22 @@ func TestHelmState_SyncReleasesCleanup(t *testing.T) {
expectedNumRemovedFiles: 2,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
logger: logger,
readFile: func(f string) ([]byte, error) {
if f != "someFile" {
return nil, fmt.Errorf("unexpected file to read: %s", f)
}
someFileContent := []byte(`foo: bar
`)
return someFileContent, nil
},
removeFile: func(f string) error {
numRemovedFiles += 1
return nil
},
fileExists: func(f string) (bool, error) {
return true, nil
},
}
testfs := testhelper.NewTestFs(map[string]string{
"/path/to/someFile": `foo: FOO`,
})
state = injectFs(state, testfs)
if errs := state.SyncReleases(&AffectedReleases{}, tt.helm, []string{}, 1); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
@@ -1383,28 +1517,23 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
expectedNumRemovedFiles: 2,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
Releases: tt.releases,
logger: logger,
readFile: func(f string) ([]byte, error) {
if f != "someFile" {
return nil, fmt.Errorf("unexpected file to read: %s", f)
}
someFileContent := []byte(`foo: bar
`)
return someFileContent, nil
},
removeFile: func(f string) error {
numRemovedFiles += 1
return nil
},
fileExists: func(f string) (bool, error) {
return true, nil
},
}
testfs := testhelper.NewTestFs(map[string]string{
"/path/to/someFile": `foo: bar
`,
})
state = injectFs(state, testfs)
if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false); errs != nil && len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
@@ -1624,8 +1753,9 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
want: []mockRelease{{"releaseA", []string{"--tiller-namespace", "tillerns"}}},
},
}
for _, tt := range tests {
i := func(t *testing.T) {
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
@@ -1651,7 +1781,7 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
t.Errorf("HelmState.ReleaseStatuses() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
}
}
t.Run(tt.name, i)
t.Run(tt.name, f)
}
}
@@ -1708,8 +1838,9 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
want: []mockRelease{{"releaseA", []string{"--timeout", "1", "--tiller-namespace", "tillerns"}}},
},
}
for _, tt := range tests {
i := func(t *testing.T) {
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
logger: logger,
@@ -1723,7 +1854,7 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
t.Errorf("HelmState.TestReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
}
}
t.Run(tt.name, i)
t.Run(tt.name, f)
}
}
@@ -1758,8 +1889,9 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
wantErr: false,
},
}
for _, tt := range tests {
i := func(t *testing.T) {
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
Releases: releases,
logger: logger,
@@ -1771,7 +1903,7 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
return
}
}
t.Run(tt.name, i)
t.Run(tt.name, f)
}
}
@@ -1785,6 +1917,8 @@ func TestHelmState_Delete(t *testing.T) {
purge bool
flags string
tillerNamespace string
kubeContext string
defKubeContext string
}{
{
name: "desired and installed (purge=false)",
@@ -1876,9 +2010,41 @@ 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 _, tt := range tests {
i := func(t *testing.T) {
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
name := "releaseA"
if tt.wantErr {
name = "releaseA-error"
@@ -1887,11 +2053,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,
}
@@ -1903,16 +2073,16 @@ 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, &affectedReleases.Deleted)
}
}
t.Run(tt.name, i)
t.Run(tt.name, f)
}
}
+99
View File
@@ -0,0 +1,99 @@
package state
import (
"fmt"
"go.uber.org/zap"
"net/url"
"path/filepath"
"sort"
)
type Storage struct {
logger *zap.SugaredLogger
FilePath string
basePath string
glob func(string) ([]string, error)
}
func NewStorage(forFile string, logger *zap.SugaredLogger, glob func(string) ([]string, error)) *Storage {
return &Storage{
FilePath: forFile,
basePath: filepath.Dir(forFile),
logger: logger,
glob: glob,
}
}
func (st *Storage) resolveFile(missingFileHandler *string, tpe, path string) ([]string, bool, error) {
title := fmt.Sprintf("%s file", tpe)
files, err := st.ExpandPaths(path)
if err != nil {
return nil, false, err
}
var handlerId string
if missingFileHandler != nil {
handlerId = *missingFileHandler
} else {
handlerId = MissingFileHandlerError
}
if len(files) == 0 {
switch handlerId {
case MissingFileHandlerError:
return nil, false, fmt.Errorf("%s matching \"%s\" does not exist in \"%s\"", title, path, st.basePath)
case MissingFileHandlerWarn:
st.logger.Warnf("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
case MissingFileHandlerInfo:
st.logger.Infof("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
case MissingFileHandlerDebug:
st.logger.Debugf("skipping missing %s matching \"%s\"", title, path)
return nil, true, nil
default:
available := []string{
MissingFileHandlerError,
MissingFileHandlerWarn,
MissingFileHandlerInfo,
MissingFileHandlerDebug,
}
return nil, false, fmt.Errorf("invalid missing file handler \"%s\" while processing \"%s\" in \"%s\": it must be one of %s", handlerId, path, st.FilePath, available)
}
}
return files, false, nil
}
func (st *Storage) ExpandPaths(globPattern string) ([]string, error) {
result := []string{}
absPathPattern := st.normalizePath(globPattern)
matches, err := st.glob(absPathPattern)
if err != nil {
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
}
sort.Strings(matches)
result = append(result, matches...)
return result, nil
}
// normalizes relative path to absolute one
func (st *Storage) normalizePath(path string) string {
u, _ := url.Parse(path)
if u.Scheme != "" || filepath.IsAbs(path) {
return path
} else {
return st.JoinBase(path)
}
}
// JoinBase returns an absolute path in the form basePath/relative
func (st *Storage) JoinBase(relPath string) string {
return filepath.Join(st.basePath, relPath)
}
+9 -3
View File
@@ -1,6 +1,8 @@
package state
import "github.com/roboll/helmfile/environment"
import (
"github.com/roboll/helmfile/pkg/environment"
)
// TemplateSpec defines the structure of a reusable and composable template for helm releases.
type TemplateSpec struct {
@@ -13,12 +15,16 @@ type EnvironmentTemplateData struct {
Environment environment.Environment
// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
Namespace string
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
Values map[string]interface{}
}
// ReleaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
type ReleaseTemplateData struct {
// releaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
type releaseTemplateData struct {
// Environment is accessible as `.Environment` from any template expression executed by the renderer
Environment environment.Environment
// Release is accessible as `.Release` from any template expression executed by the renderer
Release ReleaseSpec
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
Values map[string]interface{}
}
+40
View File
@@ -0,0 +1,40 @@
package state
import (
"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
}
+113
View File
@@ -0,0 +1,113 @@
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,
},
{
// 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,
},
{
// 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)
}
}
}
+10 -3
View File
@@ -1,7 +1,8 @@
package state
package testhelper
import (
"fmt"
"os"
"path/filepath"
"strings"
)
@@ -20,8 +21,10 @@ type TestFs struct {
func NewTestFs(files map[string]string) *TestFs {
dirs := map[string]bool{}
for abs, _ := range files {
d := filepath.Dir(abs)
dirs[d] = true
for d := filepath.Dir(abs); !dirs[d]; d = filepath.Dir(d) {
dirs[d] = true
fmt.Fprintf(os.Stderr, "testfs: recognized dir: %s\n", d)
}
}
return &TestFs{
Cwd: "/path/to",
@@ -44,6 +47,10 @@ func (f *TestFs) FileExistsAt(path string) bool {
return ok
}
func (f *TestFs) FileExists(path string) (bool, error) {
return f.FileExistsAt(path), nil
}
func (f *TestFs) DirectoryExistsAt(path string) bool {
var ok bool
if strings.Contains(path, "/") {
@@ -13,9 +13,9 @@ func (c *Context) stringTemplate() *template.Template {
}
tmpl := template.New("stringTemplate").Funcs(funcMap)
if c.preRender {
tmpl.Option("missingkey=zero")
tmpl = tmpl.Option("missingkey=zero")
} else {
tmpl.Option("missingkey=error")
tmpl = tmpl.Option("missingkey=error")
}
return tmpl
}
@@ -230,7 +230,8 @@ func Test_renderTemplateToString(t *testing.T) {
wantErr: true,
},
}
for _, tt := range tests {
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.args.envs {
err := os.Setenv(k, v)
@@ -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, ".")
@@ -2,7 +2,7 @@ package tmpl
import (
"fmt"
"github.com/roboll/helmfile/environment"
"github.com/roboll/helmfile/pkg/environment"
"reflect"
"testing"
)
-6
View File
@@ -1,6 +0,0 @@
package state
type EnvironmentSpec struct {
Values []string `yaml:"values"`
Secrets []string `yaml:"secrets"`
}
-32
View File
@@ -1,32 +0,0 @@
package state
import (
"fmt"
"github.com/roboll/helmfile/tmpl"
)
func (st *HelmState) envTemplateData() EnvironmentTemplateData {
return EnvironmentTemplateData{
st.Env,
st.Namespace,
}
}
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
r := *st
for i, rt := range st.Releases {
tmplData := ReleaseTemplateData{
Environment: st.Env,
Release: rt,
}
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)
}
st.Releases[i] = *r
}
return &r, nil
}
+3
View File
@@ -93,6 +93,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"