Repository: helm/helm (go)

Issue:
Add an option to helm template not render resources in tests directory 
This issue was already brought up in #6483 and proposed to be incorporated as a feature in helm 3.

#6444 implements an option to not render hooks, but that's not the same.

For example https://github.com/helm/charts/tree/master/stable/jenkins/templates/tests contains two manifests:

```
apiVersion: v1
kind: Pod
metadata:
  name: "{{ .Release.Name }}-ui-test-{{ randAlphaNum 5 | lower }}"
  namespace: {{ template "jenkins.namespace" . }}
  annotations:
    "helm.sh/hook": test-success
```

and

```
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ template "jenkins.fullname" . }}-tests
  namespace: {{ template "jenkins.namespace" . }}:
```

As you can see only the pod has the hook annotation. I've seen the same in other charts some are also rendering `Roles` or `RoleBindings` which are needed for testing e.g. https://github.com/helm/charts/tree/master/stable/grafana/templates/tests

Pull request description:
Add option to skip rendering helm tests
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/master/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

This helps to avoid rendering manifests which are only used for test execution.

    
When helm template is called with `--skip-tests` no manifests with helm test hook annotations are rendered.

Closes #8691

NOTE: I updated description to reflect revised implementation. It now only ignores resources with helm test annotation and not everything within tests directory.

**Special notes for your reviewer**:

- `skipTests` defaults to `false` to keep backwards compatibility
- I used the existing `testcharts/subchart` for testing and fixed Kubernetes warnings along the way when I was trying to install the chart locally. It's now valid and can be installed. 
  By using an existing test I did not have to write add and additional test case which check rendering if `skip-tests` is not used. f you prefer to create a different chart for testing just let me know and I will adapt.
- How should I update the documentation? Is there a make target or something else which can be used to generate cobra docs?

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility


