Repository: helm/helm
Pull request #6490 (2019-09-24 17:56:07)

fix(chartutil): port over enhancements to `helm create` from Helm 2
Signed-off-by: Matthew Fisher <matt.fisher@microsoft.com>


bacongobbler: Thank you for catching all those, @hickeyma. All comments addressed!

Patch:
diff --git a/cmd/helm/create.go b/cmd/helm/create.go
index 65e142097..e9f71d0a7 100644
--- a/cmd/helm/create.go
+++ b/cmd/helm/create.go
@@ -36,12 +36,13 @@ directories used in a chart.
 For example, 'helm create foo' will create a directory structure that looks
 something like this:
 
-	foo/
-	├── .helmignore   # Contains patterns to ignore when packaging Helm charts.
-	├── Chart.yaml    # Information about your chart
-	├── values.yaml   # The default values for your templates
-	├── charts/       # Charts that this chart depends on
-	└── templates/    # The template files
+    foo/
+    ├── .helmignore   # Contains patterns to ignore when packaging Helm charts.
+    ├── Chart.yaml    # Information about your chart
+    ├── values.yaml   # The default values for your templates
+    ├── charts/       # Charts that this chart depends on
+    └── templates/    # The template files
+        └── tests/    # The test files
 
 'helm create' takes a path for an argument. If directories in the given path
 do not exist, Helm will attempt to create them as it goes. If the given
diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go
index 023e3bb9a..329c0000e 100644
--- a/pkg/chartutil/create.go
+++ b/pkg/chartutil/create.go
@@ -39,6 +39,8 @@ const (
 	TemplatesDir = "templates"
 	// ChartsDir is the relative directory name for charts dependencies.
 	ChartsDir = "charts"
+	// TemplatesTestsDir is the relative directory name for tests.
+	TemplatesTestsDir = TemplatesDir + sep + "tests"
 	// IgnorefileName is the name of the Helm ignore file.
 	IgnorefileName = ".helmignore"
 	// IngressFileName is the name of the example ingress file.
@@ -47,10 +49,14 @@ const (
 	DeploymentName = TemplatesDir + sep + "deployment.yaml"
 	// ServiceName is the name of the example service file.
 	ServiceName = TemplatesDir + sep + "service.yaml"
+	// ServiceAccountName is the name of the example serviceaccount file.
+	ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml"
 	// NotesName is the name of the example NOTES.txt file.
 	NotesName = TemplatesDir + sep + "NOTES.txt"
-	// HelpersName is the name of the example NOTES.txt file.
+	// HelpersName is the name of the example helpers file.
 	HelpersName = TemplatesDir + sep + "_helpers.tpl"
+	// TestConnectionName is the name of the example test file.
+	TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml"
 )
 
 const sep = string(filepath.Separator)
@@ -88,9 +94,28 @@ image:
   repository: nginx
   pullPolicy: IfNotPresent
 
+imagePullSecrets: []
 nameOverride: ""
 fullnameOverride: ""
 
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name:
+
+podSecurityContext: {}
+  # fsGroup: 2000
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
 service:
   type: ClusterIP
   port: 80
@@ -103,11 +128,10 @@ ingress:
   hosts:
     - host: chart-example.local
       paths: []
-
   tls: []
-    # - secretName: chart-example-tls
-    #   hosts:
-    #     - chart-example.local
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
 
 resources: {}
   # We usually recommend not to specify default resources and to leave this as a conscious
@@ -149,16 +173,13 @@ const defaultIgnore = `# Patterns to ignore when building packages.
 .project
 .idea/
 *.tmproj
+.vscode/
 `
 
 const defaultIngress = `{{- if .Values.ingress.enabled -}}
 {{- $fullName := include "<CHARTNAME>.fullname" . -}}
 {{- $svcPort := .Values.service.port -}}
-{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
 apiVersion: networking.k8s.io/v1
-{{- else -}}
-apiVersion: extensions/v1beta1
-{{- end }}
 kind: Ingress
 metadata:
   name: {{ $fullName }}
@@ -210,8 +231,17 @@ spec:
       labels:
         {{- include "<CHARTNAME>.selectorLabels" . | nindent 8 }}
     spec:
+    {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+      serviceAccountName: {{ include "<CHARTNAME>.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
       containers:
         - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
           image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
           ports:
@@ -228,10 +258,10 @@ spec:
               port: http
           resources:
             {{- toYaml .Values.resources | nindent 12 }}
-    {{- with .Values.nodeSelector }}
+      {{- with .Values.nodeSelector }}
       nodeSelector:
         {{- toYaml . | nindent 8 }}
-    {{- end }}
+      {{- end }}
     {{- with .Values.affinity }}
       affinity:
         {{- toYaml . | nindent 8 }}
@@ -259,6 +289,16 @@ spec:
     {{- include "<CHARTNAME>.selectorLabels" . | nindent 4 }}
 `
 
+const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "<CHARTNAME>.serviceAccountName" . }}
+  labels:
+{{ include "<CHARTNAME>.labels" . | nindent 4 }}
+{{- end -}}
+`
+
 const defaultNotes = `1. Get the application URL by running these commands:
 {{- if .Values.ingress.enabled }}
 {{- range $host := .Values.ingress.hosts }}
