github.com/koderover/helm@v2.17.0+incompatible/pkg/chartutil/create.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package chartutil 18 19 import ( 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path/filepath" 24 25 "k8s.io/helm/pkg/proto/hapi/chart" 26 ) 27 28 const ( 29 // ChartfileName is the default Chart file name. 30 ChartfileName = "Chart.yaml" 31 // ValuesfileName is the default values file name. 32 ValuesfileName = "values.yaml" 33 // TemplatesDir is the relative directory name for templates. 34 TemplatesDir = "templates" 35 // ChartsDir is the relative directory name for charts dependencies. 36 ChartsDir = "charts" 37 // IgnorefileName is the name of the Helm ignore file. 38 IgnorefileName = ".helmignore" 39 // IngressFileName is the name of the example ingress file. 40 IngressFileName = "ingress.yaml" 41 // DeploymentName is the name of the example deployment file. 42 DeploymentName = "deployment.yaml" 43 // ServiceName is the name of the example service file. 44 ServiceName = "service.yaml" 45 // ServiceAccountName is the name of the example serviceaccount file. 46 ServiceAccountName = "serviceaccount.yaml" 47 // NotesName is the name of the example NOTES.txt file. 48 NotesName = "NOTES.txt" 49 // HelpersName is the name of the example helpers file. 50 HelpersName = "_helpers.tpl" 51 // TemplatesTestsDir is the relative directory name for templates tests. 52 TemplatesTestsDir = "templates/tests" 53 // TestConnectionName is the name of the example connection test file. 54 TestConnectionName = "test-connection.yaml" 55 ) 56 57 const defaultValues = `# Default values for %s. 58 # This is a YAML-formatted file. 59 # Declare variables to be passed into your templates. 60 61 replicaCount: 1 62 63 image: 64 repository: nginx 65 tag: stable 66 pullPolicy: IfNotPresent 67 68 imagePullSecrets: [] 69 nameOverride: "" 70 fullnameOverride: "" 71 72 serviceAccount: 73 # Specifies whether a service account should be created 74 create: true 75 # The name of the service account to use. 76 # If not set and create is true, a name is generated using the fullname template 77 name: "" 78 79 podSecurityContext: {} 80 # fsGroup: 2000 81 82 securityContext: {} 83 # capabilities: 84 # drop: 85 # - ALL 86 # readOnlyRootFilesystem: true 87 # runAsNonRoot: true 88 # runAsUser: 1000 89 90 service: 91 type: ClusterIP 92 port: 80 93 94 ingress: 95 enabled: false 96 annotations: {} 97 # kubernetes.io/ingress.class: nginx 98 # kubernetes.io/tls-acme: "true" 99 hosts: 100 - host: chart-example.local 101 paths: [] 102 103 tls: [] 104 # - secretName: chart-example-tls 105 # hosts: 106 # - chart-example.local 107 108 resources: {} 109 # We usually recommend not to specify default resources and to leave this as a conscious 110 # choice for the user. This also increases chances charts run on environments with little 111 # resources, such as Minikube. If you do want to specify resources, uncomment the following 112 # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 113 # limits: 114 # cpu: 100m 115 # memory: 128Mi 116 # requests: 117 # cpu: 100m 118 # memory: 128Mi 119 120 nodeSelector: {} 121 122 tolerations: [] 123 124 affinity: {} 125 ` 126 127 const defaultIgnore = `# Patterns to ignore when building packages. 128 # This supports shell glob matching, relative path matching, and 129 # negation (prefixed with !). Only one pattern per line. 130 .DS_Store 131 # Common VCS dirs 132 .git/ 133 .gitignore 134 .bzr/ 135 .bzrignore 136 .hg/ 137 .hgignore 138 .svn/ 139 # Common backup files 140 *.swp 141 *.bak 142 *.tmp 143 *~ 144 # Various IDEs 145 .project 146 .idea/ 147 *.tmproj 148 .vscode/ 149 ` 150 151 const defaultIngress = `{{- if .Values.ingress.enabled -}} 152 {{- $fullName := include "<CHARTNAME>.fullname" . -}} 153 {{- $svcPort := .Values.service.port -}} 154 {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 155 apiVersion: networking.k8s.io/v1beta1 156 {{- else -}} 157 apiVersion: extensions/v1beta1 158 {{- end }} 159 kind: Ingress 160 metadata: 161 name: {{ $fullName }} 162 labels: 163 {{ include "<CHARTNAME>.labels" . | indent 4 }} 164 {{- with .Values.ingress.annotations }} 165 annotations: 166 {{- toYaml . | nindent 4 }} 167 {{- end }} 168 spec: 169 {{- if .Values.ingress.tls }} 170 tls: 171 {{- range .Values.ingress.tls }} 172 - hosts: 173 {{- range .hosts }} 174 - {{ . | quote }} 175 {{- end }} 176 secretName: {{ .secretName }} 177 {{- end }} 178 {{- end }} 179 rules: 180 {{- range .Values.ingress.hosts }} 181 - host: {{ .host | quote }} 182 http: 183 paths: 184 {{- range .paths }} 185 - path: {{ . }} 186 backend: 187 serviceName: {{ $fullName }} 188 servicePort: {{ $svcPort }} 189 {{- end }} 190 {{- end }} 191 {{- end }} 192 ` 193 194 const defaultDeployment = `apiVersion: apps/v1 195 kind: Deployment 196 metadata: 197 name: {{ include "<CHARTNAME>.fullname" . }} 198 labels: 199 {{ include "<CHARTNAME>.labels" . | indent 4 }} 200 spec: 201 replicas: {{ .Values.replicaCount }} 202 selector: 203 matchLabels: 204 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 205 app.kubernetes.io/instance: {{ .Release.Name }} 206 template: 207 metadata: 208 labels: 209 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 210 app.kubernetes.io/instance: {{ .Release.Name }} 211 spec: 212 {{- with .Values.imagePullSecrets }} 213 imagePullSecrets: 214 {{- toYaml . | nindent 8 }} 215 {{- end }} 216 serviceAccountName: {{ template "<CHARTNAME>.serviceAccountName" . }} 217 securityContext: 218 {{- toYaml .Values.podSecurityContext | nindent 8 }} 219 containers: 220 - name: {{ .Chart.Name }} 221 securityContext: 222 {{- toYaml .Values.securityContext | nindent 12 }} 223 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 224 imagePullPolicy: {{ .Values.image.pullPolicy }} 225 ports: 226 - name: http 227 containerPort: 80 228 protocol: TCP 229 livenessProbe: 230 httpGet: 231 path: / 232 port: http 233 readinessProbe: 234 httpGet: 235 path: / 236 port: http 237 resources: 238 {{- toYaml .Values.resources | nindent 12 }} 239 {{- with .Values.nodeSelector }} 240 nodeSelector: 241 {{- toYaml . | nindent 8 }} 242 {{- end }} 243 {{- with .Values.affinity }} 244 affinity: 245 {{- toYaml . | nindent 8 }} 246 {{- end }} 247 {{- with .Values.tolerations }} 248 tolerations: 249 {{- toYaml . | nindent 8 }} 250 {{- end }} 251 ` 252 253 const defaultService = `apiVersion: v1 254 kind: Service 255 metadata: 256 name: {{ include "<CHARTNAME>.fullname" . }} 257 labels: 258 {{ include "<CHARTNAME>.labels" . | indent 4 }} 259 spec: 260 type: {{ .Values.service.type }} 261 ports: 262 - port: {{ .Values.service.port }} 263 targetPort: http 264 protocol: TCP 265 name: http 266 selector: 267 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 268 app.kubernetes.io/instance: {{ .Release.Name }} 269 ` 270 const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} 271 apiVersion: v1 272 kind: ServiceAccount 273 metadata: 274 name: {{ template "<CHARTNAME>.serviceAccountName" . }} 275 labels: 276 {{ include "<CHARTNAME>.labels" . | indent 4 }} 277 {{- end -}} 278 ` 279 280 const defaultNotes = `1. Get the application URL by running these commands: 281 {{- if .Values.ingress.enabled }} 282 {{- range $host := .Values.ingress.hosts }} 283 {{- range .paths }} 284 http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 285 {{- end }} 286 {{- end }} 287 {{- else if contains "NodePort" .Values.service.type }} 288 export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "<CHARTNAME>.fullname" . }}) 289 export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 290 echo http://$NODE_IP:$NODE_PORT 291 {{- else if contains "LoadBalancer" .Values.service.type }} 292 NOTE: It may take a few minutes for the LoadBalancer IP to be available. 293 You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "<CHARTNAME>.fullname" . }}' 294 export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "<CHARTNAME>.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 295 echo http://$SERVICE_IP:{{ .Values.service.port }} 296 {{- else if contains "ClusterIP" .Values.service.type }} 297 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}") 298 echo "Visit http://127.0.0.1:8080 to use your application" 299 kubectl port-forward $POD_NAME 8080:80 300 {{- end }} 301 ` 302 303 const defaultHelpers = `{{/* vim: set filetype=mustache: */}} 304 {{/* 305 Expand the name of the chart. 306 */}} 307 {{- define "<CHARTNAME>.name" -}} 308 {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 309 {{- end -}} 310 311 {{/* 312 Create a default fully qualified app name. 313 We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 314 If release name contains chart name it will be used as a full name. 315 */}} 316 {{- define "<CHARTNAME>.fullname" -}} 317 {{- if .Values.fullnameOverride -}} 318 {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 319 {{- else -}} 320 {{- $name := default .Chart.Name .Values.nameOverride -}} 321 {{- if contains $name .Release.Name -}} 322 {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 323 {{- else -}} 324 {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 325 {{- end -}} 326 {{- end -}} 327 {{- end -}} 328 329 {{/* 330 Create chart name and version as used by the chart label. 331 */}} 332 {{- define "<CHARTNAME>.chart" -}} 333 {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 334 {{- end -}} 335 336 {{/* 337 Common labels 338 */}} 339 {{- define "<CHARTNAME>.labels" -}} 340 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 341 helm.sh/chart: {{ include "<CHARTNAME>.chart" . }} 342 app.kubernetes.io/instance: {{ .Release.Name }} 343 {{- if .Chart.AppVersion }} 344 app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 345 {{- end }} 346 app.kubernetes.io/managed-by: {{ .Release.Service }} 347 {{- end -}} 348 349 {{/* 350 Create the name of the service account to use 351 */}} 352 {{- define "<CHARTNAME>.serviceAccountName" -}} 353 {{- if .Values.serviceAccount.create -}} 354 {{ default (include "<CHARTNAME>.fullname" .) .Values.serviceAccount.name }} 355 {{- else -}} 356 {{ default "default" .Values.serviceAccount.name }} 357 {{- end -}} 358 {{- end -}} 359 ` 360 361 const defaultTestConnection = `apiVersion: v1 362 kind: Pod 363 metadata: 364 name: "{{ include "<CHARTNAME>.fullname" . }}-test-connection" 365 labels: 366 {{ include "<CHARTNAME>.labels" . | indent 4 }} 367 annotations: 368 "helm.sh/hook": test-success 369 spec: 370 containers: 371 - name: wget 372 image: busybox 373 command: ['wget'] 374 args: ['{{ include "<CHARTNAME>.fullname" . }}:{{ .Values.service.port }}'] 375 restartPolicy: Never 376 ` 377 378 // CreateFrom creates a new chart, but scaffolds it from the src chart. 379 func CreateFrom(chartfile *chart.Metadata, dest string, src string) error { 380 schart, err := Load(src) 381 if err != nil { 382 return fmt.Errorf("could not load %s: %s", src, err) 383 } 384 385 schart.Metadata = chartfile 386 387 var updatedTemplates []*chart.Template 388 389 for _, template := range schart.Templates { 390 newData := Transform(string(template.Data), "<CHARTNAME>", schart.Metadata.Name) 391 updatedTemplates = append(updatedTemplates, &chart.Template{Name: template.Name, Data: newData}) 392 } 393 394 schart.Templates = updatedTemplates 395 if schart.Values != nil { 396 schart.Values = &chart.Config{Raw: string(Transform(schart.Values.Raw, "<CHARTNAME>", schart.Metadata.Name))} 397 } 398 return SaveDir(schart, dest) 399 } 400 401 // Create creates a new chart in a directory. 402 // 403 // Inside of dir, this will create a directory based on the name of 404 // chartfile.Name. It will then write the Chart.yaml into this directory and 405 // create the (empty) appropriate directories. 406 // 407 // The returned string will point to the newly created directory. It will be 408 // an absolute path, even if the provided base directory was relative. 409 // 410 // If dir does not exist, this will return an error. 411 // If Chart.yaml or any directories cannot be created, this will return an 412 // error. In such a case, this will attempt to clean up by removing the 413 // new chart directory. 414 func Create(chartfile *chart.Metadata, dir string) (string, error) { 415 path, err := filepath.Abs(dir) 416 if err != nil { 417 return path, err 418 } 419 420 if fi, err := os.Stat(path); err != nil { 421 return path, err 422 } else if !fi.IsDir() { 423 return path, fmt.Errorf("no such directory %s", path) 424 } 425 426 n := chartfile.Name 427 cdir := filepath.Join(path, n) 428 if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { 429 return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir) 430 } 431 if err := os.MkdirAll(cdir, 0755); err != nil { 432 return cdir, err 433 } 434 435 cf := filepath.Join(cdir, ChartfileName) 436 if _, err := os.Stat(cf); err != nil { 437 if err := SaveChartfile(cf, chartfile); err != nil { 438 return cdir, err 439 } 440 } 441 442 for _, d := range []string{TemplatesDir, TemplatesTestsDir, ChartsDir} { 443 if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil { 444 return cdir, err 445 } 446 } 447 448 files := []struct { 449 path string 450 content []byte 451 }{ 452 { 453 // values.yaml 454 path: filepath.Join(cdir, ValuesfileName), 455 content: []byte(fmt.Sprintf(defaultValues, chartfile.Name)), 456 }, 457 { 458 // .helmignore 459 path: filepath.Join(cdir, IgnorefileName), 460 content: []byte(defaultIgnore), 461 }, 462 { 463 // ingress.yaml 464 path: filepath.Join(cdir, TemplatesDir, IngressFileName), 465 content: Transform(defaultIngress, "<CHARTNAME>", chartfile.Name), 466 }, 467 { 468 // deployment.yaml 469 path: filepath.Join(cdir, TemplatesDir, DeploymentName), 470 content: Transform(defaultDeployment, "<CHARTNAME>", chartfile.Name), 471 }, 472 { 473 // service.yaml 474 path: filepath.Join(cdir, TemplatesDir, ServiceName), 475 content: Transform(defaultService, "<CHARTNAME>", chartfile.Name), 476 }, 477 { 478 // serviceaccount.yaml 479 path: filepath.Join(cdir, TemplatesDir, ServiceAccountName), 480 content: Transform(defaultServiceAccount, "<CHARTNAME>", chartfile.Name), 481 }, 482 { 483 // NOTES.txt 484 path: filepath.Join(cdir, TemplatesDir, NotesName), 485 content: Transform(defaultNotes, "<CHARTNAME>", chartfile.Name), 486 }, 487 { 488 // _helpers.tpl 489 path: filepath.Join(cdir, TemplatesDir, HelpersName), 490 content: Transform(defaultHelpers, "<CHARTNAME>", chartfile.Name), 491 }, 492 { 493 // test-connection.yaml 494 path: filepath.Join(cdir, TemplatesTestsDir, TestConnectionName), 495 content: Transform(defaultTestConnection, "<CHARTNAME>", chartfile.Name), 496 }, 497 } 498 499 for _, file := range files { 500 if _, err := os.Stat(file.path); err == nil { 501 // File exists and is okay. Skip it. 502 continue 503 } 504 if err := ioutil.WriteFile(file.path, file.content, 0644); err != nil { 505 return cdir, err 506 } 507 } 508 return cdir, nil 509 }