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