Patch:
diff --git a/cmd/helm/template.go b/cmd/helm/template.go
index 6123d29d4..d760fb87b 100644
--- a/cmd/helm/template.go
+++ b/cmd/helm/template.go
@@ -27,6 +27,8 @@ import (
 	"sort"
 	"strings"
 
+	"helm.sh/helm/v3/pkg/release"
+
 	"github.com/spf13/cobra"
 
 	"helm.sh/helm/v3/cmd/helm/require"
@@ -47,6 +49,7 @@ faked locally. Additionally, none of the server-side testing of chart validity
 func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	var validate bool
 	var includeCrds bool
+	var skipTests bool
 	client := action.NewInstall(cfg)
 	valueOpts := &values.Options{}
 	var extraAPIs []string
@@ -84,6 +87,9 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 				if !client.DisableHooks {
 					fileWritten := make(map[string]bool)
 					for _, m := range rel.Hooks {
+						if skipTests && isTestHook(m) {
+							continue
+						}
 						if client.OutputDir == "" {
 							fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
 						} else {
@@ -163,6 +169,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
 	f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
 	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.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.")
@@ -171,6 +178,15 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	return cmd
 }
 
+func isTestHook(h *release.Hook) bool {
+	for _, e := range h.Events {
+		if e == release.HookTest {
+			return true
+		}
+	}
+	return false
+}
+
 // The following functions (writeToFile, createOrOpenFile, and ensureDirectoryForFile)
 // are coppied from the actions package. This is part of a change to correct a
 // bug introduced by #8156. As part of the todo to refactor renderResources


Test patch:
diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go
index 6f7ca939d..9e6a0c434 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -121,6 +121,11 @@ func TestTemplateCmd(t *testing.T) {
 			wantError: true,
 			golden:    "output/template-with-invalid-yaml-debug.txt",
 		},
+		{
+			name:   "template skip-tests",
+			cmd:    fmt.Sprintf(`template '%s' --skip-tests`, chartPath),
+			golden: "output/template-skip-tests.txt",
+		},
 	}
 	runTestCmd(t, tests)
 }
diff --git a/cmd/helm/testdata/output/template-name-template.txt b/cmd/helm/testdata/output/template-name-template.txt
index 84a9e565c..5e4478937 100644
--- a/cmd/helm/testdata/output/template-name-template.txt
+++ b/cmd/helm/testdata/output/template-name-template.txt
@@ -11,7 +11,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -82,3 +83,32 @@ spec:
     name: nginx
   selector:
     app.kubernetes.io/name: subchart
+---
+# Source: subchart/templates/tests/test-config.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "foobar-YWJj-baz-testconfig"
+  annotations:
+    "helm.sh/hook": test
+data:
+  message: Hello World
+---
+# Source: subchart/templates/tests/test-nothing.yaml
+apiVersion: v1
+kind: Pod
+metadata:
+  name: "foobar-YWJj-baz-test"
+  annotations:
+    "helm.sh/hook": test
+spec:
+  containers:
+    - name: test
+      image: "alpine:latest"
+      envFrom:
+        - configMapRef:
+            name: "foobar-YWJj-baz-testconfig"
+      command:
+        - echo
+        - "$message"
+  restartPolicy: Never
diff --git a/cmd/helm/testdata/output/template-set.txt b/cmd/helm/testdata/output/template-set.txt
index 1cb97723e..0db9a9b74 100644
--- a/cmd/helm/testdata/output/template-set.txt
+++ b/cmd/helm/testdata/output/template-set.txt
@@ -11,7 +11,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -82,3 +83,32 @@ spec:
     name: apache
   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/cmd/helm/testdata/output/template-show-only-glob.txt b/cmd/helm/testdata/output/template-show-only-glob.txt
index cc651f596..b2d2b1c2d 100644
--- a/cmd/helm/testdata/output/template-show-only-glob.txt
+++ b/cmd/helm/testdata/output/template-show-only-glob.txt
@@ -5,7 +5,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
diff --git a/cmd/helm/testdata/output/template-skip-tests.txt b/cmd/helm/testdata/output/template-skip-tests.txt
new file mode 100644
index 000000000..16808ede3
--- /dev/null
+++ b/cmd/helm/testdata/output/template-skip-tests.txt
@@ -0,0 +1,86 @@
+---
+# 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: "18"
+    kube-version/version: "v1.18.0"
+    kube-api-version/test: v1
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+    targetPort: 80
+    protocol: TCP
+    name: nginx
+  selector:
+    app.kubernetes.io/name: subchart
diff --git a/cmd/helm/testdata/output/template-values-files.txt b/cmd/helm/testdata/output/template-values-files.txt
index 1cb97723e..0db9a9b74 100644
--- a/cmd/helm/testdata/output/template-values-files.txt
+++ b/cmd/helm/testdata/output/template-values-files.txt
@@ -11,7 +11,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -82,3 +83,32 @@ spec:
     name: apache
   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/cmd/helm/testdata/output/template-with-api-version.txt b/cmd/helm/testdata/output/template-with-api-version.txt
index ea4b5c96b..3e488f0d2 100644
--- a/cmd/helm/testdata/output/template-with-api-version.txt
+++ b/cmd/helm/testdata/output/template-with-api-version.txt
@@ -11,7 +11,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -83,3 +84,32 @@ spec:
     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/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt
index fa2a79bac..4bd5d2e29 100644
--- a/cmd/helm/testdata/output/template-with-crds.txt
+++ b/cmd/helm/testdata/output/template-with-crds.txt
@@ -3,13 +3,14 @@
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  name: testCRDs
+  name: testcrds.testcrdgroups.example.com
 spec:
-  group: testCRDGroups
+  group: testcrdgroups.example.com
+  version: v1alpha1
   names:
     kind: TestCRD
     listKind: TestCRDList
-    plural: TestCRDs
+    plural: testcrds
     shortNames:
       - tc
     singular: authconfig
@@ -27,7 +28,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -99,3 +101,32 @@ spec:
     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/cmd/helm/testdata/output/template.txt b/cmd/helm/testdata/output/template.txt
index 9195f98b7..a81934b20 100644
--- a/cmd/helm/testdata/output/template.txt
+++ b/cmd/helm/testdata/output/template.txt
@@ -11,7 +11,8 @@ kind: Role
 metadata:
   name: subchart-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
 ---
 # Source: subchart/templates/subdir/rolebinding.yaml
@@ -82,3 +83,32 @@ spec:
     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/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml b/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml
index fca77fd4b..ad770b632 100644
--- a/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml
+++ b/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml
@@ -1,13 +1,14 @@
 apiVersion: apiextensions.k8s.io/v1beta1
 kind: CustomResourceDefinition
 metadata:
-  name: testCRDs
+  name: testcrds.testcrdgroups.example.com
 spec:
-  group: testCRDGroups
+  group: testcrdgroups.example.com
+  version: v1alpha1
   names:
     kind: TestCRD
     listKind: TestCRDList
-    plural: TestCRDs
+    plural: testcrds
     shortNames:
       - tc
     singular: authconfig
diff --git a/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml b/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml
index 91b954e5f..31cff9200 100644
--- a/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml
+++ b/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml
@@ -3,5 +3,6 @@ kind: Role
 metadata:
   name: {{ .Chart.Name }}-role
 rules:
-- resources: ["*"]
+- apiGroups: [""]
+  resources: ["pods"]
   verbs: ["get","list","watch"]
diff --git a/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml
new file mode 100644
index 000000000..0aa3eea29
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "{{ .Release.Name }}-testconfig"
+  annotations:
+    "helm.sh/hook": test
+data:
+  message: Hello World
diff --git a/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml
new file mode 100644
index 000000000..0fe6dbbf3
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml
@@ -0,0 +1,17 @@
+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