github.com/zsuzhengdu/helm@v3.0.0-beta.3+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 "strings" 25 26 "github.com/pkg/errors" 27 "sigs.k8s.io/yaml" 28 29 "helm.sh/helm/pkg/chart" 30 "helm.sh/helm/pkg/chart/loader" 31 ) 32 33 const ( 34 // ChartfileName is the default Chart file name. 35 ChartfileName = "Chart.yaml" 36 // ValuesfileName is the default values file name. 37 ValuesfileName = "values.yaml" 38 // TemplatesDir is the relative directory name for templates. 39 TemplatesDir = "templates" 40 // ChartsDir is the relative directory name for charts dependencies. 41 ChartsDir = "charts" 42 // IgnorefileName is the name of the Helm ignore file. 43 IgnorefileName = ".helmignore" 44 // IngressFileName is the name of the example ingress file. 45 IngressFileName = TemplatesDir + sep + "ingress.yaml" 46 // DeploymentName is the name of the example deployment file. 47 DeploymentName = TemplatesDir + sep + "deployment.yaml" 48 // ServiceName is the name of the example service file. 49 ServiceName = TemplatesDir + sep + "service.yaml" 50 // NotesName is the name of the example NOTES.txt file. 51 NotesName = TemplatesDir + sep + "NOTES.txt" 52 // HelpersName is the name of the example NOTES.txt file. 53 HelpersName = TemplatesDir + sep + "_helpers.tpl" 54 ) 55 56 const sep = string(filepath.Separator) 57 58 const defaultChartfile = `apiVersion: v2 59 name: %s 60 description: A Helm chart for Kubernetes 61 62 # A chart can be either an 'application' or a 'library' chart. 63 # 64 # Application charts are a collection of templates that can be packaged into versioned archives 65 # to be deployed. 66 # 67 # Library charts provide useful utilities or functions for the chart developer. They're included as 68 # a dependency of application charts to inject those utilities and functions into the rendering 69 # pipeline. Library charts do not define any templates and therefore cannot be deployed. 70 type: application 71 72 # This is the chart version. This version number should be incremented each time you make changes 73 # to the chart and its templates, including the app version. 74 version: 0.1.0 75 76 # This is the version number of the application being deployed. This version number should be 77 # incremented each time you make changes to the application. 78 appVersion: 1.16.0 79 ` 80 81 const defaultValues = `# Default values for %s. 82 # This is a YAML-formatted file. 83 # Declare variables to be passed into your templates. 84 85 replicaCount: 1 86 87 image: 88 repository: nginx 89 pullPolicy: IfNotPresent 90 91 nameOverride: "" 92 fullnameOverride: "" 93 94 service: 95 type: ClusterIP 96 port: 80 97 98 ingress: 99 enabled: false 100 annotations: {} 101 # kubernetes.io/ingress.class: nginx 102 # kubernetes.io/tls-acme: "true" 103 hosts: 104 - host: chart-example.local 105 paths: [] 106 107 tls: [] 108 # - secretName: chart-example-tls 109 # hosts: 110 # - chart-example.local 111 112 resources: {} 113 # We usually recommend not to specify default resources and to leave this as a conscious 114 # choice for the user. This also increases chances charts run on environments with little 115 # resources, such as Minikube. If you do want to specify resources, uncomment the following 116 # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 117 # limits: 118 # cpu: 100m 119 # memory: 128Mi 120 # requests: 121 # cpu: 100m 122 # memory: 128Mi 123 124 nodeSelector: {} 125 126 tolerations: [] 127 128 affinity: {} 129 ` 130 131 const defaultIgnore = `# Patterns to ignore when building packages. 132 # This supports shell glob matching, relative path matching, and 133 # negation (prefixed with !). Only one pattern per line. 134 .DS_Store 135 # Common VCS dirs 136 .git/ 137 .gitignore 138 .bzr/ 139 .bzrignore 140 .hg/ 141 .hgignore 142 .svn/ 143 # Common backup files 144 *.swp 145 *.bak 146 *.tmp 147 *~ 148 # Various IDEs 149 .project 150 .idea/ 151 *.tmproj 152 ` 153 154 const defaultIngress = `{{- if .Values.ingress.enabled -}} 155 {{- $fullName := include "<CHARTNAME>.fullname" . -}} 156 {{- $svcPort := .Values.service.port -}} 157 apiVersion: networking.k8s.io/v1beta1 158 kind: Ingress 159 metadata: 160 name: {{ $fullName }} 161 labels: 162 {{ include "<CHARTNAME>.labels" . | indent 4 }} 163 {{- with .Values.ingress.annotations }} 164 annotations: 165 {{- toYaml . | nindent 4 }} 166 {{- end }} 167 spec: 168 {{- if .Values.ingress.tls }} 169 tls: 170 {{- range .Values.ingress.tls }} 171 - hosts: 172 {{- range .hosts }} 173 - {{ . | quote }} 174 {{- end }} 175 secretName: {{ .secretName }} 176 {{- end }} 177 {{- end }} 178 rules: 179 {{- range .Values.ingress.hosts }} 180 - host: {{ .host | quote }} 181 http: 182 paths: 183 {{- range .paths }} 184 - path: {{ . }} 185 backend: 186 serviceName: {{ $fullName }} 187 servicePort: {{ $svcPort }} 188 {{- end }} 189 {{- end }} 190 {{- end }} 191 ` 192 193 const defaultDeployment = `apiVersion: apps/v1beta2 194 kind: Deployment 195 metadata: 196 name: {{ template "<CHARTNAME>.fullname" . }} 197 labels: 198 {{ include "<CHARTNAME>.labels" . | indent 4 }} 199 spec: 200 replicas: {{ .Values.replicaCount }} 201 selector: 202 matchLabels: 203 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 204 app.kubernetes.io/instance: {{ .Release.Name }} 205 template: 206 metadata: 207 labels: 208 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 209 app.kubernetes.io/instance: {{ .Release.Name }} 210 spec: 211 containers: 212 - name: {{ .Chart.Name }} 213 image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" 214 imagePullPolicy: {{ .Values.image.pullPolicy }} 215 ports: 216 - name: http 217 containerPort: 80 218 protocol: TCP 219 livenessProbe: 220 httpGet: 221 path: / 222 port: http 223 readinessProbe: 224 httpGet: 225 path: / 226 port: http 227 resources: 228 {{ toYaml .Values.resources | indent 12 }} 229 {{- with .Values.nodeSelector }} 230 nodeSelector: 231 {{ toYaml . | indent 8 }} 232 {{- end }} 233 {{- with .Values.affinity }} 234 affinity: 235 {{ toYaml . | indent 8 }} 236 {{- end }} 237 {{- with .Values.tolerations }} 238 tolerations: 239 {{ toYaml . | indent 8 }} 240 {{- end }} 241 ` 242 243 const defaultService = `apiVersion: v1 244 kind: Service 245 metadata: 246 name: {{ template "<CHARTNAME>.fullname" . }} 247 labels: 248 {{ include "<CHARTNAME>.labels" . | indent 4 }} 249 spec: 250 type: {{ .Values.service.type }} 251 ports: 252 - port: {{ .Values.service.port }} 253 targetPort: http 254 protocol: TCP 255 name: http 256 selector: 257 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 258 app.kubernetes.io/instance: {{ .Release.Name }} 259 ` 260 261 const defaultNotes = `1. Get the application URL by running these commands: 262 {{- if .Values.ingress.enabled }} 263 {{- range $host := .Values.ingress.hosts }} 264 {{- range .paths }} 265 http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 266 {{- end }} 267 {{- end }} 268 {{- else if contains "NodePort" .Values.service.type }} 269 export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "<CHARTNAME>.fullname" . }}) 270 export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") 271 echo http://$NODE_IP:$NODE_PORT 272 {{- else if contains "LoadBalancer" .Values.service.type }} 273 NOTE: It may take a few minutes for the LoadBalancer IP to be available. 274 You can watch the status of by running 'kubectl get svc -w {{ template "<CHARTNAME>.fullname" . }}' 275 export SERVICE_IP=$(kubectl get svc {{ template "<CHARTNAME>.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 276 echo http://$SERVICE_IP:{{ .Values.service.port }} 277 {{- else if contains "ClusterIP" .Values.service.type }} 278 export POD_NAME=$(kubectl get pods -l "app={{ template "<CHARTNAME>.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 279 echo "Visit http://127.0.0.1:8080 to use your application" 280 kubectl port-forward $POD_NAME 8080:80 281 {{- end }} 282 ` 283 284 const defaultHelpers = `{{/* vim: set filetype=mustache: */}} 285 {{/* 286 Expand the name of the chart. 287 */}} 288 {{- define "<CHARTNAME>.name" -}} 289 {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 290 {{- end -}} 291 292 {{/* 293 Create a default fully qualified app name. 294 We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 295 If release name contains chart name it will be used as a full name. 296 */}} 297 {{- define "<CHARTNAME>.fullname" -}} 298 {{- if .Values.fullnameOverride -}} 299 {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 300 {{- else -}} 301 {{- $name := default .Chart.Name .Values.nameOverride -}} 302 {{- if contains $name .Release.Name -}} 303 {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 304 {{- else -}} 305 {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 306 {{- end -}} 307 {{- end -}} 308 {{- end -}} 309 310 {{/* 311 Create chart name and version as used by the chart label. 312 */}} 313 {{- define "<CHARTNAME>.chart" -}} 314 {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 315 {{- end -}} 316 317 {{/* 318 Common labels 319 */}} 320 {{- define "<CHARTNAME>.labels" -}} 321 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 322 helm.sh/chart: {{ include "<CHARTNAME>.chart" . }} 323 app.kubernetes.io/instance: {{ .Release.Name }} 324 {{- if .Chart.AppVersion }} 325 app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 326 {{- end }} 327 app.kubernetes.io/managed-by: {{ .Release.Service }} 328 {{- end -}} 329 ` 330 331 // CreateFrom creates a new chart, but scaffolds it from the src chart. 332 func CreateFrom(chartfile *chart.Metadata, dest, src string) error { 333 schart, err := loader.Load(src) 334 if err != nil { 335 return errors.Wrapf(err, "could not load %s", src) 336 } 337 338 schart.Metadata = chartfile 339 340 var updatedTemplates []*chart.File 341 342 for _, template := range schart.Templates { 343 newData := transform(string(template.Data), schart.Name()) 344 updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) 345 } 346 347 schart.Templates = updatedTemplates 348 b, err := yaml.Marshal(schart.Values) 349 if err != nil { 350 return errors.Wrap(err, "reading values file") 351 } 352 353 var m map[string]interface{} 354 if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { 355 return errors.Wrap(err, "transforming values file") 356 } 357 schart.Values = m 358 359 return SaveDir(schart, dest) 360 } 361 362 // Create creates a new chart in a directory. 363 // 364 // Inside of dir, this will create a directory based on the name of 365 // chartfile.Name. It will then write the Chart.yaml into this directory and 366 // create the (empty) appropriate directories. 367 // 368 // The returned string will point to the newly created directory. It will be 369 // an absolute path, even if the provided base directory was relative. 370 // 371 // If dir does not exist, this will return an error. 372 // If Chart.yaml or any directories cannot be created, this will return an 373 // error. In such a case, this will attempt to clean up by removing the 374 // new chart directory. 375 func Create(name, dir string) (string, error) { 376 path, err := filepath.Abs(dir) 377 if err != nil { 378 return path, err 379 } 380 381 if fi, err := os.Stat(path); err != nil { 382 return path, err 383 } else if !fi.IsDir() { 384 return path, errors.Errorf("no such directory %s", path) 385 } 386 387 cdir := filepath.Join(path, name) 388 if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { 389 return cdir, errors.Errorf("file %s already exists and is not a directory", cdir) 390 } 391 392 files := []struct { 393 path string 394 content []byte 395 }{ 396 { 397 // Chart.yaml 398 path: filepath.Join(cdir, ChartfileName), 399 content: []byte(fmt.Sprintf(defaultChartfile, name)), 400 }, 401 { 402 // values.yaml 403 path: filepath.Join(cdir, ValuesfileName), 404 content: []byte(fmt.Sprintf(defaultValues, name)), 405 }, 406 { 407 // .helmignore 408 path: filepath.Join(cdir, IgnorefileName), 409 content: []byte(defaultIgnore), 410 }, 411 { 412 // ingress.yaml 413 path: filepath.Join(cdir, IngressFileName), 414 content: transform(defaultIngress, name), 415 }, 416 { 417 // deployment.yaml 418 path: filepath.Join(cdir, DeploymentName), 419 content: transform(defaultDeployment, name), 420 }, 421 { 422 // service.yaml 423 path: filepath.Join(cdir, ServiceName), 424 content: transform(defaultService, name), 425 }, 426 { 427 // NOTES.txt 428 path: filepath.Join(cdir, NotesName), 429 content: transform(defaultNotes, name), 430 }, 431 { 432 // _helpers.tpl 433 path: filepath.Join(cdir, HelpersName), 434 content: transform(defaultHelpers, name), 435 }, 436 } 437 438 for _, file := range files { 439 if _, err := os.Stat(file.path); err == nil { 440 // File exists and is okay. Skip it. 441 continue 442 } 443 if err := writeFile(file.path, file.content); err != nil { 444 return cdir, err 445 } 446 } 447 return cdir, nil 448 } 449 450 // transform performs a string replacement of the specified source for 451 // a given key with the replacement string 452 func transform(src, replacement string) []byte { 453 return []byte(strings.ReplaceAll(src, "<CHARTNAME>", replacement)) 454 } 455 456 func writeFile(name string, content []byte) error { 457 if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { 458 return err 459 } 460 return ioutil.WriteFile(name, content, 0644) 461 }