Repository: helm/helm
Pull request #9040 (2020-11-20 09:59:48)

feat(helm): Support setting --kube-version
Closes https://github.com/helm/helm/issues/7326 although it's already closed due to staleness

**What this PR does / why we need it**:

Helm 2 supported specifying `--kube-version` for `helm template` to set the kubernetes version used. This ports it to helm 3. `--api-versions` is a similar flag that was already ported.

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


afirth: @bacongobbler I believe the review comments were addressed, and this is (still) stopping me from moving to helm3 in workflows which use template.

Using templates allows me to patch upstream outputs without adding infinite values that nobody else needs. It also allows me to `git diff` the changes that will be applied to the cluster in PRs for review
afirth: workaround with kustomize to patch the apiVersion: 

```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
[...]
patches:
- patch: |-
    - op: replace
      path: "/apiVersion"
      value: rbac.authorization.k8s.io/v1beta1
  target:
    group: rbac.authorization.k8s.io
    version: v1
    labelSelector: "app.kubernetes.io/managed-by=Helm"
```
Shini31: I'm starting to study the migration to HELM 3 and the delete of `--kube-version` parameter is a problem for me with the use of the template command

Patch:
diff --git a/cmd/helm/template.go b/cmd/helm/template.go
index 1110771f0..e3c1d421f 100644
--- a/cmd/helm/template.go
+++ b/cmd/helm/template.go
@@ -52,6 +52,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	var skipTests bool
 	client := action.NewInstall(cfg)
 	valueOpts := &values.Options{}
+	var kubeVersion string
 	var extraAPIs []string
 	var showFiles []string
 
