Repository: roboll/helmfile (go)

Issue:
Chartify renders objects with incorrect apiVersion
Hey guys,

Just another issue I've noticed with Chartify and Helmfile: while trying to install `prometheus-community/kube-prometheus-stack` chart with Helmfile and setting `forceNamespace` key in my `helmfile.yaml`, the Ingress object is generated with `apiVersion: networking.k8s.io/v1` which is not supported by our current K8s version. So it seems that the logic in `kube-prometheus-stack.ingress.apiVersion` [helper function](https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/templates/_helpers.tpl#L104) is just ignored.

And then, when I'm trying to do, for example, `helmfile diff` I get the following error:
```
error: unable to recognize "/var/folders/km/x2g0dhc17mn7sb5czxsmz3gw0000gp/T/chartify134751137/arn:aws:eks:eu-west-1:XXX:cluster/XXX/monitoring/prometheus-operator-ec2/kube-prometheus-stack/files/templates/prometheus/ingress.yaml": no matches for kind "Ingress" in version "networking.k8s.io/v1"
```

However, once I remove the `forceNamespace`, everything is working just fine.

Could you please assist?

How to reproduce: basically the configurations are the same as I described in #1857, but the cluster version should be the one that doesn't support the `networking.k8s.io/v1` API version yet(in our case it's 1.18, which only supports `networking.k8s.io/v1beta1`).

K8s version: 1.18
Helm version: 3.5.4
Helmfile version: 0.139.7

Thanks a lot,

Alex.

Pull request:
Enhance support for kube-version and api-versions
This adds support for `kube-version` and `api-versions` to be available to `chartify` so that it works even if your release requires `chartify` due to that you use features like `forceNamespace`, `jsonPatches`, `strategicMergePatches`, and so on.

This also enhances `ReleaseSpec` which corresponds to each item of `releases[]` in your `helmfile.yaml` to also accept `kubeVersion` and `apiVersions`, in addition to the top-level `kubeVersion` and `apiVersions` we have today.

The top-level ones works as the default values for release-specific ones. If you have been using the top-level ones, keep using it. It is backward-compatible. If you want to specify it per release, because, for example, your releases are deployed across clusters(in case you differentiate `kubeContext` fields), try the new fields added to the release spec.

Resolves #1864

Patch:
diff --git a/README.md b/README.md
index 5caf975..69d5157 100644
--- a/README.md
+++ b/README.md
@@ -872,7 +872,10 @@ environments:
       - git::https://git.company.org/helmfiles/global/gcp.yaml?ref=master
       - git::https://git.company.org/helmfiles/global/europe-west.yaml?ref=master
       - git::https://ci:{{ env "CI_JOB_TOKEN" }}@gitlab.com/org/repository-name.git@/config.dev.yaml?ref={{ env "APP_COMMIT_SHA" }}  # Private Gitlab Repo
-
+  staging:
+     values:
+      -  git::https://{{ env "GITHUB_PAT" }}@github.com/[$GITHUB_ORGorGITHUB_USER]/repository-name.git@/values.dev.yaml?ref=main #Github Private repo
+      -  http://$HOSTNAME/artifactory/example-repo-local/test.tgz@values.yaml #Artifactory url
 ---
 
 releases:
diff --git a/pkg/app/two_pass_renderer.go b/pkg/app/two_pass_renderer.go
index a871547..561c2ca 100644
--- a/pkg/app/two_pass_renderer.go
+++ b/pkg/app/two_pass_renderer.go
@@ -21,11 +21,7 @@ func prependLineNumbers(text string) string {
 }
 
 func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
-	tmplData := state.EnvironmentTemplateData{
-		Environment: *firstPassEnv,
-		Namespace:   r.namespace,
-		Values:      map[string]interface{}{},
-	}
+	tmplData := state.NewEnvironmentTemplateData(*firstPassEnv, r.namespace, map[string]interface{}{})
 	firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
 
 	// parse as much as we can, tolerate errors, this is a preparse
@@ -127,11 +123,7 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
 		r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
 	}
 