@@ -267,16 +307,16 @@ const defaultNotes = `1. Get the application URL by running these commands:
   {{- end }}
 {{- end }}
 {{- else if contains "NodePort" .Values.service.type }}
-  export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "<CHARTNAME>.fullname" . }})
-  export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}")
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "<CHARTNAME>.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
   echo http://$NODE_IP:$NODE_PORT
 {{- else if contains "LoadBalancer" .Values.service.type }}
      NOTE: It may take a few minutes for the LoadBalancer IP to be available.
-           You can watch the status of by running 'kubectl get svc -w {{ include "<CHARTNAME>.fullname" . }}'
-  export SERVICE_IP=$(kubectl get svc {{ include "<CHARTNAME>.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "<CHARTNAME>.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "<CHARTNAME>.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
   echo http://$SERVICE_IP:{{ .Values.service.port }}
 {{- else if contains "ClusterIP" .Values.service.type }}
-  export POD_NAME=$(kubectl get pods -l "app.kubernetes.io/name={{ include "<CHARTNAME>.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "<CHARTNAME>.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
   echo "Visit http://127.0.0.1:8080 to use your application"
   kubectl port-forward $POD_NAME 8080:80
 {{- end }}
@@ -334,6 +374,34 @@ Selector labels
 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
 app.kubernetes.io/instance: {{ .Release.Name }}
 {{- end -}}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "<CHARTNAME>.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create -}}
+    {{ default (include "<CHARTNAME>.fullname" .) .Values.serviceAccount.name }}
+{{- else -}}
+    {{ default "default" .Values.serviceAccount.name }}
+{{- end -}}
+{{- end -}}
+`
+
+const defaultTestConnection = `apiVersion: v1
+kind: Pod
+metadata:
+  name: "{{ include "<CHARTNAME>.fullname" . }}-test-connection"
+  labels:
+{{ include "<CHARTNAME>.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": test-success
+spec:
+  containers:
+    - name: wget
+      image: busybox
+      command: ['wget']
+      args:  ['{{ include "<CHARTNAME>.fullname" . }}:{{ .Values.service.port }}']
+  restartPolicy: Never
 `
 
 // CreateFrom creates a new chart, but scaffolds it from the src chart.
@@ -431,6 +499,11 @@ func Create(name, dir string) (string, error) {
 			path:    filepath.Join(cdir, ServiceName),
 			content: transform(defaultService, name),
 		},
+		{
+			// serviceaccount.yaml
+			path:    filepath.Join(cdir, ServiceAccountName),
+			content: transform(defaultServiceAccount, name),
+		},
 		{
 			// NOTES.txt
 			path:    filepath.Join(cdir, NotesName),
@@ -441,6 +514,11 @@ func Create(name, dir string) (string, error) {
 			path:    filepath.Join(cdir, HelpersName),
 			content: transform(defaultHelpers, name),
 		},
+		{
+			// test-connection.yaml
+			path:    filepath.Join(cdir, TestConnectionName),
+			content: transform(defaultTestConnection, name),
+		},
 	}
 
 	for _, file := range files {


Test patch:
diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go
index d9b312262..0a9b7b9a5 100644
--- a/cmd/helm/create_test.go
+++ b/cmd/helm/create_test.go
@@ -106,8 +106,9 @@ func TestCreateStarterCmd(t *testing.T) {
 		t.Errorf("Wrong API version: %q", c.Metadata.APIVersion)
 	}
 
-	if l := len(c.Templates); l != 6 {
-		t.Errorf("Expected 5 templates, got %d", l)
+	expectedNumberOfTemplates := 8
+	if l := len(c.Templates); l != expectedNumberOfTemplates {
+		t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l)
 	}
 
 	found := false
@@ -173,8 +174,9 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) {
 		t.Errorf("Wrong API version: %q", c.Metadata.APIVersion)
 	}
 
-	if l := len(c.Templates); l != 6 {
-		t.Errorf("Expected 5 templates, got %d", l)
+	expectedNumberOfTemplates := 8
+	if l := len(c.Templates); l != expectedNumberOfTemplates {
+		t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l)
 	}
 
 	found := false
diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go
index 87205cab0..82fde586c 100644
--- a/pkg/chartutil/create_test.go
+++ b/pkg/chartutil/create_test.go
@@ -55,8 +55,11 @@ func TestCreate(t *testing.T) {
 		HelpersName,
 		IgnorefileName,
 		NotesName,
+		ServiceAccountName,
 		ServiceName,
 		TemplatesDir,
+		TemplatesTestsDir,
+		TestConnectionName,
 		ValuesfileName,
 	} {
 		if _, err := os.Stat(filepath.Join(dir, f)); err != nil {