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  }