mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 16:37:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82f77ee50 | ||
|
|
e6bb8282db | ||
|
|
b6a04d012c | ||
|
|
9eef44ffe6 | ||
|
|
cfe309ec21 | ||
|
|
65818cd345 | ||
|
|
ddb5be1b9d | ||
|
|
f61334d9bb | ||
|
|
3bf0f7d54e | ||
|
|
7dec948950 | ||
|
|
e0d0a1cf7f | ||
|
|
31768b5bac | ||
|
|
1bde201eae | ||
|
|
1831d3375f | ||
|
|
2e38f42a40 | ||
|
|
1da3488599 | ||
|
|
78b03e0d92 | ||
|
|
11eda66eaa | ||
|
|
65d404b276 | ||
|
|
72425aebfc | ||
|
|
34fbfb4fd0 | ||
|
|
56c27c2bd9 | ||
|
|
34c793d87e | ||
|
|
b9862bdae6 | ||
|
|
e3e7905f07 | ||
|
|
2e57beb448 | ||
|
|
3986f6497e | ||
|
|
820abbc06d | ||
|
|
3710f6233e | ||
|
|
2d2b3e486f | ||
|
|
65ee6a2124 | ||
|
|
1d3f5f8a33 | ||
|
|
e2d6dc4afa | ||
|
|
f6057a1cca | ||
|
|
c68fc5bc50 | ||
|
|
68b95f14d4 | ||
|
|
9318d8a040 | ||
|
|
44686298f2 | ||
|
|
206372b7aa | ||
|
|
e1c04971c1 | ||
|
|
b3f4586ed8 | ||
|
|
591086dac9 | ||
|
|
1226ea6d1a | ||
|
|
681c866ce1 | ||
|
|
dd70c857a5 | ||
|
|
a896f801ab | ||
|
|
4a5996d083 | ||
|
|
f6264bfa9d | ||
|
|
3b3d3092dd |
+3
-1
@@ -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
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
# Helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
|
||||
Deploy Kubernetes Helm Charts
|
||||
|
||||
[](https://quay.io/repository/roboll/helmfile)
|
||||
[](https://slack.sweetops.com)
|
||||
|
||||
## status
|
||||
## Status
|
||||
|
||||
Even though Helmfile is used in production environments [across multiple organizations](USERS.md), it is still in its early stage of development, hence versioned 0.x.
|
||||
|
||||
Helmfile complies to Semantic Versioning 2.0.0 in which v0.x means that there could be backward-incompatible changes for every release.
|
||||
|
||||
Note that we will try our best to document any backward incompatibility.
|
||||
Note that we will try our best to document any backward incompatibility. And in reality, helmfile had no breaking change for a year or so.
|
||||
|
||||
## about
|
||||
## About
|
||||
|
||||
Helmfile is a declarative spec for deploying helm charts. It lets you...
|
||||
|
||||
@@ -23,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}}`}}\
|
||||
"]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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?
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package environment
|
||||
|
||||
type Environment struct {
|
||||
Name string
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
var EmptyEnvironment Environment
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, " "))
|
||||
@@ -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 ©, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
copy := e.DeepCopy()
|
||||
if other != nil {
|
||||
if err := mergo.Merge(©, other, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ©, nil
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type EnvironmentValuesLoader struct {
|
||||
storage *Storage
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger) *EnvironmentValuesLoader {
|
||||
return &EnvironmentValuesLoader{
|
||||
storage: storage,
|
||||
readFile: readFile,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []interface{}) (map[string]interface{}, error) {
|
||||
result := map[string]interface{}{}
|
||||
|
||||
for _, entry := range valuesEntries {
|
||||
maps := []interface{}{}
|
||||
|
||||
switch strOrMap := entry.(type) {
|
||||
case string:
|
||||
urlOrPath := strOrMap
|
||||
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
|
||||
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
|
||||
bytes, err := r.RenderToBytes(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
|
||||
}
|
||||
maps = append(maps, m)
|
||||
if ld.logger != nil {
|
||||
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
|
||||
}
|
||||
}
|
||||
case map[interface{}]interface{}:
|
||||
maps = append(maps, strOrMap)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
|
||||
}
|
||||
for _, m := range maps {
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
|
||||
// See https://github.com/roboll/helmfile/issues/677
|
||||
vals, err := maputil.CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&result, &vals, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package state
|
||||
|
||||
type EnvironmentSpec struct {
|
||||
Values []string `yaml:"values"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user