mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 15:23:20 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b48a8cca | ||
|
|
0e00cdf2a5 | ||
|
|
f522ba320b | ||
|
|
8f1a15c9cd | ||
|
|
fa95e0dd92 | ||
|
|
eb36e93c5a | ||
|
|
3edc7d6f33 | ||
|
|
57df25c3ff | ||
|
|
14a392666e | ||
|
|
283dac1531 | ||
|
|
f5e565ea3e | ||
|
|
5f8b2e5c7f |
@@ -3,6 +3,7 @@
|
||||
Deploy Kubernetes Helm Charts
|
||||
|
||||
[](https://quay.io/repository/roboll/helmfile)
|
||||
[](https://slack.sweetops.com)
|
||||
|
||||
## status
|
||||
|
||||
@@ -52,6 +53,14 @@ helmDefaults:
|
||||
timeout: 600
|
||||
recreatePods: true
|
||||
force: true
|
||||
# enable TLS for request to Tiller
|
||||
tls: true
|
||||
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
|
||||
tlsCACert: "path/to/ca.pem"
|
||||
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
|
||||
tlsCert: "path/to/cert.pem"
|
||||
# path to TLS key file (default "$HELM_HOME/key.pem")
|
||||
tlsKey: "path/to/key.pem"
|
||||
|
||||
releases:
|
||||
# Published chart example
|
||||
@@ -101,6 +110,14 @@ releases:
|
||||
installed: true
|
||||
# restores previous state in case of failed release
|
||||
atomic: true
|
||||
# enable TLS for request to Tiller
|
||||
tls: true
|
||||
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
|
||||
tlsCACert: "path/to/ca.pem"
|
||||
# path to TLS certificate file (default "$HELM_HOME/cert.pem")
|
||||
tlsCert: "path/to/cert.pem"
|
||||
# path to TLS key file (default "$HELM_HOME/key.pem")
|
||||
tlsKey: "path/to/key.pem"
|
||||
|
||||
# Local chart example
|
||||
- name: grafana # name of this release
|
||||
@@ -196,16 +213,20 @@ NAME:
|
||||
USAGE:
|
||||
helmfile [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
v0.52.0
|
||||
|
||||
COMMANDS:
|
||||
repos sync repositories from state file (helm repo add && helm repo update)
|
||||
charts sync releases from state file (helm upgrade --install)
|
||||
charts DEPRECATED: sync releases from state file (helm upgrade --install)
|
||||
diff diff releases from state file against env (helm diff)
|
||||
template template releases from state file against env (helm template)
|
||||
lint lint charts from state file (helm lint)
|
||||
sync sync all resources from state file (repos, releases and chart deps)
|
||||
apply apply all resources from state file only when there are changes
|
||||
status retrieve status of releases in state file
|
||||
delete delete releases from state file (helm delete)
|
||||
delete DEPRECATED: delete releases from state file (helm delete)
|
||||
destroy deletes and then purges releases
|
||||
test test releases from state file (helm test)
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
@@ -249,7 +270,15 @@ The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds tha
|
||||
|
||||
An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters.
|
||||
|
||||
### delete
|
||||
### destroy
|
||||
|
||||
The `helmfile destroy` sub-command deletes and purges all the releases defined in the manifests.
|
||||
|
||||
`helmfile --interactive destroy` instructs Helmfile to request your confirmation before actually deleting releases.
|
||||
|
||||
`destroy` basically runs `helm delete --purge` on all the targeted releases. If you don't want purging, use `helmfile delete` instead.
|
||||
|
||||
### delete (DEPRECATED)
|
||||
|
||||
The `helmfile delete` sub-command deletes all the releases defined in the manifests.
|
||||
|
||||
@@ -730,6 +759,12 @@ Use the [Helmfile Best Practices Guide](/docs/writing-helmfile.md) to write adva
|
||||
- Default values
|
||||
- Layering
|
||||
|
||||
We also have dedicated documentation on the following topics which might interest you:
|
||||
|
||||
- [Shared Configurations Across Teams](/docs/shared-configurations-across-teams.md)
|
||||
|
||||
Or join our friendly slack community in the [`#helmfile`](https://slack.sweetops.com) channel to ask questions and get help. Check out our [slack archive](https://archive.sweetops.com/helmfile/) for good examples of how others are using it.
|
||||
|
||||
## Using env files
|
||||
|
||||
Helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
|
||||
@@ -742,7 +777,7 @@ Please see #203 for more context.
|
||||
|
||||
## Running helmfile interactively
|
||||
|
||||
`helmfile --interactive [apply|delete]` requests confirmation from you before actually modifying your cluster.
|
||||
`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster.
|
||||
|
||||
Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts.
|
||||
|
||||
|
||||
@@ -83,9 +83,6 @@ func GetArgs(args string, state *state.HelmState) []string {
|
||||
}
|
||||
}
|
||||
|
||||
if state.HelmDefaults.TillerNamespace != "" {
|
||||
argsMap.SetArg("--tiller-namespace", state.HelmDefaults.TillerNamespace, false)
|
||||
}
|
||||
if state.HelmDefaults.KubeContext != "" {
|
||||
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Shared Configuration Across Teams
|
||||
|
||||
Assume you have a two or more teams, each works for a different internal or external service, like:
|
||||
|
||||
- Product 1
|
||||
- Product 2
|
||||
- Observability
|
||||
|
||||
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
- name: product2-api
|
||||
chart: saas-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
This works, but what if you wanted to a separate cluster per service to achieve smaller blast radius?
|
||||
|
||||
Let's start by creating a `helmfile.yaml` for each service.
|
||||
|
||||
`product1/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
`product2/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product2-api
|
||||
chart: product2-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
You will (of course!) notice this isn't DRY.
|
||||
|
||||
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
|
||||
|
||||
`observability/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
|
||||
|
||||
`product1/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
`product2/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
|
||||
releases:
|
||||
- name: product2-api
|
||||
chart: product2-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
## Using sub-helmfile as a template
|
||||
|
||||
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
|
||||
|
||||
`shared/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product{{ env "PRODUCT_ID" }}-api
|
||||
chart: product{{ env "PRODUCT_ID" }}-charts/api
|
||||
# snip
|
||||
- name: product{{ env "PRODUCT_ID" }}-web
|
||||
chart: product{{ env "PRODUCT_ID" }}-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
Then you only need one single product helmfile
|
||||
|
||||
|
||||
`product/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
- ../shared/helmfile.yaml
|
||||
```
|
||||
|
||||
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
|
||||
|
||||
```console
|
||||
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
|
||||
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
|
||||
```
|
||||
+4
-4
@@ -103,16 +103,16 @@ func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) ReleaseStatus(name string) error {
|
||||
func (helm *execer) ReleaseStatus(name string, flags ...string) error {
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
out, err := helm.exec(append([]string{"status", name})...)
|
||||
out, err := helm.exec(append([]string{"status", name}, flags...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) List(filter string) (string, error) {
|
||||
func (helm *execer) List(filter string, flags ...string) (string, error) {
|
||||
helm.logger.Infof("Listing releases matching %v", filter)
|
||||
out, err := helm.exec(append([]string{"list", filter})...)
|
||||
out, err := helm.exec(append([]string{"list", filter}, flags...)...)
|
||||
helm.write(out)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ type Interface interface {
|
||||
TemplateRelease(chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
Lint(chart string, flags ...string) error
|
||||
ReleaseStatus(name string) error
|
||||
ReleaseStatus(name string, flags ...string) error
|
||||
DeleteRelease(name string, flags ...string) error
|
||||
TestRelease(name string, flags ...string) error
|
||||
List(filter string) (string, error)
|
||||
List(filter string, flags ...string) (string, error)
|
||||
DecryptSecret(name string) (string, error)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func main() {
|
||||
},
|
||||
{
|
||||
Name: "charts",
|
||||
Usage: "sync releases from state file (helm upgrade --install)",
|
||||
Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
@@ -435,7 +435,7 @@ Do you really want to apply?
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "delete releases from state file (helm delete)",
|
||||
Usage: "DEPRECATED: delete releases from state file (helm delete)",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
@@ -476,6 +476,43 @@ Do you really want to delete?
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "destroy",
|
||||
Usage: "deletes and then purges releases",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return 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(helm, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test",
|
||||
Usage: "test releases from state file (helm test)",
|
||||
|
||||
+21
-16
@@ -110,6 +110,7 @@ func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error {
|
||||
|
||||
func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error {
|
||||
noMatchInHelmfiles := true
|
||||
|
||||
err := a.visitStateFiles(fileOrDir, func(f string) error {
|
||||
content, err := a.readFile(f)
|
||||
if err != nil {
|
||||
@@ -153,8 +154,6 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
|
||||
}
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
|
||||
if len(st.Helmfiles) > 0 {
|
||||
noMatchInSubHelmfiles := true
|
||||
for _, m := range st.Helmfiles {
|
||||
@@ -170,26 +169,25 @@ func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmStat
|
||||
}
|
||||
}
|
||||
noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles
|
||||
} else {
|
||||
var err error
|
||||
st, err = st.ExecuteTemplates()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, err)
|
||||
}
|
||||
|
||||
var processed bool
|
||||
processed, errs = converge(st, helm)
|
||||
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
||||
}
|
||||
|
||||
return clean(st, errs)
|
||||
templated, tmplErr := st.ExecuteTemplates()
|
||||
if tmplErr != nil {
|
||||
return fmt.Errorf("failed executing release templates in \"%s\": %v", f, tmplErr)
|
||||
}
|
||||
processed, errs := converge(templated, helm)
|
||||
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
||||
return clean(templated, errs)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if noMatchInHelmfiles {
|
||||
return &NoMatchingHelmfileError{selectors: a.Selectors, env: a.Env}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -204,13 +202,20 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
|
||||
}
|
||||
}
|
||||
|
||||
releaseNameCounts := map[string]int{}
|
||||
type Key struct {
|
||||
TillerNamespace, Name string
|
||||
}
|
||||
releaseNameCounts := map[Key]int{}
|
||||
for _, r := range st.Releases {
|
||||
releaseNameCounts[r.Name]++
|
||||
tillerNamespace := st.HelmDefaults.TillerNamespace
|
||||
if r.TillerNamespace != "" {
|
||||
tillerNamespace = r.TillerNamespace
|
||||
}
|
||||
releaseNameCounts[Key{tillerNamespace, r.Name}]++
|
||||
}
|
||||
for name, c := range releaseNameCounts {
|
||||
if c > 1 {
|
||||
return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)}
|
||||
return false, []error{fmt.Errorf("duplicate release \"%s\" found in \"%s\": there were %d releases named \"%s\" matching specified selector", name.Name, name.TillerNamespace, c, name.Name)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -247,6 +247,8 @@ releases:
|
||||
chart: stable/prometheus
|
||||
`,
|
||||
"/path/to/helmfile.d/b.yaml": `
|
||||
helmDefaults:
|
||||
tillerNamespace: zoo
|
||||
releases:
|
||||
- name: grafana
|
||||
chart: stable/grafana
|
||||
@@ -258,6 +260,16 @@ releases:
|
||||
chart: charts/foo
|
||||
labels:
|
||||
duplicated: yes
|
||||
- name: bar
|
||||
chart: charts/foo
|
||||
tillerNamespace: bar1
|
||||
labels:
|
||||
duplicatedOK: yes
|
||||
- name: bar
|
||||
chart: charts/foo
|
||||
tillerNamespace: bar2
|
||||
labels:
|
||||
duplicatedOK: yes
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -272,7 +284,8 @@ releases:
|
||||
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
|
||||
{label: "name", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
|
||||
// See https://github.com/roboll/helmfile/issues/193
|
||||
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found: there were 2 releases named \"foo\" matching specified selector"},
|
||||
{label: "duplicated=yes", expectedCount: 0, expectErr: true, errMsg: "failed processing /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
|
||||
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
|
||||
@@ -114,6 +114,18 @@ func (c *creator) CreateFromYaml(content []byte, file string, env string) (*Helm
|
||||
state.Env = *e
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
@@ -67,3 +67,7 @@ func (r ReleaseSpec) Clone() (*ReleaseSpec, error) {
|
||||
|
||||
return &deserialized, nil
|
||||
}
|
||||
|
||||
func (r ReleaseSpec) Desired() bool {
|
||||
return r.Installed == nil || *r.Installed
|
||||
}
|
||||
|
||||
+169
-34
@@ -47,6 +47,9 @@ type HelmState struct {
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
|
||||
removeFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
|
||||
runner helmexec.Runner
|
||||
}
|
||||
|
||||
@@ -68,6 +71,11 @@ type HelmSpec struct {
|
||||
Force bool `yaml:"force"`
|
||||
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
|
||||
Atomic bool `yaml:"atomic"`
|
||||
|
||||
TLS bool `yaml:"tls"`
|
||||
TLSCACert string `yaml:"tlsCACert"`
|
||||
TLSKey string `yaml:"tlsKey"`
|
||||
TLSCert string `yaml:"tlsCert"`
|
||||
}
|
||||
|
||||
// RepositorySpec that defines values for a helm repo
|
||||
@@ -121,6 +129,13 @@ type ReleaseSpec struct {
|
||||
|
||||
ValuesPathPrefix string `yaml:"valuesPathPrefix"`
|
||||
|
||||
TillerNamespace string `yaml:"tillerNamespace"`
|
||||
|
||||
TLS *bool `yaml:"tls"`
|
||||
TLSCACert string `yaml:"tlsCACert"`
|
||||
TLSKey string `yaml:"tlsKey"`
|
||||
TLSCert string `yaml:"tlsCert"`
|
||||
|
||||
// generatedValues are values that need cleaned up on exit
|
||||
generatedValues []string
|
||||
}
|
||||
@@ -187,7 +202,14 @@ type syncPrepareResult struct {
|
||||
|
||||
// SyncReleases wrapper for executing helm upgrade on the releases
|
||||
func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int) ([]syncPrepareResult, []error) {
|
||||
releases := st.Releases
|
||||
releases := []*ReleaseSpec{}
|
||||
for i, _ := range st.Releases {
|
||||
if !st.Releases[i].Desired() {
|
||||
continue
|
||||
}
|
||||
releases = append(releases, &st.Releases[i])
|
||||
}
|
||||
|
||||
numReleases := len(releases)
|
||||
jobs := make(chan *ReleaseSpec, numReleases)
|
||||
results := make(chan syncPrepareResult, numReleases)
|
||||
@@ -200,7 +222,7 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
numReleases,
|
||||
func() {
|
||||
for i := 0; i < numReleases; i++ {
|
||||
jobs <- &releases[i]
|
||||
jobs <- releases[i]
|
||||
}
|
||||
close(jobs)
|
||||
},
|
||||
@@ -221,8 +243,11 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
errs = append(errs, &ReleaseError{release, err})
|
||||
}
|
||||
|
||||
if _, err := os.Stat(valfile); os.IsNotExist(err) {
|
||||
ok, err := st.fileExists(valfile)
|
||||
if err != nil {
|
||||
errs = append(errs, &ReleaseError{release, err})
|
||||
} else if !ok {
|
||||
errs = append(errs, &ReleaseError{release, fmt.Errorf("file does not exist: %s", valfile)})
|
||||
}
|
||||
flags = append(flags, "--values", valfile)
|
||||
}
|
||||
@@ -252,8 +277,8 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
|
||||
return res, errs
|
||||
}
|
||||
|
||||
func isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, error) {
|
||||
out, err := helm.List("^" + release.Name + "$")
|
||||
func (st *HelmState) isReleaseInstalled(helm helmexec.Interface, release ReleaseSpec) (bool, error) {
|
||||
out, err := helm.List("^"+release.Name+"$", st.tillerFlags(&release)...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if out != "" {
|
||||
@@ -266,8 +291,8 @@ func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface) ([]*Rele
|
||||
detected := []*ReleaseSpec{}
|
||||
for i, _ := range st.Releases {
|
||||
release := st.Releases[i]
|
||||
if release.Installed != nil && !*release.Installed {
|
||||
installed, err := isReleaseInstalled(helm, release)
|
||||
if !release.Desired() {
|
||||
installed, err := st.isReleaseInstalled(helm, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if installed {
|
||||
@@ -304,8 +329,8 @@ func (st *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []st
|
||||
flags := prep.flags
|
||||
chart := normalizeChart(st.basePath, release.Chart)
|
||||
var relErr *ReleaseError
|
||||
if release.Installed != nil && !*release.Installed {
|
||||
installed, err := isReleaseInstalled(helm, *release)
|
||||
if !release.Desired() {
|
||||
installed, err := st.isReleaseInstalled(helm, *release)
|
||||
if err != nil {
|
||||
relErr = &ReleaseError{release, err}
|
||||
} else if installed {
|
||||
@@ -439,6 +464,10 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues
|
||||
}
|
||||
|
||||
for _, release := range st.Releases {
|
||||
if !release.Desired() {
|
||||
continue
|
||||
}
|
||||
|
||||
flags, err := st.flagsForTemplate(helm, &release)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -495,6 +524,10 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
|
||||
}
|
||||
|
||||
for _, release := range st.Releases {
|
||||
if !release.Desired() {
|
||||
continue
|
||||
}
|
||||
|
||||
flags, err := st.flagsForLint(helm, &release)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -550,12 +583,14 @@ type diffPrepareResult struct {
|
||||
}
|
||||
|
||||
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) {
|
||||
releases := []ReleaseSpec{}
|
||||
for _, r := range st.Releases {
|
||||
if r.Installed == nil || *r.Installed {
|
||||
releases = append(releases, r)
|
||||
releases := []*ReleaseSpec{}
|
||||
for i, _ := range st.Releases {
|
||||
if !st.Releases[i].Desired() {
|
||||
continue
|
||||
}
|
||||
releases = append(releases, &st.Releases[i])
|
||||
}
|
||||
|
||||
numReleases := len(releases)
|
||||
jobs := make(chan *ReleaseSpec, numReleases)
|
||||
results := make(chan diffPrepareResult, numReleases)
|
||||
@@ -568,7 +603,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
|
||||
numReleases,
|
||||
func() {
|
||||
for i := 0; i < numReleases; i++ {
|
||||
jobs <- &releases[i]
|
||||
jobs <- releases[i]
|
||||
}
|
||||
close(jobs)
|
||||
},
|
||||
@@ -697,18 +732,31 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
|
||||
|
||||
func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
|
||||
return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec) error {
|
||||
return helm.ReleaseStatus(release.Name)
|
||||
if !release.Desired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
|
||||
return helm.ReleaseStatus(release.Name, flags...)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReleases wrapper for executing helm delete on the releases
|
||||
func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error {
|
||||
return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error {
|
||||
if !release.Desired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
if purge {
|
||||
flags = append(flags, "--purge")
|
||||
}
|
||||
installed, err := isReleaseInstalled(helm, release)
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
|
||||
installed, err := st.isReleaseInstalled(helm, release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -722,11 +770,17 @@ func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error
|
||||
// TestReleases wrapper for executing helm test on the releases
|
||||
func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int, concurrency int) []error {
|
||||
return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec) error {
|
||||
if !release.Desired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
if cleanup {
|
||||
flags = append(flags, "--cleanup")
|
||||
}
|
||||
flags = append(flags, "--timeout", strconv.Itoa(timeout))
|
||||
flags = st.appendTillerFlags(flags, &release)
|
||||
|
||||
return helm.TestRelease(release.Name, flags...)
|
||||
})
|
||||
}
|
||||
@@ -737,7 +791,7 @@ func (st *HelmState) Clean() []error {
|
||||
|
||||
for _, release := range st.Releases {
|
||||
for _, value := range release.generatedValues {
|
||||
err := os.Remove(value)
|
||||
err := st.removeFile(value)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -929,6 +983,47 @@ 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)
|
||||
for _, a := range adds {
|
||||
flags = append(flags, a)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (st *HelmState) tillerFlags(release *ReleaseSpec) []string {
|
||||
flags := []string{}
|
||||
if release.TillerNamespace != "" {
|
||||
flags = append(flags, "--tiller-namespace", release.TillerNamespace)
|
||||
} else if st.HelmDefaults.TillerNamespace != "" {
|
||||
flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
|
||||
}
|
||||
|
||||
if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
|
||||
flags = append(flags, "--tls")
|
||||
}
|
||||
|
||||
if release.TLSKey != "" {
|
||||
flags = append(flags, "--tls-key", release.TLSKey)
|
||||
} else if st.HelmDefaults.TLSKey != "" {
|
||||
flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey)
|
||||
}
|
||||
|
||||
if release.TLSCert != "" {
|
||||
flags = append(flags, "--tls-cert", release.TLSCert)
|
||||
} else if st.HelmDefaults.TLSCert != "" {
|
||||
flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert)
|
||||
}
|
||||
|
||||
if release.TLSCACert != "" {
|
||||
flags = append(flags, "--tls-ca-cert", release.TLSCACert)
|
||||
} else if st.HelmDefaults.TLSCACert != "" {
|
||||
flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
|
||||
flags := []string{}
|
||||
if release.Version != "" {
|
||||
@@ -967,6 +1062,8 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
|
||||
flags = append(flags, "--atomic")
|
||||
}
|
||||
|
||||
flags = st.appendTillerFlags(flags, release)
|
||||
|
||||
common, err := st.namespaceAndValuesFlags(helm, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -995,6 +1092,8 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec)
|
||||
flags = append(flags, "--devel")
|
||||
}
|
||||
|
||||
flags = st.appendTillerFlags(flags, release)
|
||||
|
||||
common, err := st.namespaceAndValuesFlags(helm, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1020,23 +1119,25 @@ func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
|
||||
return r.RenderToBytes(path)
|
||||
}
|
||||
|
||||
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
|
||||
flags := []string{}
|
||||
if release.Namespace != "" {
|
||||
flags = append(flags, "--namespace", release.Namespace)
|
||||
}
|
||||
for _, value := range release.Values {
|
||||
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(release.ValuesPathPrefix + typedValue)
|
||||
path := st.normalizePath(typedValue)
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" {
|
||||
return nil, err
|
||||
} else if *release.MissingFileHandler == "Warn" {
|
||||
ok, err := st.fileExists(path)
|
||||
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 *release.MissingFileHandler == "Info" {
|
||||
} else if *missingFileHandler == "Info" {
|
||||
st.logger.Infof("skipping missing values file \"%s\"", path)
|
||||
continue
|
||||
} else {
|
||||
@@ -1060,8 +1161,7 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
|
||||
return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
|
||||
}
|
||||
st.logger.Debugf("successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes))
|
||||
flags = append(flags, "--values", valfile.Name())
|
||||
|
||||
generatedFiles = append(generatedFiles, valfile.Name())
|
||||
case map[interface{}]interface{}:
|
||||
valfile, err := ioutil.TempFile("", "values")
|
||||
if err != nil {
|
||||
@@ -1073,14 +1173,49 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
|
||||
if err := encoder.Encode(typedValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
release.generatedValues = append(release.generatedValues, valfile.Name())
|
||||
flags = append(flags, "--values", valfile.Name())
|
||||
generatedFiles = append(generatedFiles, valfile.Name())
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
|
||||
}
|
||||
}
|
||||
return generatedFiles, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) {
|
||||
flags := []string{}
|
||||
if release.Namespace != "" {
|
||||
flags = append(flags, "--namespace", release.Namespace)
|
||||
}
|
||||
|
||||
values := []interface{}{}
|
||||
for _, v := range release.Values {
|
||||
switch typedValue := v.(type) {
|
||||
case string:
|
||||
path := st.normalizePath(release.ValuesPathPrefix + typedValue)
|
||||
values = append(values, path)
|
||||
default:
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
|
||||
generatedFiles, err := st.generateTemporaryValuesFiles(values, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
flags = append(flags, "--values", f)
|
||||
}
|
||||
|
||||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
|
||||
for _, value := range release.Secrets {
|
||||
path := st.normalizePath(release.ValuesPathPrefix + value)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
ok, err := st.fileExists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
if release.MissingFileHandler == nil || *release.MissingFileHandler == "Error" {
|
||||
return nil, err
|
||||
} else if *release.MissingFileHandler == "Warn" {
|
||||
|
||||
+592
-10
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
)
|
||||
|
||||
@@ -129,6 +130,10 @@ func TestHelmState_applyDefaultsTo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func boolValue(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestHelmState_flagsForUpgrade(t *testing.T) {
|
||||
enable := true
|
||||
disable := false
|
||||
@@ -424,6 +429,79 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
|
||||
"--namespace", "test-namespace",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tiller",
|
||||
defaults: HelmSpec{},
|
||||
release: &ReleaseSpec{
|
||||
Chart: "test/chart",
|
||||
Version: "0.1",
|
||||
Name: "test-charts",
|
||||
TLS: boolValue(true),
|
||||
TillerNamespace: "tiller-system",
|
||||
TLSKey: "key.pem",
|
||||
TLSCert: "cert.pem",
|
||||
TLSCACert: "ca.pem",
|
||||
},
|
||||
want: []string{
|
||||
"--version", "0.1",
|
||||
"--tiller-namespace", "tiller-system",
|
||||
"--tls",
|
||||
"--tls-key", "key.pem",
|
||||
"--tls-cert", "cert.pem",
|
||||
"--tls-ca-cert", "ca.pem",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tiller-override-defaults",
|
||||
defaults: HelmSpec{
|
||||
TLS: false,
|
||||
TillerNamespace: "a",
|
||||
TLSKey: "b.pem",
|
||||
TLSCert: "c.pem",
|
||||
TLSCACert: "d.pem",
|
||||
},
|
||||
release: &ReleaseSpec{
|
||||
Chart: "test/chart",
|
||||
Version: "0.1",
|
||||
Name: "test-charts",
|
||||
TLS: boolValue(true),
|
||||
TillerNamespace: "tiller-system",
|
||||
TLSKey: "key.pem",
|
||||
TLSCert: "cert.pem",
|
||||
TLSCACert: "ca.pem",
|
||||
},
|
||||
want: []string{
|
||||
"--version", "0.1",
|
||||
"--tiller-namespace", "tiller-system",
|
||||
"--tls",
|
||||
"--tls-key", "key.pem",
|
||||
"--tls-cert", "cert.pem",
|
||||
"--tls-ca-cert", "ca.pem",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tiller-from-defaults",
|
||||
defaults: HelmSpec{
|
||||
TLS: true,
|
||||
TillerNamespace: "tiller-system",
|
||||
TLSKey: "key.pem",
|
||||
TLSCert: "cert.pem",
|
||||
TLSCACert: "ca.pem",
|
||||
},
|
||||
release: &ReleaseSpec{
|
||||
Chart: "test/chart",
|
||||
Version: "0.1",
|
||||
Name: "test-charts",
|
||||
},
|
||||
want: []string{
|
||||
"--version", "0.1",
|
||||
"--tiller-namespace", "tiller-system",
|
||||
"--tls",
|
||||
"--tls-key", "key.pem",
|
||||
"--tls-cert", "cert.pem",
|
||||
"--tls-ca-cert", "ca.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -553,10 +631,18 @@ func Test_normalizeChart(t *testing.T) {
|
||||
|
||||
// mocking helmexec.Interface
|
||||
|
||||
type listKey struct {
|
||||
filter string
|
||||
flags string
|
||||
}
|
||||
|
||||
type mockHelmExec struct {
|
||||
charts []string
|
||||
repo []string
|
||||
releases []mockRelease
|
||||
deleted []mockRelease
|
||||
lists map[listKey]string
|
||||
diffed []mockRelease
|
||||
}
|
||||
|
||||
type mockRelease struct {
|
||||
@@ -602,20 +688,22 @@ func (helm *mockHelmExec) SyncRelease(name, chart string, flags ...string) error
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) DiffRelease(name, chart string, flags ...string) error {
|
||||
helm.diffed = append(helm.diffed, mockRelease{name: name, flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) ReleaseStatus(release string) error {
|
||||
func (helm *mockHelmExec) ReleaseStatus(release string, flags ...string) error {
|
||||
if strings.Contains(release, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.releases = append(helm.releases, mockRelease{name: release, flags: []string{}})
|
||||
helm.releases = append(helm.releases, mockRelease{name: release, flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error {
|
||||
helm.deleted = append(helm.deleted, mockRelease{name: name, flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) List(filter string) (string, error) {
|
||||
return "", nil
|
||||
func (helm *mockHelmExec) List(filter string, flags ...string) (string, error) {
|
||||
return helm.lists[listKey{filter: filter, flags: strings.Join(flags, "")}], nil
|
||||
}
|
||||
func (helm *mockHelmExec) DecryptSecret(name string) (string, error) {
|
||||
return "", nil
|
||||
@@ -726,6 +814,18 @@ func TestHelmState_SyncReleases(t *testing.T) {
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{}}},
|
||||
},
|
||||
{
|
||||
name: "with tiller args",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
TillerNamespace: "tillerns",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
{
|
||||
name: "escaped values",
|
||||
releases: []ReleaseSpec{
|
||||
@@ -806,6 +906,298 @@ func TestHelmState_SyncReleases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_DiffReleases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
releases []ReleaseSpec
|
||||
helm *mockHelmExec
|
||||
wantReleases []mockRelease
|
||||
}{
|
||||
{
|
||||
name: "normal release",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{}}},
|
||||
},
|
||||
{
|
||||
name: "with tiller args",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
TillerNamespace: "tillerns",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{"--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
{
|
||||
name: "escaped values",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "someList",
|
||||
Value: "a,b,c",
|
||||
},
|
||||
{
|
||||
Name: "json",
|
||||
Value: "{\"name\": \"john\"}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
|
||||
},
|
||||
{
|
||||
name: "set single value from file",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "FOO",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
File: "path/to/bar",
|
||||
},
|
||||
{
|
||||
Name: "baz",
|
||||
Value: "BAZ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
|
||||
},
|
||||
{
|
||||
name: "set single array value in an array",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "foo.bar[0]",
|
||||
Values: []string{
|
||||
"A",
|
||||
"B",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
Releases: tt.releases,
|
||||
logger: logger,
|
||||
}
|
||||
_, err := state.DiffReleases(tt.helm, []string{}, 1, false, false, false)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.helm.diffed, tt.wantReleases) {
|
||||
t.Errorf("HelmState.DiffReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.wantReleases)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_SyncReleasesCleanup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
releases []ReleaseSpec
|
||||
helm *mockHelmExec
|
||||
expectedNumRemovedFiles int
|
||||
}{
|
||||
{
|
||||
name: "normal release",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 0,
|
||||
},
|
||||
{
|
||||
name: "inline values",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
Values: []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"someList": "a,b,c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 1,
|
||||
},
|
||||
{
|
||||
name: "inline values and values file",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
Values: []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"someList": "a,b,c",
|
||||
},
|
||||
"someFile",
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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
|
||||
},
|
||||
}
|
||||
if errs := state.SyncReleases(tt.helm, []string{}, 1); errs != nil && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
||||
if errs := state.Clean(); errs != nil && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
||||
if numRemovedFiles != tt.expectedNumRemovedFiles {
|
||||
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_DiffReleasesCleanup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
releases []ReleaseSpec
|
||||
helm *mockHelmExec
|
||||
expectedNumRemovedFiles int
|
||||
}{
|
||||
{
|
||||
name: "normal release",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 0,
|
||||
},
|
||||
{
|
||||
name: "inline values",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
Values: []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"someList": "a,b,c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 1,
|
||||
},
|
||||
{
|
||||
name: "inline values and values file",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
Values: []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"someList": "a,b,c",
|
||||
},
|
||||
"someFile",
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
expectedNumRemovedFiles: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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
|
||||
},
|
||||
}
|
||||
if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false); errs != nil && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
||||
if errs := state.Clean(); errs != nil && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
||||
if numRemovedFiles != tt.expectedNumRemovedFiles {
|
||||
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_UpdateDeps(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: "/src",
|
||||
@@ -870,12 +1262,62 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
|
||||
helm: &mockHelmExec{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "complain missing values file for desired release",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "error",
|
||||
Values: []interface{}{
|
||||
"foo.yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should not complain missing values file for undesired release",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "error",
|
||||
Values: []interface{}{
|
||||
"foo.yaml",
|
||||
},
|
||||
Installed: boolValue(false),
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with tiller args",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseA",
|
||||
TillerNamespace: "tillerns",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []mockRelease{{"releaseA", []string{"--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i := func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
Releases: tt.releases,
|
||||
logger: logger,
|
||||
fileExists: func(f string) (bool, error) {
|
||||
if f != "foo.yaml" {
|
||||
return false, fmt.Errorf("unexpected file: %s", f)
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
readFile: func(f string) ([]byte, error) {
|
||||
if f != "foo.yaml" {
|
||||
return nil, fmt.Errorf("unexpected file: %s", f)
|
||||
}
|
||||
return []byte{}, nil
|
||||
},
|
||||
}
|
||||
errs := state.ReleaseStatuses(tt.helm, 1)
|
||||
if (errs != nil) != tt.wantErr {
|
||||
@@ -892,12 +1334,13 @@ func TestHelmState_ReleaseStatuses(t *testing.T) {
|
||||
|
||||
func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cleanup bool
|
||||
releases []ReleaseSpec
|
||||
helm *mockHelmExec
|
||||
want []mockRelease
|
||||
wantErr bool
|
||||
name string
|
||||
cleanup bool
|
||||
releases []ReleaseSpec
|
||||
helm *mockHelmExec
|
||||
want []mockRelease
|
||||
wantErr bool
|
||||
tillerNamespace string
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
@@ -930,6 +1373,17 @@ func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
|
||||
helm: &mockHelmExec{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with tiller args",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseA",
|
||||
TillerNamespace: "tillerns",
|
||||
},
|
||||
},
|
||||
helm: &mockHelmExec{},
|
||||
want: []mockRelease{{"releaseA", []string{"--timeout", "1", "--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i := func(t *testing.T) {
|
||||
@@ -996,3 +1450,131 @@ func TestHelmState_NoReleaseMatched(t *testing.T) {
|
||||
t.Run(tt.name, i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
deleted []mockRelease
|
||||
wantErr bool
|
||||
desired *bool
|
||||
installed bool
|
||||
purge bool
|
||||
flags string
|
||||
tillerNamespace string
|
||||
}{
|
||||
{
|
||||
name: "desired and installed (purge=false)",
|
||||
wantErr: false,
|
||||
desired: boolValue(true),
|
||||
installed: true,
|
||||
purge: false,
|
||||
deleted: []mockRelease{{"releaseA", []string{}}},
|
||||
},
|
||||
{
|
||||
name: "desired(default) and installed (purge=false)",
|
||||
wantErr: false,
|
||||
desired: nil,
|
||||
installed: true,
|
||||
purge: false,
|
||||
deleted: []mockRelease{{"releaseA", []string{}}},
|
||||
},
|
||||
{
|
||||
name: "desired and installed (purge=true)",
|
||||
wantErr: false,
|
||||
desired: boolValue(true),
|
||||
installed: true,
|
||||
purge: true,
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge"}}},
|
||||
},
|
||||
{
|
||||
name: "desired but not installed (purge=false)",
|
||||
wantErr: false,
|
||||
desired: boolValue(true),
|
||||
installed: false,
|
||||
purge: false,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "desired but not installed (purge=true)",
|
||||
wantErr: false,
|
||||
desired: boolValue(true),
|
||||
installed: false,
|
||||
purge: true,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "installed but filtered (purge=false)",
|
||||
wantErr: false,
|
||||
desired: boolValue(false),
|
||||
installed: true,
|
||||
purge: false,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "installed but filtered (purge=true)",
|
||||
wantErr: false,
|
||||
desired: boolValue(false),
|
||||
installed: true,
|
||||
purge: true,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "not installed, and filtered (purge=false)",
|
||||
wantErr: false,
|
||||
desired: boolValue(false),
|
||||
installed: false,
|
||||
purge: false,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "not installed, and filtered (purge=true)",
|
||||
wantErr: false,
|
||||
desired: boolValue(false),
|
||||
installed: false,
|
||||
purge: true,
|
||||
deleted: []mockRelease{},
|
||||
},
|
||||
{
|
||||
name: "with tiller args",
|
||||
wantErr: false,
|
||||
desired: nil,
|
||||
installed: true,
|
||||
purge: true,
|
||||
tillerNamespace: "tillerns",
|
||||
flags: "--tiller-namespacetillerns",
|
||||
deleted: []mockRelease{{"releaseA", []string{"--purge", "--tiller-namespace", "tillerns"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i := func(t *testing.T) {
|
||||
release := ReleaseSpec{
|
||||
Name: "releaseA",
|
||||
Installed: tt.desired,
|
||||
TillerNamespace: tt.tillerNamespace,
|
||||
}
|
||||
releases := []ReleaseSpec{
|
||||
release,
|
||||
}
|
||||
state := &HelmState{
|
||||
Releases: releases,
|
||||
logger: logger,
|
||||
}
|
||||
helm := &mockHelmExec{
|
||||
lists: map[listKey]string{},
|
||||
deleted: []mockRelease{},
|
||||
}
|
||||
if tt.installed {
|
||||
helm.lists[listKey{filter: "^releaseA$", flags: tt.flags}] = "releaseA"
|
||||
}
|
||||
errs := state.DeleteReleases(helm, tt.purge)
|
||||
if (errs != nil) != tt.wantErr {
|
||||
t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(tt.deleted, helm.deleted) {
|
||||
t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.deleted)
|
||||
}
|
||||
}
|
||||
t.Run(tt.name, i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ func TestRenderTemplate_Defaulting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplateToString(s string) (string, error) {
|
||||
func renderTemplateToString(s string, data ...interface{}) (string, error) {
|
||||
ctx := &Context{readFile: func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
|
||||
}}
|
||||
tplString, err := ctx.RenderTemplateToBuffer(s)
|
||||
tplString, err := ctx.RenderTemplateToBuffer(s, data...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -125,6 +125,7 @@ func Test_renderTemplateToString(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
envs map[string]string
|
||||
data interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -177,6 +178,18 @@ func Test_renderTemplateToString(t *testing.T) {
|
||||
want: "7",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get",
|
||||
args: args{
|
||||
s: `{{ . | get "Foo" }}, {{ . | get "Bar" "2" }}`,
|
||||
envs: map[string]string{},
|
||||
data: map[string]interface{}{
|
||||
"Foo": "1",
|
||||
},
|
||||
},
|
||||
want: "1, 2",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "env var not set",
|
||||
args: args{
|
||||
@@ -225,7 +238,7 @@ func Test_renderTemplateToString(t *testing.T) {
|
||||
t.Error("renderTemplateToString() could not set env var for testing")
|
||||
}
|
||||
}
|
||||
got, err := renderTemplateToString(tt.args.s)
|
||||
got, err := renderTemplateToString(tt.args.s, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
return
|
||||
|
||||
+39
-8
@@ -14,36 +14,67 @@ func (e *noValueError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func get(path string, obj interface{}) (interface{}, error) {
|
||||
func get(path string, varArgs ...interface{}) (interface{}, error) {
|
||||
var defSet bool
|
||||
var def interface{}
|
||||
var obj interface{}
|
||||
switch len(varArgs) {
|
||||
case 1:
|
||||
defSet = false
|
||||
def = nil
|
||||
obj = varArgs[0]
|
||||
case 2:
|
||||
defSet = true
|
||||
def = varArgs[0]
|
||||
obj = varArgs[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected number of args pased to the template function get(path, [def, ]obj): expected 1 or 2, got %d, args was %v", len(varArgs), varArgs)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return obj, nil
|
||||
}
|
||||
keys := strings.Split(path, ".")
|
||||
var v interface{}
|
||||
var ok bool
|
||||
switch typedObj := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
v, ok := typedObj[keys[0]]
|
||||
v, ok = typedObj[keys[0]]
|
||||
if !ok {
|
||||
if defSet {
|
||||
return def, nil
|
||||
}
|
||||
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
|
||||
}
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
case map[interface{}]interface{}:
|
||||
v, ok := typedObj[keys[0]]
|
||||
v, ok = typedObj[keys[0]]
|
||||
if !ok {
|
||||
if defSet {
|
||||
return def, nil
|
||||
}
|
||||
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
|
||||
}
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
default:
|
||||
maybeStruct := reflect.ValueOf(typedObj)
|
||||
if maybeStruct.NumField() < 1 {
|
||||
if maybeStruct.Kind() != reflect.Struct {
|
||||
return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])}
|
||||
} else if maybeStruct.NumField() < 1 {
|
||||
return nil, &noValueError{fmt.Sprintf("no accessible struct fields for key \"%s\"", keys[0])}
|
||||
}
|
||||
f := maybeStruct.FieldByName(keys[0])
|
||||
if !f.IsValid() {
|
||||
if defSet {
|
||||
return def, nil
|
||||
}
|
||||
return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)}
|
||||
}
|
||||
v := f.Interface()
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
v = f.Interface()
|
||||
}
|
||||
|
||||
if defSet {
|
||||
return get(strings.Join(keys[1:], "."), def, v)
|
||||
}
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
}
|
||||
|
||||
func getOrNil(path string, o interface{}) (interface{}, error) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type EmptyStruct struct {
|
||||
}
|
||||
|
||||
func TestGetStruct(t *testing.T) {
|
||||
type Foo struct{ Bar string }
|
||||
|
||||
@@ -23,6 +26,12 @@ func TestGetStruct(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but was not occurred")
|
||||
}
|
||||
|
||||
_, err = get("foo", EmptyStruct{})
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected error but was not occurred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMap(t *testing.T) {
|
||||
@@ -44,6 +53,34 @@ func TestGetMap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet_Default(t *testing.T) {
|
||||
obj := map[string]interface{}{"Foo": map[string]interface{}{}, "foo": 1}
|
||||
|
||||
v1, err := get("Foo.Bar", "Bar", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v1 != "Bar" {
|
||||
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
|
||||
}
|
||||
|
||||
v2, err := get("Baz", "Baz", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v2 != "Baz" {
|
||||
t.Errorf("unexpected value for path Baz in %v: expected=Baz, actual=%v", obj, v2)
|
||||
}
|
||||
|
||||
_, err = get("foo.Bar", "fooBar", obj)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected error but was not occurred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrNilStruct(t *testing.T) {
|
||||
type Foo struct{ Bar string }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user