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/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/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 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) + } +}