-	tmplData := state.EnvironmentTemplateData{
-		Environment: *finalEnv,
-		Namespace:   r.namespace,
-		Values:      vals,
-	}
+	tmplData := state.NewEnvironmentTemplateData(*finalEnv, r.namespace, vals)
 	secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
 	yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
 	if err != nil {
diff --git a/pkg/state/envvals_loader.go b/pkg/state/envvals_loader.go
index cd73ed3..c1ea0ec 100644
--- a/pkg/state/envvals_loader.go
+++ b/pkg/state/envvals_loader.go
@@ -61,7 +61,8 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
 				} else {
 					env = *ctxEnv
 				}
-				tmplData := EnvironmentTemplateData{env, "", map[string]interface{}{}}
+
+				tmplData := NewEnvironmentTemplateData(env, "", map[string]interface{}{})
 				r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
 				bytes, err := r.RenderToBytes(f)
 				if err != nil {
diff --git a/pkg/state/state.go b/pkg/state/state.go
index 4aa1817..b952521 100644
--- a/pkg/state/state.go
+++ b/pkg/state/state.go
@@ -60,8 +60,12 @@ type ReleaseSetSpec struct {
 	CommonLabels        map[string]string `yaml:"commonLabels,omitempty"`
 	Releases            []ReleaseSpec     `yaml:"releases,omitempty"`
 	Selectors           []string          `yaml:"-"`
-	ApiVersions         []string          `yaml:"apiVersions,omitempty"`
-	KubeVersion         string            `yaml:"kubeVersion,omitempty"`
+
+	// Capabilities.APIVersions
+	ApiVersions []string `yaml:"apiVersions,omitempty"`
+
+	// Capabilities.KubeVersion
+	KubeVersion string `yaml:"kubeVersion,omitempty"`
 
 	// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
 	Hooks []event.Hook `yaml:"hooks,omitempty"`
@@ -249,6 +253,12 @@ type ReleaseSpec struct {
 	ValuesTemplate    []interface{} `yaml:"valuesTemplate,omitempty"`
 	SetValuesTemplate []SetValue    `yaml:"setTemplate,omitempty"`
 
+	// Capabilities.APIVersions
+	ApiVersions []string `yaml:"apiVersions,omitempty"`
+
+	// Capabilities.KubeVersion
+	KubeVersion string `yaml:"kubeVersion,omitempty"`
+
 	// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
 	EnvValues []SetValue `yaml:"env,omitempty"`
 
@@ -1121,6 +1131,9 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 
 					chartifyOpts.Validate = opts.Validate
 
+					chartifyOpts.KubeVersion = release.KubeVersion
+					chartifyOpts.ApiVersions = release.ApiVersions
+
 					out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))
 					if err != nil {
 						results <- &chartPrepareResult{err: err}
@@ -2471,10 +2484,7 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
 		return nil, nil, err
 	}
 
-	flags = st.appendApiVersionsFlags(flags)
-	if st.KubeVersion != "" {
-		flags = append(flags, "--kube-version", st.KubeVersion)
-	}
+	flags = st.appendApiVersionsFlags(flags, release)
 
 	common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
 	if err != nil {
@@ -2536,10 +2546,15 @@ func (st *HelmState) chartVersionFlags(release *ReleaseSpec) []string {
 	return flags
 }
 
-func (st *HelmState) appendApiVersionsFlags(flags []string) []string {
-	for _, a := range st.ApiVersions {
+func (st *HelmState) appendApiVersionsFlags(flags []string, r *ReleaseSpec) []string {
+	for _, a := range r.ApiVersions {
 		flags = append(flags, "--api-versions", a)
 	}
+
+	if r.KubeVersion != "" {
+		flags = append(flags, "--kube-version", st.KubeVersion)
+	}
+
 	return flags
 }
 
diff --git a/pkg/state/state_exec_tmpl.go b/pkg/state/state_exec_tmpl.go
index 1635208..9e3ab61 100644
--- a/pkg/state/state_exec_tmpl.go
+++ b/pkg/state/state_exec_tmpl.go
@@ -100,6 +100,13 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
 		for k, v := range st.CommonLabels {
 			release.Labels[k] = v
 		}
+		if len(release.ApiVersions) == 0 {
+			release.ApiVersions = st.ApiVersions
+		}
+		if release.KubeVersion == "" {
+			release.KubeVersion = st.KubeVersion
+		}
+
 		successFlag := false
 		for it, prev := 0, &release; it < 6; it++ {
 			tmplData := st.createReleaseTemplateData(prev, vals)
diff --git a/pkg/state/types.go b/pkg/state/types.go
index 5105378..6ae91c3 100644
--- a/pkg/state/types.go
+++ b/pkg/state/types.go
@@ -16,7 +16,14 @@ type EnvironmentTemplateData struct {
 	// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
 	Namespace string
 	// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
-	Values map[string]interface{}
+	Values      map[string]interface{}
+	StateValues *map[string]interface{}
+}
+
+func NewEnvironmentTemplateData(environment environment.Environment, namespace string, values map[string]interface{}) *EnvironmentTemplateData {
+	d := EnvironmentTemplateData{environment, namespace, values, nil}
+	d.StateValues = &d.Values
+	return &d
 }
 
 // releaseTemplateData provides variables accessible while executing golang text/template expressions in release templates
diff --git a/pkg/tmpl/context_funcs.go b/pkg/tmpl/context_funcs.go
index 57b7cb6..574be4d 100644
--- a/pkg/tmpl/context_funcs.go
+++ b/pkg/tmpl/context_funcs.go
@@ -1,6 +1,7 @@
 package tmpl
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -20,6 +21,7 @@ type Values = map[string]interface{}
 func (c *Context) createFuncMap() template.FuncMap {
 	funcMap := template.FuncMap{
 		"exec":             c.Exec,
+		"isFile":           c.IsFile,
 		"readFile":         c.ReadFile,
 		"readDir":          c.ReadDir,
 		"toYaml":           ToYaml,
@@ -117,6 +119,24 @@ func (c *Context) Exec(command string, args []interface{}, inputs ...string) (st
 	return string(bytes), nil
 }
 
+func (c *Context) IsFile(filename string) (bool, error) {
+	var path string
+	if filepath.IsAbs(filename) {
+		path = filename
+	} else {
+		path = filepath.Join(c.basePath, filename)
+	}
+
+	stat, err := os.Stat(path)
+	if err == nil {
+		return !stat.IsDir(), nil
+	}
+	if errors.Is(err, os.ErrNotExist) {
+		return false, nil
+	}
+	return false, err
+}
+
 func (c *Context) ReadFile(filename string) (string, error) {
 	var path string
 	if filepath.IsAbs(filename) {


Test patch:
diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go
index ffb609b..f604b40 100644
--- a/pkg/state/temp_test.go
+++ b/pkg/state/temp_test.go
@@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
 	run(testcase{
 		subject: "baseline",
 		release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
-		want:    "foo-values-779795898b",
+		want:    "foo-values-6d97d4c7ff",
 	})
 
 	run(testcase{
 		subject: "different bytes content",
 		release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
 		data:    []byte(`{"k":"v"}`),
-		want:    "foo-values-6b7ffbccc",
+		want:    "foo-values-74fcbfbd68",
 	})
 
 	run(testcase{
 		subject: "different map content",
 		release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
 		data:    map[string]interface{}{"k": "v"},
-		want:    "foo-values-6d57bbfccf",
+		want:    "foo-values-7b954b6b6b",
 	})
 
 	run(testcase{
 		subject: "different chart",
 		release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
-		want:    "foo-values-5c45b58947",
+		want:    "foo-values-5c54676568",
 	})
 
 	run(testcase{
 		subject: "different name",
 		release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
-		want:    "bar-values-99d5fdbcc",
+		want:    "bar-values-5fff459546",
 	})
 
 	run(testcase{
 		subject: "specific ns",
 		release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
-		want:    "myns-foo-values-544cb97d7f",
+		want:    "myns-foo-values-cfd9959dd",
 	})
 
 	for id, n := range ids {