From 16288dfa7da390bee208f46eaeb69061507c7ca8 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Wed, 27 May 2020 11:42:43 +0900 Subject: [PATCH] feat: GA of Kustomize and K8s manifests support (#1172) This is the GA version of the helm-x integration #673 developed last year. You get all the following benefits without an extra helm plugin: - Ability to add ad-hoc chart dependencies/aliases, without forking the chart (Fixes #876 ) - Ability to patch resulting K8s resources before installing the helm chart - Ability to install a kustomization as a chart (Requires `kustomize` binary to be available in `$PATH` - Ability to install a directory of K8s manifests as a chart - etc. --- docs/advanced-features.md | 91 ++++++++++++++++ go.mod | 1 + go.sum | 27 +++++ pkg/app/app.go | 212 +++++++++++++++++++++++++++++--------- pkg/app/app_test.go | 68 ------------ pkg/app/destroy_test.go | 10 -- pkg/app/diff_test.go | 16 --- pkg/app/run.go | 35 +++++++ pkg/state/helmx.go | 47 +++++++-- pkg/state/state.go | 75 ++++++++------ pkg/state/state_run.go | 2 - 11 files changed, 400 insertions(+), 184 deletions(-) create mode 100644 docs/advanced-features.md diff --git a/docs/advanced-features.md b/docs/advanced-features.md new file mode 100644 index 00000000..5d391686 --- /dev/null +++ b/docs/advanced-features.md @@ -0,0 +1,91 @@ +## Advanced Features + +- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile) + +### Import Configuration Parameters into Helmfile + +Helmfile integrates [vals]() to import configuration parameters from following backends: + +- AWS SSM Parameter Store +- AWS SecretsManager +- Vault +- SOPS + +See [Vals "Suported Backends"](https://github.com/variantdev/vals#suported-backends) for the full list of available backends. + +This feature was implemented in https://github.com/roboll/helmfile/pull/906. +If you're curious how it's designed and how it works, please consult the pull request. + +### Deploy Kustomizations with Helmfile + +You can deploy [kustomize](https://github.com/kubernetes-sigs/kustomize) "kustomization"s with Helmfile. + +Most of Kustomize operations that is usually done with `kustomize edit` can be done declaratively via Helm values.yaml files. + +Under the hood, Helmfile transforms the kustomization into a local chart in a temporary directory so that it can be `helm upgrade --install`ed. + +The transformation is done by generating (1)a temporary kustomization from various options and (2)temporary chart from the temporary kustomization. + +An example pseudo code for the transformation logic can be written as: + +```console +$ TMPCHART=/tmp/sometmpdir +$ mkdir -p ${TMPCHART}/templates +$ somehow_generate_chart_yaml ${TMPCHART}/Chart.yaml + +$ TMPKUSTOMIZATION=/tmp/sometmpdir2 +$ somehow_generate_temp_kustomization_yaml ${TMPKUSTOMIZATION}/kustomization.yaml +$ kustomize build ${TMPKUSTOMIZATION}/kustomization.yaml > ${TMPCHART}/templates/all.yaml +``` + +Let's say you have a `helmfile.yaml` that looks like the below: + +```yaml +releases: +- name: myapp + chart: mykustomization + values: + - values.yaml +``` + +Helmfile firstly generates a temporary `kustomization.yaml` that looks like: + +```yaml +bases: +- $(ABS_PATH_TO_HELMFILE_YAML}/mykustomization +``` + +Followed by the below steps: + +- Running `kustomize edit set image $IMAGE` for every `$IMAGE` generated from your values.yaml +- Running `kustomize edit set nameprefix $NAMEPREFIX` with the nameprefix specified in your values.yaml +- Running `kustomize edit set namesuffix $NAMESUFFIX` with the namesuffix specified in your values.yaml +- Running `kustomize edit set namespace $NS` with the namespace specified in your values.yaml + +A `values.yaml` file for kustomization would look like the below: + +```yaml +images: +# kustomize edit set image mysql=eu.gcr.io/my-project/mysql@canary +- name: mysql + newName: eu.gcr.io/my-project/mysql + newTag: canary +# kustomize edit set image myapp=my-registry/my-app@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 +- name: myapp + digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 + newName: my-registry/my-app + +# kustomize edit set nameprefix foo- +namePrefix: foo- + +# kustomize edit set namesuffix -bar +nameSuffix: -bar + +# kustomize edit set namespace myapp +namespace: myapp +``` + +At this point, Helmfile can generate a complete kustomization from the base kustomization you specified in `releases[].chart` of your helmfile.yaml and `values.yaml`, +which can be included in the temporary chart. + +After all, Helmfile just installs the temporary chart like standard charts, which allows you to manage everything with Helmfile regardless of each app is declared using a Helm chart or a kustomization. diff --git a/go.mod b/go.mod index 3e34e9bb..8aae4e81 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 github.com/urfave/cli v1.20.0 + github.com/variantdev/chartify v0.3.0 github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363 github.com/variantdev/vals v0.4.1-0.20200501114609-9cebe482281c go.mozilla.org/sops v0.0.0-20190912205235-14a22d7a7060 // indirect diff --git a/go.sum b/go.sum index 2800b547..93aca979 100644 --- a/go.sum +++ b/go.sum @@ -278,6 +278,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= @@ -646,6 +647,12 @@ github.com/oracle/oci-go-sdk v7.0.0+incompatible h1:oj5ESjXwwkFRdhZSnPlShvLWYdt/ github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8= github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= +github.com/otiai10/copy v1.1.1 h1:PH7IFlRQ6Fv9vYmuXbDRLdgTHoP1w483kPNUP2bskpo= +github.com/otiai10/copy v1.1.1/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -774,6 +781,20 @@ 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 v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/variantdev/chartify v0.0.0-20200330123007-ddc79388188c h1:PiAOZUdKtf8tIfps1fLaEsP9BCTT8hR9FawTfAuLY5o= +github.com/variantdev/chartify v0.0.0-20200330123007-ddc79388188c/go.mod h1:DzFQ1XeNBpbLdbze39ai3auS4b+Xo/KzRluEY/LyqlM= +github.com/variantdev/chartify v0.1.0 h1:RHo/di1tXvydrJwwbHoWduvG3r9nU0oj7RvuxmQLCW0= +github.com/variantdev/chartify v0.1.0/go.mod h1:DzFQ1XeNBpbLdbze39ai3auS4b+Xo/KzRluEY/LyqlM= +github.com/variantdev/chartify v0.1.1 h1:4K+6XotrJ/U5BoQc29BTOWpNGnzsCBEDahApiOoPURc= +github.com/variantdev/chartify v0.1.1/go.mod h1:DzFQ1XeNBpbLdbze39ai3auS4b+Xo/KzRluEY/LyqlM= +github.com/variantdev/chartify v0.1.2 h1:BAaZawFXJfcgk6HVM3o+/Q9Xzyv1rkPd9m+YeWM7+dg= +github.com/variantdev/chartify v0.1.2/go.mod h1:DzFQ1XeNBpbLdbze39ai3auS4b+Xo/KzRluEY/LyqlM= +github.com/variantdev/chartify v0.1.3 h1:3WEaT1ZYLaivcVdlE2poxBCw2KxNEaOzHB9k4rj5zXk= +github.com/variantdev/chartify v0.1.3/go.mod h1:DzFQ1XeNBpbLdbze39ai3auS4b+Xo/KzRluEY/LyqlM= +github.com/variantdev/chartify v0.2.0 h1:oaG/o+juw1C5PTAbRlsxUTgGYPmIneoqnYI7KQyVJJU= +github.com/variantdev/chartify v0.2.0/go.mod h1:0tw+4doFHsNnhttYx7I9Pv/dsZ82BD4UuTV9saBOcfw= +github.com/variantdev/chartify v0.3.0 h1:vm9cY+Amj44g7scz/FVpa3s13XqzFvTJrdM6iufiKEM= +github.com/variantdev/chartify v0.3.0/go.mod h1:0tw+4doFHsNnhttYx7I9Pv/dsZ82BD4UuTV9saBOcfw= github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363 h1:KrfQBEUn+wEOQ/6UIfoqRDvn+Q/wZridQ7t0G1vQqKE= github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363/go.mod h1:pH1TQsNSLj2uxMo9NNl9zdGy01Wtn+/2MT96BrKmVyE= github.com/variantdev/vals v0.4.0 h1:O1O7/sWhlvozcY2DjZBzlE1notxwVo6UBT1+w7HsO/k= @@ -1105,6 +1126,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= +gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d h1:LCPbGQ34PMrwad11aMZ+dbz5SAsq/0ySjRwQ8I9Qwd8= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1128,6 +1153,8 @@ k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b h1:fVkKJL9FIpA8LSJyHVM00M k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b/go.mod h1:FW86P8YXVLsbuplGMZeb20J3jYHscrDqw4jELaFJvRU= k8s.io/klog v0.0.0-20190306015804-8e90cee79f82 h1:SHucoAy7lRb+w5oC/hbXyZg+zX+Wftn6hD4tGzHCVqA= k8s.io/klog v0.0.0-20190306015804-8e90cee79f82/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= layeh.com/radius v0.0.0-20190322222518-890bc1058917 h1:BDXFaFzUt5EIqe/4wrTc4AcYZWP6iC6Ult+jQWLh5eU= layeh.com/radius v0.0.0-20190322222518-890bc1058917/go.mod h1:fywZKyu//X7iRzaxLgPWsvc0L26IUpVvE/aeIL2JtIQ= diff --git a/pkg/app/app.go b/pkg/app/app.go index 42064dd0..c92c1d24 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -100,20 +100,44 @@ func Init(app *App) *App { } func (a *App) Deps(c DepsConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { - return run.Deps(c) + return a.ForEachStateFiltered(func(run *Run) (errs []error) { + err := run.withPreparedCharts(false, func() { + errs = run.Deps(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) Repos(c ReposConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { - return run.Repos(c) + return a.ForEachStateFiltered(func(run *Run) (errs []error) { + err := run.withPreparedCharts(false, func() { + errs = run.Repos(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { - return run.DeprecatedSyncCharts(c) + return a.ForEachStateFiltered(func(run *Run) (errs []error) { + err := run.withPreparedCharts(false, func() { + errs = run.DeprecatedSyncCharts(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } @@ -125,12 +149,24 @@ func (a *App) Diff(c DiffConfigProvider) error { err := a.ForEachState(func(run *Run) (bool, []error) { var criticalErrs []error - msg, matched, affected, errs := run.Diff(c) + var msg *string + + var matched, affected bool + + var errs []error + + err := run.withPreparedCharts(false, func() { + msg, matched, affected, errs = run.Diff(c) + }) if msg != nil { a.Logger.Info(*msg) } + if err != nil { + errs = append(errs, err) + } + affectedAny = affectedAny || affected for i := range errs { @@ -171,20 +207,44 @@ func (a *App) Diff(c DiffConfigProvider) error { } func (a *App) Template(c TemplateConfigProvider) error { - return a.ForEachState(func(run *Run) (bool, []error) { - return a.template(run, c) + return a.ForEachState(func(run *Run) (ok bool, errs []error) { + err := run.withPreparedCharts(true, func() { + ok, errs = a.template(run, c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) Lint(c LintConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { - return run.Lint(c) + return a.ForEachStateFiltered(func(run *Run) (errs []error) { + err := run.withPreparedCharts(true, func() { + errs = run.Lint(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) Sync(c SyncConfigProvider) error { - return a.ForEachState(func(run *Run) (bool, []error) { - return a.sync(run, c) + return a.ForEachState(func(run *Run) (ok bool, errs []error) { + err := run.withPreparedCharts(false, func() { + ok, errs = a.sync(run, c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } @@ -197,14 +257,22 @@ func (a *App) Apply(c ApplyConfigProvider) error { opts = append(opts, SetRetainValuesFiles(c.RetainValuesFiles())) - err := a.ForEachState(func(run *Run) (bool, []error) { - matched, updated, errs := a.apply(run, c) + err := a.ForEachState(func(run *Run) (ok bool, errs []error) { + err := run.withPreparedCharts(false, func() { + matched, updated, es := a.apply(run, c) - mut.Lock() - any = any || updated - mut.Unlock() + mut.Lock() + any = any || updated + mut.Unlock() - return matched, errs + ok, errs = matched, es + }) + + if err != nil { + errs = append(errs, err) + } + + return }, opts...) if err != nil { @@ -221,43 +289,85 @@ func (a *App) Apply(c ApplyConfigProvider) error { } func (a *App) Status(c StatusesConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { - return run.Status(c) + return a.ForEachStateFiltered(func(run *Run) (errs []error) { + err := run.withPreparedCharts(false, func() { + errs = run.Status(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) Delete(c DeleteConfigProvider) error { - return a.ForEachState(func(run *Run) (bool, []error) { - return a.delete(run, c.Purge(), c) + return a.ForEachState(func(run *Run) (ok bool, errs []error) { + err := run.withPreparedCharts(false, func() { + ok, errs = a.delete(run, c.Purge(), c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }, SetReverse(true)) } func (a *App) Destroy(c DestroyConfigProvider) error { - return a.ForEachState(func(run *Run) (bool, []error) { - return a.delete(run, true, c) + return a.ForEachState(func(run *Run) (ok bool, errs []error) { + err := run.withPreparedCharts(false, func() { + ok, errs = a.delete(run, true, c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }, SetReverse(true)) } func (a *App) Test(c TestConfigProvider) error { - return a.ForEachStateFiltered(func(run *Run) []error { + return a.ForEachStateFiltered(func(run *Run) (errs []error) { if c.Cleanup() && run.helm.IsHelm3() { a.Logger.Warnf("warn: requested cleanup will not be applied. " + "To clean up test resources with Helm 3, you have to remove them manually " + "or set helm.sh/hook-delete-policy\n") } - return run.Test(c) + err := run.withPreparedCharts(false, func() { + errs = run.Test(c) + }) + + if err != nil { + errs = append(errs, err) + } + + return }) } func (a *App) PrintState(c StateConfigProvider) error { - return a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState) []error { - state, err := st.ToYaml() + return a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState) (errs []error) { + err := NewRun(st, nil, NewContext()).withPreparedCharts(false, func() { + state, err := st.ToYaml() + if err != nil { + errs = []error{err} + return + } + fmt.Printf("---\n# Source: %s\n\n%+v", st.FilePath, state) + + errs = []error{} + }) + if err != nil { - return []error{err} + errs = append(errs, err) } - fmt.Printf("---\n# Source: %s\n\n%+v", st.FilePath, state) - return []error{} + + return }) } @@ -265,22 +375,32 @@ func (a *App) ListReleases(c ListConfigProvider) error { var releases []*HelmRelease err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState) []error { - //var releases m - for _, r := range st.Releases { - labels := "" - for k, v := range r.Labels { - labels = fmt.Sprintf("%s,%s:%s", labels, k, v) + err := NewRun(st, nil, NewContext()).withPreparedCharts(false, func() { + + //var releases m + for _, r := range st.Releases { + labels := "" + for k, v := range r.Labels { + labels = fmt.Sprintf("%s,%s:%s", labels, k, v) + } + labels = strings.Trim(labels, ",") + installed := r.Installed == nil || *r.Installed + releases = append(releases, &HelmRelease{ + Name: r.Name, + Namespace: r.Namespace, + Enabled: installed, + Labels: labels, + }) } - labels = strings.Trim(labels, ",") - installed := r.Installed == nil || *r.Installed - releases = append(releases, &HelmRelease{ - Name: r.Name, - Namespace: r.Namespace, - Enabled: installed, - Labels: labels, - }) + }) + + var errs []error + + if err != nil { + errs = append(errs, err) } - return []error{} + + return errs }) if err != nil { diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 555b928c..4cb46147 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2715,10 +2715,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 10 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: anotherbackend (charts/anotherbackend) UPDATED backend-v1 (charts/backend) DELETED @@ -2739,11 +2735,7 @@ GROUP RELEASES 5 logging, front-proxy processing releases in group 1/5: frontend-v1, frontend-v2, frontend-v3 -worker 1/1 started -worker 1/1 finished processing releases in group 2/5: backend-v1, backend-v2 -worker 1/1 started -worker 1/1 finished processing releases in group 3/5: anotherbackend processing releases in group 4/5: database, servicemesh processing releases in group 5/5: logging, front-proxy @@ -2756,37 +2748,17 @@ GROUP RELEASES 5 frontend-v1, frontend-v2, frontend-v3 processing releases in group 1/5: logging, front-proxy -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^logging$ --kube-contextdefault--deployed--failed--pending} getting deployed release version failed:unexpected list key: {^front-proxy$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 2/5: database, servicemesh -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^database$ --kube-contextdefault--deployed--failed--pending} getting deployed release version failed:unexpected list key: {^servicemesh$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 3/5: anotherbackend -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^anotherbackend$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 4/5: backend-v1, backend-v2 -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^backend-v2$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 5/5: frontend-v1, frontend-v2, frontend-v3 -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^frontend-v3$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished UPDATED RELEASES: NAME CHART VERSION @@ -2904,10 +2876,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 3 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: bar (mychart2) UPDATED baz (mychart3) UPDATED @@ -2919,18 +2887,10 @@ GROUP RELEASES 2 foo processing releases in group 1/2: baz, bar -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^baz$ --kube-contextdefault--deployed--failed--pending} getting deployed release version failed:unexpected list key: {^bar$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 2/2: foo -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^foo$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished UPDATED RELEASES: NAME CHART VERSION @@ -3184,10 +3144,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 2 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: bar (mychart2) UPDATED foo (mychart1) UPDATED @@ -3198,17 +3154,9 @@ GROUP RELEASES 2 tns2/ns2/bar processing releases in group 1/2: tns1/ns1/foo -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^foo$ --tiller-namespacetns1--kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 2/2: tns2/ns2/bar -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^bar$ --tiller-namespacetns2--kube-contextdefault--deployed--failed--pending} -worker 1/1 finished UPDATED RELEASES: NAME CHART VERSION @@ -3532,10 +3480,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 2 release(s) matching app=test found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: external-secrets (incubator/raw) UPDATED my-release (incubator/raw) UPDATED @@ -3552,19 +3496,11 @@ processing releases in group 1/3: kube-system/kubernetes-external-secrets processing releases in group 2/3: default/external-secrets 1 release(s) matching app=test found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished processing releases in group 3/3: default/my-release 1 release(s) matching app=test found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deployed--failed--pending} -worker 1/1 finished UPDATED RELEASES: NAME CHART VERSION @@ -3736,10 +3672,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 2 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: baz (mychart3) UPDATED foo (mychart1) UPDATED diff --git a/pkg/app/destroy_test.go b/pkg/app/destroy_test.go index c7157348..ceacaaf6 100644 --- a/pkg/app/destroy_test.go +++ b/pkg/app/destroy_test.go @@ -295,30 +295,20 @@ GROUP RELEASES 5 front-proxy, logging processing releases in group 1/5: frontend-v3, frontend-v2, frontend-v1 -worker 1/1 started release "frontend-v3" processed release "frontend-v2" processed release "frontend-v1" processed -worker 1/1 finished processing releases in group 2/5: backend-v2, backend-v1 -worker 1/1 started release "backend-v2" processed release "backend-v1" processed -worker 1/1 finished processing releases in group 3/5: anotherbackend -worker 1/1 started release "anotherbackend" processed -worker 1/1 finished processing releases in group 4/5: database, servicemesh -worker 1/1 started release "database" processed release "servicemesh" processed -worker 1/1 finished processing releases in group 5/5: front-proxy, logging -worker 1/1 started release "front-proxy" processed release "logging" processed -worker 1/1 finished DELETED RELEASES: NAME diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index 6abf91be..2069fdd3 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -324,10 +324,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 10 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: anotherbackend (charts/anotherbackend) UPDATED backend-v1 (charts/backend) DELETED @@ -440,10 +436,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 3 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: bar (mychart2) UPDATED baz (mychart3) UPDATED @@ -687,10 +679,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 2 release(s) found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: bar (mychart2) UPDATED foo (mychart1) UPDATED @@ -1001,10 +989,6 @@ second-pass rendering result of "helmfile.yaml.part.0": merged environment: &{default map[] map[]} 2 release(s) matching app=test found in helmfile.yaml -worker 1/1 started -worker 1/1 finished -worker 1/1 started -worker 1/1 finished Affected releases are: external-secrets (incubator/raw) UPDATED my-release (incubator/raw) UPDATED diff --git a/pkg/app/run.go b/pkg/app/run.go index 3dee83ab..b1ed6d43 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -5,6 +5,8 @@ import ( "github.com/roboll/helmfile/pkg/argparser" "github.com/roboll/helmfile/pkg/helmexec" "github.com/roboll/helmfile/pkg/state" + "io/ioutil" + "os" "sort" "strings" ) @@ -14,6 +16,8 @@ type Run struct { helm helmexec.Interface ctx Context + ReleaseToChart map[string]string + Ask func(string) bool } @@ -28,6 +32,37 @@ func (r *Run) askForConfirmation(msg string) bool { return AskForConfirmation(msg) } +func (r *Run) withPreparedCharts(forceDownload bool, f func()) error { + if r.ReleaseToChart != nil { + panic("Run.PrepareCharts can be called only once") + } + + // Create tmp directory and bail immediately if it fails + dir, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(dir) + + releaseToChart, errs := state.PrepareCharts(r.helm, r.state, dir, 2, "template", forceDownload) + + if len(errs) > 0 { + return fmt.Errorf("%v", errs) + } + + for i := range r.state.Releases { + rel := &r.state.Releases[i] + + rel.Chart = releaseToChart[rel.Name] + } + + r.ReleaseToChart = releaseToChart + + f() + + return nil +} + func (r *Run) Deps(c DepsConfigProvider) []error { r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index 92e11441..afc5f99a 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -1,5 +1,10 @@ package state +import ( + "github.com/variantdev/chartify" + "strings" +) + type Dependency struct { Chart string `yaml:"chart"` Version string `yaml:"version"` @@ -7,49 +12,73 @@ type Dependency struct { } func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) ([]string, error) { + for _, adopt := range release.Adopt { + flags = append(flags, "--adopt", adopt) + } + + return flags, nil +} + +func (st *HelmState) PrepareChartify(release *ReleaseSpec) (bool, *chartify.ChartifyOpts) { + var opts chartify.ChartifyOpts + + var shouldRun bool + for _, d := range release.Dependencies { var dep string + if d.Alias != "" { dep += d.Alias + "=" + } else { + a := strings.Split(d.Chart, "/") + + chart := a[len(a)-1] + + dep += chart + "=" } + dep += d.Chart + if d.Version != "" { dep += ":" + d.Version } - flags = append(flags, "--dependency", dep) - } - for _, adopt := range release.Adopt { - flags = append(flags, "--adopt", adopt) + opts.AdhocChartDependencies = append(opts.AdhocChartDependencies, dep) + + shouldRun = true } jsonPatches := release.JSONPatches if len(jsonPatches) > 0 { generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler) if err != nil { - return nil, err + return false, nil } for _, f := range generatedFiles { - flags = append(flags, "--json-patch", f) + opts.JsonPatches = append(opts.JsonPatches, f) } release.generatedValues = append(release.generatedValues, generatedFiles...) + + shouldRun = true } strategicMergePatches := release.StrategicMergePatches if len(strategicMergePatches) > 0 { generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler) if err != nil { - return nil, err + return false, nil } for _, f := range generatedFiles { - flags = append(flags, "--strategic-merge-patch", f) + opts.StrategicMergePatches = append(opts.StrategicMergePatches, f) } release.generatedValues = append(release.generatedValues, generatedFiles...) + + shouldRun = true } - return flags, nil + return shouldRun, &opts } diff --git a/pkg/state/state.go b/pkg/state/state.go index 43498079..415f5d2f 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/variantdev/chartify" "io" "io/ioutil" "os" @@ -699,8 +700,20 @@ func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helme } } -// downloadCharts will download and untar charts for Lint and Template -func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string) (map[string]string, []error) { +// PrepareCharts creates temporary directories of charts. +// +// Each resulting "chart" can be one of the followings: +// +// (1) local chart +// (2) temporary local chart generated from kustomization or manifests +// (3) remote chart +// +// When running `helmfile lint` or `helmfile template`, PrepareCharts will download and untar charts for linting and templating. +// +// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart. +// +// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors. +func PrepareCharts(helm helmexec.Interface, st *HelmState, dir string, concurrency int, helmfileCommand string, forceDownload bool) (map[string]string, []error) { temp := make(map[string]string, len(st.Releases)) type downloadResults struct { releaseName string @@ -711,6 +724,12 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr jobQueue := make(chan *ReleaseSpec, len(st.Releases)) results := make(chan *downloadResults, len(st.Releases)) + var helm3 bool + + if helm != nil { + helm3 = helm.IsHelm3() + } + st.scatterGather( concurrency, len(st.Releases), @@ -722,8 +741,24 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr }, func(_ int) { for release := range jobQueue { - chartPath := "" - if pathExists(normalizeChart(st.basePath, release.Chart)) { + var chartPath string + + if shouldChartify, opts := st.PrepareChartify(release); shouldChartify { + c := chartify.New( + chartify.HelmBin(st.DefaultHelmBinary), + chartify.UseHelm3(helm3), + ) + + out, err := c.Chartify(release.Name, release.Chart, chartify.WithChartifyOpts(opts)) + if err != nil { + errs = append(errs, err) + } else { + // TODO Chartify + chartPath = out + } + } else if !forceDownload { + chartPath = release.Chart + } else if pathExists(normalizeChart(st.basePath, release.Chart)) { chartPath = normalizeChart(st.basePath, release.Chart) } else { fetchFlags := []string{} @@ -751,6 +786,7 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr chartPath = filepath.Dir(fullChartPath) } } + results <- &downloadResults{release.Name, chartPath} } }, @@ -789,20 +825,6 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, helm.SetExtraArgs() errs := []error{} - // Create tmp directory and bail immediately if it fails - dir, err := ioutil.TempDir("", "") - if err != nil { - errs = append(errs, err) - return errs - } - defer os.RemoveAll(dir) - - temp, errs := st.downloadCharts(helm, dir, workerLimit, "template") - - if errs != nil { - errs = append(errs, err) - return errs - } if len(args) > 0 { helm.SetExtraArgs(args...) @@ -856,7 +878,7 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, } if len(errs) == 0 { - if err := helm.TemplateRelease(release.Name, temp[release.Name], flags...); err != nil { + if err := helm.TemplateRelease(release.Name, release.Chart, flags...); err != nil { errs = append(errs, err) } } @@ -894,19 +916,6 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st helm.SetExtraArgs() errs := []error{} - // Create tmp directory and bail immediately if it fails - dir, err := ioutil.TempDir("", "") - if err != nil { - errs = append(errs, err) - return errs - } - defer os.RemoveAll(dir) - - temp, errs := st.downloadCharts(helm, dir, workerLimit, "lint") - if errs != nil { - errs = append(errs, err) - return errs - } if len(args) > 0 { helm.SetExtraArgs(args...) @@ -942,7 +951,7 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st } if len(errs) == 0 { - if err := helm.Lint(release.Name, temp[release.Name], flags...); err != nil { + if err := helm.Lint(release.Name, release.Chart, flags...); err != nil { errs = append(errs, err) } } diff --git a/pkg/state/state_run.go b/pkg/state/state_run.go index b86bd8c2..b46ab948 100644 --- a/pkg/state/state_run.go +++ b/pkg/state/state_run.go @@ -38,9 +38,7 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun for w := 1; w <= concurrency; w++ { go func(id int) { - st.logger.Debugf("worker %d/%d started", id, concurrency) receiveInputsAndProduceIntermediates(id) - st.logger.Debugf("worker %d/%d finished", id, concurrency) waitGroup.Done() }(w) }