@@ -64,6 +65,14 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			return compInstall(args, toComplete, client)
 		},
 		RunE: func(_ *cobra.Command, args []string) error {
+			if kubeVersion != "" {
+				parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
+				if err != nil {
+					return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
+				}
+				client.KubeVersion = parsedKubeVersion
+			}
+
 			client.DryRun = true
 			client.ReleaseName = "RELEASE-NAME"
 			client.Replace = true // Skip the name check
@@ -171,6 +180,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
 	f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
 	f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
+	f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
 	f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
 	f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
 	bindPostRenderFlag(cmd, &client.PostRenderer)
diff --git a/pkg/action/install.go b/pkg/action/install.go
index 4de0b64e6..af99717d1 100644
--- a/pkg/action/install.go
+++ b/pkg/action/install.go
@@ -92,8 +92,10 @@ type Install struct {
 	SubNotes                 bool
 	DisableOpenAPIValidation bool
 	IncludeCRDs              bool
+	// KubeVersion allows specifying a custom kubernetes version to use and
 	// APIVersions allows a manual set of supported API Versions to be passed
 	// (for things like templating). These are ignored if ClientOnly is false
+	KubeVersion *chartutil.KubeVersion
 	APIVersions chartutil.VersionSet
 	// Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false
 	IsUpgrade bool
@@ -196,7 +198,10 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
 	if i.ClientOnly {
 		// Add mock objects in here so it doesn't use Kube API server
 		// NOTE(bacongobbler): used for `helm template`
-		i.cfg.Capabilities = chartutil.DefaultCapabilities
+		i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
+		if i.KubeVersion != nil {
+			i.cfg.Capabilities.KubeVersion = *i.KubeVersion
+		}
 		i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
 		i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
 
diff --git a/pkg/chartutil/capabilities.go b/pkg/chartutil/capabilities.go
index c002e33f2..d24228056 100644
--- a/pkg/chartutil/capabilities.go
+++ b/pkg/chartutil/capabilities.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"strconv"
 
+	"github.com/Masterminds/semver/v3"
 	"k8s.io/client-go/kubernetes/scheme"
 
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -58,6 +59,14 @@ type Capabilities struct {
 	HelmVersion helmversion.BuildInfo
 }
 
+func (capabilities *Capabilities) Copy() *Capabilities {
+	return &Capabilities{
+		KubeVersion: capabilities.KubeVersion,
+		APIVersions: capabilities.APIVersions,
+		HelmVersion: capabilities.HelmVersion,
+	}
+}
+
 // KubeVersion is the Kubernetes version.
 type KubeVersion struct {
 	Version string // Kubernetes version
@@ -73,6 +82,19 @@ func (kv *KubeVersion) String() string { return kv.Version }
 // Deprecated: use KubeVersion.Version.
 func (kv *KubeVersion) GitVersion() string { return kv.Version }
 
+// ParseKubeVersion parses kubernetes version from string
+func ParseKubeVersion(version string) (*KubeVersion, error) {
+	sv, err := semver.NewVersion(version)
+	if err != nil {
+		return nil, err
+	}
+	return &KubeVersion{
+		Version: "v" + sv.String(),
+		Major:   strconv.FormatUint(sv.Major(), 10),
+		Minor:   strconv.FormatUint(sv.Minor(), 10),
+	}, nil
+}
+
 // VersionSet is a set of Kubernetes API versions.
 type VersionSet []string
 


Test patch:
diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go
index 9e6a0c434..30ee2722b 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -74,6 +74,11 @@ func TestTemplateCmd(t *testing.T) {
 			cmd:    fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-lib-archive-dep"),
 			golden: "output/template-chart-with-template-lib-archive-dep.txt",
 		},
+		{
+			name:   "check kube version",
+			cmd:    fmt.Sprintf("template --kube-version 1.16.0 '%s'", chartPath),
+			golden: "output/template-with-kube-version.txt",
+		},
 		{
 			name:   "check kube api versions",
 			cmd:    fmt.Sprintf("template --api-versions helm.k8s.io/test '%s'", chartPath),
diff --git a/cmd/helm/testdata/output/template-show-only-multiple.txt b/cmd/helm/testdata/output/template-show-only-multiple.txt
index 20b6bebed..81a1d8e3c 100644
--- a/cmd/helm/testdata/output/template-show-only-multiple.txt
+++ b/cmd/helm/testdata/output/template-show-only-multiple.txt
@@ -10,7 +10,6 @@ metadata:
     kube-version/major: "1"
     kube-version/minor: "20"
     kube-version/version: "v1.20.0"
-    kube-api-version/test: v1
 spec:
   type: ClusterIP
   ports:
diff --git a/cmd/helm/testdata/output/template-show-only-one.txt b/cmd/helm/testdata/output/template-show-only-one.txt
index f3aedb55d..a354bc0cc 100644
--- a/cmd/helm/testdata/output/template-show-only-one.txt
+++ b/cmd/helm/testdata/output/template-show-only-one.txt
@@ -10,7 +10,6 @@ metadata:
     kube-version/major: "1"
     kube-version/minor: "20"
     kube-version/version: "v1.20.0"
-    kube-api-version/test: v1
 spec:
   type: ClusterIP
   ports:
diff --git a/cmd/helm/testdata/output/template-skip-tests.txt b/cmd/helm/testdata/output/template-skip-tests.txt
index 6e657e50b..8e8181474 100644
--- a/cmd/helm/testdata/output/template-skip-tests.txt
+++ b/cmd/helm/testdata/output/template-skip-tests.txt
@@ -74,7 +74,6 @@ metadata:
     kube-version/major: "1"
     kube-version/minor: "20"
     kube-version/version: "v1.20.0"
-    kube-api-version/test: v1
 spec:
   type: ClusterIP
   ports:
diff --git a/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt
index fe8e24520..758e2e39f 100644
--- a/cmd/helm/testdata/output/template-with-crds.txt
+++ b/cmd/helm/testdata/output/template-with-crds.txt
@@ -91,7 +91,6 @@ metadata:
     kube-version/major: "1"
     kube-version/minor: "20"
     kube-version/version: "v1.20.0"
-    kube-api-version/test: v1
 spec:
   type: ClusterIP
   ports:
diff --git a/cmd/helm/testdata/output/template-with-kube-version.txt b/cmd/helm/testdata/output/template-with-kube-version.txt
new file mode 100644
index 000000000..836905268
--- /dev/null
+++ b/cmd/helm/testdata/output/template-with-kube-version.txt
@@ -0,0 +1,114 @@
+---
+# Source: subchart/templates/subdir/serviceaccount.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: subchart-sa
+---
+# Source: subchart/templates/subdir/role.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: subchart-role
+rules:
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["get","list","watch"]
+---
+# Source: subchart/templates/subdir/rolebinding.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: subchart-binding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: subchart-role
+subjects:
+- kind: ServiceAccount
+  name: subchart-sa
+  namespace: default
+---
+# Source: subchart/charts/subcharta/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: subcharta
+  labels:
+    helm.sh/chart: "subcharta-0.1.0"
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+    targetPort: 80
+    protocol: TCP
+    name: apache
+  selector:
+    app.kubernetes.io/name: subcharta
+---
+# Source: subchart/charts/subchartb/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: subchartb
+  labels:
+    helm.sh/chart: "subchartb-0.1.0"
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+    targetPort: 80
+    protocol: TCP
+    name: nginx
+  selector:
+    app.kubernetes.io/name: subchartb
+---
+# Source: subchart/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: subchart
+  labels:
+    helm.sh/chart: "subchart-0.1.0"
+    app.kubernetes.io/instance: "RELEASE-NAME"
+    kube-version/major: "1"
+    kube-version/minor: "16"
+    kube-version/version: "v1.16.0"
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+    targetPort: 80
+    protocol: TCP
+    name: nginx
+  selector:
+    app.kubernetes.io/name: subchart
+---
+# Source: subchart/templates/tests/test-config.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "RELEASE-NAME-testconfig"
+  annotations:
+    "helm.sh/hook": test
+data:
+  message: Hello World
+---
+# Source: subchart/templates/tests/test-nothing.yaml
+apiVersion: v1
+kind: Pod
+metadata:
+  name: "RELEASE-NAME-test"
+  annotations:
+    "helm.sh/hook": test
+spec:
+  containers:
+    - name: test
+      image: "alpine:latest"
+      envFrom:
+        - configMapRef:
+            name: "RELEASE-NAME-testconfig"
+      command:
+        - echo
+        - "$message"
+  restartPolicy: Never
diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go
index 7134abfc5..a68d16b45 100644
--- a/pkg/chartutil/capabilities_test.go
+++ b/pkg/chartutil/capabilities_test.go
@@ -66,3 +66,19 @@ func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
 		t.Errorf("Expected default HelmVersion to be v3.5, got %q", hv.Version)
 	}
 }
+
+func TestParseKubeVersion(t *testing.T) {
+	kv, err := ParseKubeVersion("v1.16.0")
+	if err != nil {
+		t.Errorf("Expected v1.16.0 to parse successfully")
+	}
+	if kv.Version != "v1.16.0" {
+		t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String())
+	}
+	if kv.Major != "1" {
+		t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
+	}
+	if kv.Minor != "16" {
+		t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor)
+	}
+}