github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "regexp" 26 "strings" 27 28 "github.com/pkg/errors" 29 "sigs.k8s.io/yaml" 30 31 "github.com/stefanmcshane/helm/pkg/chart" 32 "github.com/stefanmcshane/helm/pkg/chart/loader" 33 ) 34 35 // chartName is a regular expression for testing the supplied name of a chart. 36 // This regular expression is probably stricter than it needs to be. We can relax it 37 // somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be 38 // problematic. 39 var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$") 40 41 const ( 42 // ChartfileName is the default Chart file name. 43 ChartfileName = "Chart.yaml" 44 // ValuesfileName is the default values file name. 45 ValuesfileName = "values.yaml" 46 // SchemafileName is the default values schema file name. 47 SchemafileName = "values.schema.json" 48 // TemplatesDir is the relative directory name for templates. 49 TemplatesDir = "templates" 50 // ChartsDir is the relative directory name for charts dependencies. 51 ChartsDir = "charts" 52 // TemplatesTestsDir is the relative directory name for tests. 53 TemplatesTestsDir = TemplatesDir + sep + "tests" 54 // IgnorefileName is the name of the Helm ignore file. 55 IgnorefileName = ".helmignore" 56 // IngressFileName is the name of the example ingress file. 57 IngressFileName = TemplatesDir + sep + "ingress.yaml" 58 // DeploymentName is the name of the example deployment file. 59 DeploymentName = TemplatesDir + sep + "deployment.yaml" 60 // ServiceName is the name of the example service file. 61 ServiceName = TemplatesDir + sep + "service.yaml" 62 // ServiceAccountName is the name of the example serviceaccount file. 63 ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" 64 // HorizontalPodAutoscalerName is the name of the example hpa file. 65 HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" 66 // NotesName is the name of the example NOTES.txt file. 67 NotesName = TemplatesDir + sep + "NOTES.txt" 68 // HelpersName is the name of the example helpers file. 69 HelpersName = TemplatesDir + sep + "_helpers.tpl" 70 // TestConnectionName is the name of the example test file. 71 TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" 72 ) 73 74 // maxChartNameLength is lower than the limits we know of with certain file systems, 75 // and with certain Kubernetes fields. 76 const maxChartNameLength = 250 77 78 const sep = string(filepath.Separator) 79 80 const defaultChartfile = `apiVersion: v2 81 name: %s 82 description: A Helm chart for Kubernetes 83 84 # A chart can be either an 'application' or a 'library' chart. 85 # 86 # Application charts are a collection of templates that can be packaged into versioned archives 87 # to be deployed. 88 # 89 # Library charts provide useful utilities or functions for the chart developer. They're included as 90 # a dependency of application charts to inject those utilities and functions into the rendering 91 # pipeline. Library charts do not define any templates and therefore cannot be deployed. 92 type: application 93 94 # This is the chart version. This version number should be incremented each time you make changes 95 # to the chart and its templates, including the app version. 96 # Versions are expected to follow Semantic Versioning (https://semver.org/) 97 version: 0.1.0 98 99 # This is the version number of the application being deployed. This version number should be 100 # incremented each time you make changes to the application. Versions are not expected to 101 # follow Semantic Versioning. They should reflect the version the application is using. 102 # It is recommended to use it with quotes. 103 appVersion: "1.16.0" 104 ` 105 106 const defaultValues = `# Default values for %s. 107 # This is a YAML-formatted file. 108 # Declare variables to be passed into your templates. 109 110 replicaCount: 1 111 112 image: 113 repository: nginx 114 pullPolicy: IfNotPresent 115 # Overrides the image tag whose default is the chart appVersion. 116 tag: "" 117 118 imagePullSecrets: [] 119 nameOverride: "" 120 fullnameOverride: "" 121 122 serviceAccount: 123 # Specifies whether a service account should be created 124 create: true 125 # Annotations to add to the service account 126 annotations: {} 127 # The name of the service account to use. 128 # If not set and create is true, a name is generated using the fullname template 129 name: "" 130 131 podAnnotations: {} 132 133 podSecurityContext: {} 134 # fsGroup: 2000 135 136 securityContext: {} 137 # capabilities: 138 # drop: 139 # - ALL 140 # readOnlyRootFilesystem: true 141 # runAsNonRoot: true 142 # runAsUser: 1000 143 144 service: 145 type: ClusterIP 146 port: 80 147 148 ingress: 149 enabled: false 150 className: "" 151 annotations: {} 152 # kubernetes.io/ingress.class: nginx 153 # kubernetes.io/tls-acme: "true" 154 hosts: 155 - host: chart-example.local 156 paths: 157 - path: / 158 pathType: ImplementationSpecific 159 tls: [] 160 # - secretName: chart-example-tls 161 # hosts: 162 # - chart-example.local 163 164 resources: {} 165 # We usually recommend not to specify default resources and to leave this as a conscious 166 # choice for the user. This also increases chances charts run on environments with little 167 # resources, such as Minikube. If you do want to specify resources, uncomment the following 168 # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 169 # limits: 170 # cpu: 100m 171 # memory: 128Mi 172 # requests: 173 # cpu: 100m 174 # memory: 128Mi 175 176 autoscaling: 177 enabled: false 178 minReplicas: 1 179 maxReplicas: 100 180 targetCPUUtilizationPercentage: 80 181 # targetMemoryUtilizationPercentage: 80 182 183 nodeSelector: {} 184 185 tolerations: [] 186 187 affinity: {} 188 ` 189 190 const defaultIgnore = `# Patterns to ignore when building packages. 191 # This supports shell glob matching, relative path matching, and 192 # negation (prefixed with !). Only one pattern per line. 193 .DS_Store 194 # Common VCS dirs 195 .git/ 196 .gitignore 197 .bzr/ 198 .bzrignore 199 .hg/ 200 .hgignore 201 .svn/ 202 # Common backup files 203 *.swp 204 *.bak 205 *.tmp 206 *.orig 207 *~ 208 # Various IDEs 209 .project 210 .idea/ 211 *.tmproj 212 .vscode/ 213 ` 214 215 const defaultIngress = `{{- if .Values.ingress.enabled -}} 216 {{- $fullName := include "<CHARTNAME>.fullname" . -}} 217 {{- $svcPort := .Values.service.port -}} 218 {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 219 {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 220 {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 221 {{- end }} 222 {{- end }} 223 {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 224 apiVersion: networking.k8s.io/v1 225 {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 226 apiVersion: networking.k8s.io/v1beta1 227 {{- else -}} 228 apiVersion: extensions/v1beta1 229 {{- end }} 230 kind: Ingress 231 metadata: 232 name: {{ $fullName }} 233 labels: 234 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 235 {{- with .Values.ingress.annotations }} 236 annotations: 237 {{- toYaml . | nindent 4 }} 238 {{- end }} 239 spec: 240 {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 241 ingressClassName: {{ .Values.ingress.className }} 242 {{- end }} 243 {{- if .Values.ingress.tls }} 244 tls: 245 {{- range .Values.ingress.tls }} 246 - hosts: 247 {{- range .hosts }} 248 - {{ . | quote }} 249 {{- end }} 250 secretName: {{ .secretName }} 251 {{- end }} 252 {{- end }} 253 rules: 254 {{- range .Values.ingress.hosts }} 255 - host: {{ .host | quote }} 256 http: 257 paths: 258 {{- range .paths }} 259 - path: {{ .path }} 260 {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 261 pathType: {{ .pathType }} 262 {{- end }} 263 backend: 264 {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 265 service: 266 name: {{ $fullName }} 267 port: 268 number: {{ $svcPort }} 269 {{- else }} 270 serviceName: {{ $fullName }} 271 servicePort: {{ $svcPort }} 272 {{- end }} 273 {{- end }} 274 {{- end }} 275 {{- end }} 276 ` 277 278 const defaultDeployment = `apiVersion: apps/v1 279 kind: Deployment 280 metadata: 281 name: {{ include "<CHARTNAME>.fullname" . }} 282 labels: 283 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 284 spec: 285 {{- if not .Values.autoscaling.enabled }} 286 replicas: {{ .Values.replicaCount }} 287 {{- end }} 288 selector: 289 matchLabels: 290 {{- include "<CHARTNAME>.selectorLabels" . | nindent 6 }} 291 template: 292 metadata: 293 {{- with .Values.podAnnotations }} 294 annotations: 295 {{- toYaml . | nindent 8 }} 296 {{- end }} 297 labels: 298 {{- include "<CHARTNAME>.selectorLabels" . | nindent 8 }} 299 spec: 300 {{- with .Values.imagePullSecrets }} 301 imagePullSecrets: 302 {{- toYaml . | nindent 8 }} 303 {{- end }} 304 serviceAccountName: {{ include "<CHARTNAME>.serviceAccountName" . }} 305 securityContext: 306 {{- toYaml .Values.podSecurityContext | nindent 8 }} 307 containers: 308 - name: {{ .Chart.Name }} 309 securityContext: 310 {{- toYaml .Values.securityContext | nindent 12 }} 311 image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 312 imagePullPolicy: {{ .Values.image.pullPolicy }} 313 ports: 314 - name: http 315 containerPort: {{ .Values.service.port }} 316 protocol: TCP 317 livenessProbe: 318 httpGet: 319 path: / 320 port: http 321 readinessProbe: 322 httpGet: 323 path: / 324 port: http 325 resources: 326 {{- toYaml .Values.resources | nindent 12 }} 327 {{- with .Values.nodeSelector }} 328 nodeSelector: 329 {{- toYaml . | nindent 8 }} 330 {{- end }} 331 {{- with .Values.affinity }} 332 affinity: 333 {{- toYaml . | nindent 8 }} 334 {{- end }} 335 {{- with .Values.tolerations }} 336 tolerations: 337 {{- toYaml . | nindent 8 }} 338 {{- end }} 339 ` 340 341 const defaultService = `apiVersion: v1 342 kind: Service 343 metadata: 344 name: {{ include "<CHARTNAME>.fullname" . }} 345 labels: 346 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 347 spec: 348 type: {{ .Values.service.type }} 349 ports: 350 - port: {{ .Values.service.port }} 351 targetPort: http 352 protocol: TCP 353 name: http 354 selector: 355 {{- include "<CHARTNAME>.selectorLabels" . | nindent 4 }} 356 ` 357 358 const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} 359 apiVersion: v1 360 kind: ServiceAccount 361 metadata: 362 name: {{ include "<CHARTNAME>.serviceAccountName" . }} 363 labels: 364 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 365 {{- with .Values.serviceAccount.annotations }} 366 annotations: 367 {{- toYaml . | nindent 4 }} 368 {{- end }} 369 {{- end }} 370 ` 371 372 const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} 373 apiVersion: autoscaling/v2beta1 374 kind: HorizontalPodAutoscaler 375 metadata: 376 name: {{ include "<CHARTNAME>.fullname" . }} 377 labels: 378 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 379 spec: 380 scaleTargetRef: 381 apiVersion: apps/v1 382 kind: Deployment 383 name: {{ include "<CHARTNAME>.fullname" . }} 384 minReplicas: {{ .Values.autoscaling.minReplicas }} 385 maxReplicas: {{ .Values.autoscaling.maxReplicas }} 386 metrics: 387 {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 388 - type: Resource 389 resource: 390 name: cpu 391 targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 392 {{- end }} 393 {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 394 - type: Resource 395 resource: 396 name: memory 397 targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 398 {{- end }} 399 {{- end }} 400 ` 401 402 const defaultNotes = `1. Get the application URL by running these commands: 403 {{- if .Values.ingress.enabled }} 404 {{- range $host := .Values.ingress.hosts }} 405 {{- range .paths }} 406 http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 407 {{- end }} 408 {{- end }} 409 {{- else if contains "NodePort" .Values.service.type }} 410 export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "<CHARTNAME>.fullname" . }}) 411 export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 412 echo http://$NODE_IP:$NODE_PORT 413 {{- else if contains "LoadBalancer" .Values.service.type }} 414 NOTE: It may take a few minutes for the LoadBalancer IP to be available. 415 You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "<CHARTNAME>.fullname" . }}' 416 export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "<CHARTNAME>.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 417 echo http://$SERVICE_IP:{{ .Values.service.port }} 418 {{- else if contains "ClusterIP" .Values.service.type }} 419 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}") 420 export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 421 echo "Visit http://127.0.0.1:8080 to use your application" 422 kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 423 {{- end }} 424 ` 425 426 const defaultHelpers = `{{/* 427 Expand the name of the chart. 428 */}} 429 {{- define "<CHARTNAME>.name" -}} 430 {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 431 {{- end }} 432 433 {{/* 434 Create a default fully qualified app name. 435 We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 436 If release name contains chart name it will be used as a full name. 437 */}} 438 {{- define "<CHARTNAME>.fullname" -}} 439 {{- if .Values.fullnameOverride }} 440 {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 441 {{- else }} 442 {{- $name := default .Chart.Name .Values.nameOverride }} 443 {{- if contains $name .Release.Name }} 444 {{- .Release.Name | trunc 63 | trimSuffix "-" }} 445 {{- else }} 446 {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 447 {{- end }} 448 {{- end }} 449 {{- end }} 450 451 {{/* 452 Create chart name and version as used by the chart label. 453 */}} 454 {{- define "<CHARTNAME>.chart" -}} 455 {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 456 {{- end }} 457 458 {{/* 459 Common labels 460 */}} 461 {{- define "<CHARTNAME>.labels" -}} 462 helm.sh/chart: {{ include "<CHARTNAME>.chart" . }} 463 {{ include "<CHARTNAME>.selectorLabels" . }} 464 {{- if .Chart.AppVersion }} 465 app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 466 {{- end }} 467 app.kubernetes.io/managed-by: {{ .Release.Service }} 468 {{- end }} 469 470 {{/* 471 Selector labels 472 */}} 473 {{- define "<CHARTNAME>.selectorLabels" -}} 474 app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }} 475 app.kubernetes.io/instance: {{ .Release.Name }} 476 {{- end }} 477 478 {{/* 479 Create the name of the service account to use 480 */}} 481 {{- define "<CHARTNAME>.serviceAccountName" -}} 482 {{- if .Values.serviceAccount.create }} 483 {{- default (include "<CHARTNAME>.fullname" .) .Values.serviceAccount.name }} 484 {{- else }} 485 {{- default "default" .Values.serviceAccount.name }} 486 {{- end }} 487 {{- end }} 488 ` 489 490 const defaultTestConnection = `apiVersion: v1 491 kind: Pod 492 metadata: 493 name: "{{ include "<CHARTNAME>.fullname" . }}-test-connection" 494 labels: 495 {{- include "<CHARTNAME>.labels" . | nindent 4 }} 496 annotations: 497 "helm.sh/hook": test 498 spec: 499 containers: 500 - name: wget 501 image: busybox 502 command: ['wget'] 503 args: ['{{ include "<CHARTNAME>.fullname" . }}:{{ .Values.service.port }}'] 504 restartPolicy: Never 505 ` 506 507 // Stderr is an io.Writer to which error messages can be written 508 // 509 // In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward 510 // compatibility. 511 var Stderr io.Writer = os.Stderr 512 513 // CreateFrom creates a new chart, but scaffolds it from the src chart. 514 func CreateFrom(chartfile *chart.Metadata, dest, src string) error { 515 schart, err := loader.Load(src) 516 if err != nil { 517 return errors.Wrapf(err, "could not load %s", src) 518 } 519 520 schart.Metadata = chartfile 521 522 var updatedTemplates []*chart.File 523 524 for _, template := range schart.Templates { 525 newData := transform(string(template.Data), schart.Name()) 526 updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) 527 } 528 529 schart.Templates = updatedTemplates 530 b, err := yaml.Marshal(schart.Values) 531 if err != nil { 532 return errors.Wrap(err, "reading values file") 533 } 534 535 var m map[string]interface{} 536 if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { 537 return errors.Wrap(err, "transforming values file") 538 } 539 schart.Values = m 540 541 // SaveDir looks for the file values.yaml when saving rather than the values 542 // key in order to preserve the comments in the YAML. The name placeholder 543 // needs to be replaced on that file. 544 for _, f := range schart.Raw { 545 if f.Name == ValuesfileName { 546 f.Data = transform(string(f.Data), schart.Name()) 547 } 548 } 549 550 return SaveDir(schart, dest) 551 } 552 553 // Create creates a new chart in a directory. 554 // 555 // Inside of dir, this will create a directory based on the name of 556 // chartfile.Name. It will then write the Chart.yaml into this directory and 557 // create the (empty) appropriate directories. 558 // 559 // The returned string will point to the newly created directory. It will be 560 // an absolute path, even if the provided base directory was relative. 561 // 562 // If dir does not exist, this will return an error. 563 // If Chart.yaml or any directories cannot be created, this will return an 564 // error. In such a case, this will attempt to clean up by removing the 565 // new chart directory. 566 func Create(name, dir string) (string, error) { 567 568 // Sanity-check the name of a chart so user doesn't create one that causes problems. 569 if err := validateChartName(name); err != nil { 570 return "", err 571 } 572 573 path, err := filepath.Abs(dir) 574 if err != nil { 575 return path, err 576 } 577 578 if fi, err := os.Stat(path); err != nil { 579 return path, err 580 } else if !fi.IsDir() { 581 return path, errors.Errorf("no such directory %s", path) 582 } 583 584 cdir := filepath.Join(path, name) 585 if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { 586 return cdir, errors.Errorf("file %s already exists and is not a directory", cdir) 587 } 588 589 files := []struct { 590 path string 591 content []byte 592 }{ 593 { 594 // Chart.yaml 595 path: filepath.Join(cdir, ChartfileName), 596 content: []byte(fmt.Sprintf(defaultChartfile, name)), 597 }, 598 { 599 // values.yaml 600 path: filepath.Join(cdir, ValuesfileName), 601 content: []byte(fmt.Sprintf(defaultValues, name)), 602 }, 603 { 604 // .helmignore 605 path: filepath.Join(cdir, IgnorefileName), 606 content: []byte(defaultIgnore), 607 }, 608 { 609 // ingress.yaml 610 path: filepath.Join(cdir, IngressFileName), 611 content: transform(defaultIngress, name), 612 }, 613 { 614 // deployment.yaml 615 path: filepath.Join(cdir, DeploymentName), 616 content: transform(defaultDeployment, name), 617 }, 618 { 619 // service.yaml 620 path: filepath.Join(cdir, ServiceName), 621 content: transform(defaultService, name), 622 }, 623 { 624 // serviceaccount.yaml 625 path: filepath.Join(cdir, ServiceAccountName), 626 content: transform(defaultServiceAccount, name), 627 }, 628 { 629 // hpa.yaml 630 path: filepath.Join(cdir, HorizontalPodAutoscalerName), 631 content: transform(defaultHorizontalPodAutoscaler, name), 632 }, 633 { 634 // NOTES.txt 635 path: filepath.Join(cdir, NotesName), 636 content: transform(defaultNotes, name), 637 }, 638 { 639 // _helpers.tpl 640 path: filepath.Join(cdir, HelpersName), 641 content: transform(defaultHelpers, name), 642 }, 643 { 644 // test-connection.yaml 645 path: filepath.Join(cdir, TestConnectionName), 646 content: transform(defaultTestConnection, name), 647 }, 648 } 649 650 for _, file := range files { 651 if _, err := os.Stat(file.path); err == nil { 652 // There is no handle to a preferred output stream here. 653 fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path) 654 } 655 if err := writeFile(file.path, file.content); err != nil { 656 return cdir, err 657 } 658 } 659 // Need to add the ChartsDir explicitly as it does not contain any file OOTB 660 if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil { 661 return cdir, err 662 } 663 return cdir, nil 664 } 665 666 // transform performs a string replacement of the specified source for 667 // a given key with the replacement string 668 func transform(src, replacement string) []byte { 669 return []byte(strings.ReplaceAll(src, "<CHARTNAME>", replacement)) 670 } 671 672 func writeFile(name string, content []byte) error { 673 if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { 674 return err 675 } 676 return ioutil.WriteFile(name, content, 0644) 677 } 678 679 func validateChartName(name string) error { 680 if name == "" || len(name) > maxChartNameLength { 681 return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) 682 } 683 if !chartName.MatchString(name) { 684 return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) 685 } 686 return nil 687 }