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