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  }