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  }