github.com/brockwood/helm@v2.8.0-rc.1.0.20180112204834-077be881c4cc+incompatible/pkg/chartutil/create.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     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  	"k8s.io/helm/pkg/proto/hapi/chart"
    27  )
    28  
    29  const (
    30  	// ChartfileName is the default Chart file name.
    31  	ChartfileName = "Chart.yaml"
    32  	// ValuesfileName is the default values file name.
    33  	ValuesfileName = "values.yaml"
    34  	// TemplatesDir is the relative directory name for templates.
    35  	TemplatesDir = "templates"
    36  	// ChartsDir is the relative directory name for charts dependencies.
    37  	ChartsDir = "charts"
    38  	// IgnorefileName is the name of the Helm ignore file.
    39  	IgnorefileName = ".helmignore"
    40  	// IngressFileName is the name of the example ingress file.
    41  	IngressFileName = "ingress.yaml"
    42  	// DeploymentName is the name of the example deployment file.
    43  	DeploymentName = "deployment.yaml"
    44  	// ServiceName is the name of the example service file.
    45  	ServiceName = "service.yaml"
    46  	// NotesName is the name of the example NOTES.txt file.
    47  	NotesName = "NOTES.txt"
    48  	// HelpersName is the name of the example NOTES.txt file.
    49  	HelpersName = "_helpers.tpl"
    50  )
    51  
    52  const defaultValues = `# Default values for %s.
    53  # This is a YAML-formatted file.
    54  # Declare variables to be passed into your templates.
    55  replicaCount: 1
    56  image:
    57    repository: nginx
    58    tag: stable
    59    pullPolicy: IfNotPresent
    60  service:
    61    name: nginx
    62    type: ClusterIP
    63    externalPort: 80
    64    internalPort: 80
    65  ingress:
    66    enabled: false
    67    # Used to create an Ingress record.
    68    hosts:
    69      - chart-example.local
    70    annotations:
    71      # kubernetes.io/ingress.class: nginx
    72      # kubernetes.io/tls-acme: "true"
    73    tls:
    74      # Secrets must be manually created in the namespace.
    75      # - secretName: chart-example-tls
    76      #   hosts:
    77      #     - chart-example.local
    78  resources: {}
    79    # We usually recommend not to specify default resources and to leave this as a conscious
    80    # choice for the user. This also increases chances charts run on environments with little
    81    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    82    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    83    # limits:
    84    #  cpu: 100m
    85    #  memory: 128Mi
    86    # requests:
    87    #  cpu: 100m
    88    #  memory: 128Mi
    89  `
    90  
    91  const defaultIgnore = `# Patterns to ignore when building packages.
    92  # This supports shell glob matching, relative path matching, and
    93  # negation (prefixed with !). Only one pattern per line.
    94  .DS_Store
    95  # Common VCS dirs
    96  .git/
    97  .gitignore
    98  .bzr/
    99  .bzrignore
   100  .hg/
   101  .hgignore
   102  .svn/
   103  # Common backup files
   104  *.swp
   105  *.bak
   106  *.tmp
   107  *~
   108  # Various IDEs
   109  .project
   110  .idea/
   111  *.tmproj
   112  `
   113  
   114  const defaultIngress = `{{- if .Values.ingress.enabled -}}
   115  {{- $serviceName := include "<CHARTNAME>.fullname" . -}}
   116  {{- $servicePort := .Values.service.externalPort -}}
   117  apiVersion: extensions/v1beta1
   118  kind: Ingress
   119  metadata:
   120    name: {{ template "<CHARTNAME>.fullname" . }}
   121    labels:
   122      app: {{ template "<CHARTNAME>.name" . }}
   123      chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
   124      release: {{ .Release.Name }}
   125      heritage: {{ .Release.Service }}
   126    annotations:
   127      {{- range $key, $value := .Values.ingress.annotations }}
   128        {{ $key }}: {{ $value | quote }}
   129      {{- end }}
   130  spec:
   131    rules:
   132      {{- range $host := .Values.ingress.hosts }}
   133      - host: {{ $host }}
   134        http:
   135          paths:
   136            - path: /
   137              backend:
   138                serviceName: {{ $serviceName }}
   139                servicePort: {{ $servicePort }}
   140      {{- end -}}
   141    {{- if .Values.ingress.tls }}
   142    tls:
   143  {{ toYaml .Values.ingress.tls | indent 4 }}
   144    {{- end -}}
   145  {{- end -}}
   146  `
   147  
   148  const defaultDeployment = `apiVersion: extensions/v1beta1
   149  kind: Deployment
   150  metadata:
   151    name: {{ template "<CHARTNAME>.fullname" . }}
   152    labels:
   153      app: {{ template "<CHARTNAME>.name" . }}
   154      chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
   155      release: {{ .Release.Name }}
   156      heritage: {{ .Release.Service }}
   157  spec:
   158    replicas: {{ .Values.replicaCount }}
   159    template:
   160      metadata:
   161        labels:
   162          app: {{ template "<CHARTNAME>.name" . }}
   163          release: {{ .Release.Name }}
   164      spec:
   165        containers:
   166          - name: {{ .Chart.Name }}
   167            image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
   168            imagePullPolicy: {{ .Values.image.pullPolicy }}
   169            ports:
   170              - containerPort: {{ .Values.service.internalPort }}
   171            livenessProbe:
   172              httpGet:
   173                path: /
   174                port: {{ .Values.service.internalPort }}
   175            readinessProbe:
   176              httpGet:
   177                path: /
   178                port: {{ .Values.service.internalPort }}
   179            resources:
   180  {{ toYaml .Values.resources | indent 12 }}
   181      {{- if .Values.nodeSelector }}
   182        nodeSelector:
   183  {{ toYaml .Values.nodeSelector | indent 8 }}
   184      {{- end }}
   185  `
   186  
   187  const defaultService = `apiVersion: v1
   188  kind: Service
   189  metadata:
   190    name: {{ template "<CHARTNAME>.fullname" . }}
   191    labels:
   192      app: {{ template "<CHARTNAME>.name" . }}
   193      chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
   194      release: {{ .Release.Name }}
   195      heritage: {{ .Release.Service }}
   196  spec:
   197    type: {{ .Values.service.type }}
   198    ports:
   199      - port: {{ .Values.service.externalPort }}
   200        targetPort: {{ .Values.service.internalPort }}
   201        protocol: TCP
   202        name: {{ .Values.service.name }}
   203    selector:
   204      app: {{ template "<CHARTNAME>.name" . }}
   205      release: {{ .Release.Name }}
   206  `
   207  
   208  const defaultNotes = `1. Get the application URL by running these commands:
   209  {{- if .Values.ingress.enabled }}
   210  {{- range .Values.ingress.hosts }}
   211    http://{{ . }}
   212  {{- end }}
   213  {{- else if contains "NodePort" .Values.service.type }}
   214    export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "<CHARTNAME>.fullname" . }})
   215    export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
   216    echo http://$NODE_IP:$NODE_PORT
   217  {{- else if contains "LoadBalancer" .Values.service.type }}
   218       NOTE: It may take a few minutes for the LoadBalancer IP to be available.
   219             You can watch the status of by running 'kubectl get svc -w {{ template "<CHARTNAME>.fullname" . }}'
   220    export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "<CHARTNAME>.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
   221    echo http://$SERVICE_IP:{{ .Values.service.externalPort }}
   222  {{- else if contains "ClusterIP" .Values.service.type }}
   223    export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "<CHARTNAME>.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
   224    echo "Visit http://127.0.0.1:8080 to use your application"
   225    kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }}
   226  {{- end }}
   227  `
   228  
   229  const defaultHelpers = `{{/* vim: set filetype=mustache: */}}
   230  {{/*
   231  Expand the name of the chart.
   232  */}}
   233  {{- define "<CHARTNAME>.name" -}}
   234  {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
   235  {{- end -}}
   236  
   237  {{/*
   238  Create a default fully qualified app name.
   239  We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
   240  If release name contains chart name it will be used as a full name.
   241  */}}
   242  {{- define "<CHARTNAME>.fullname" -}}
   243  {{- $name := default .Chart.Name .Values.nameOverride -}}
   244  {{- if contains $name .Release.Name -}}
   245  {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
   246  {{- else -}}
   247  {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
   248  {{- end -}}
   249  {{- end -}}
   250  `
   251  
   252  // CreateFrom creates a new chart, but scaffolds it from the src chart.
   253  func CreateFrom(chartfile *chart.Metadata, dest string, src string) error {
   254  	schart, err := Load(src)
   255  	if err != nil {
   256  		return fmt.Errorf("could not load %s: %s", src, err)
   257  	}
   258  
   259  	schart.Metadata = chartfile
   260  	return SaveDir(schart, dest)
   261  }
   262  
   263  // Create creates a new chart in a directory.
   264  //
   265  // Inside of dir, this will create a directory based on the name of
   266  // chartfile.Name. It will then write the Chart.yaml into this directory and
   267  // create the (empty) appropriate directories.
   268  //
   269  // The returned string will point to the newly created directory. It will be
   270  // an absolute path, even if the provided base directory was relative.
   271  //
   272  // If dir does not exist, this will return an error.
   273  // If Chart.yaml or any directories cannot be created, this will return an
   274  // error. In such a case, this will attempt to clean up by removing the
   275  // new chart directory.
   276  func Create(chartfile *chart.Metadata, dir string) (string, error) {
   277  	path, err := filepath.Abs(dir)
   278  	if err != nil {
   279  		return path, err
   280  	}
   281  
   282  	if fi, err := os.Stat(path); err != nil {
   283  		return path, err
   284  	} else if !fi.IsDir() {
   285  		return path, fmt.Errorf("no such directory %s", path)
   286  	}
   287  
   288  	n := chartfile.Name
   289  	cdir := filepath.Join(path, n)
   290  	if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() {
   291  		return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir)
   292  	}
   293  	if err := os.MkdirAll(cdir, 0755); err != nil {
   294  		return cdir, err
   295  	}
   296  
   297  	cf := filepath.Join(cdir, ChartfileName)
   298  	if _, err := os.Stat(cf); err != nil {
   299  		if err := SaveChartfile(cf, chartfile); err != nil {
   300  			return cdir, err
   301  		}
   302  	}
   303  
   304  	for _, d := range []string{TemplatesDir, ChartsDir} {
   305  		if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil {
   306  			return cdir, err
   307  		}
   308  	}
   309  
   310  	files := []struct {
   311  		path    string
   312  		content []byte
   313  	}{
   314  		{
   315  			// values.yaml
   316  			path:    filepath.Join(cdir, ValuesfileName),
   317  			content: []byte(fmt.Sprintf(defaultValues, chartfile.Name)),
   318  		},
   319  		{
   320  			// .helmignore
   321  			path:    filepath.Join(cdir, IgnorefileName),
   322  			content: []byte(defaultIgnore),
   323  		},
   324  		{
   325  			// ingress.yaml
   326  			path:    filepath.Join(cdir, TemplatesDir, IngressFileName),
   327  			content: []byte(strings.Replace(defaultIngress, "<CHARTNAME>", chartfile.Name, -1)),
   328  		},
   329  		{
   330  			// deployment.yaml
   331  			path:    filepath.Join(cdir, TemplatesDir, DeploymentName),
   332  			content: []byte(strings.Replace(defaultDeployment, "<CHARTNAME>", chartfile.Name, -1)),
   333  		},
   334  		{
   335  			// service.yaml
   336  			path:    filepath.Join(cdir, TemplatesDir, ServiceName),
   337  			content: []byte(strings.Replace(defaultService, "<CHARTNAME>", chartfile.Name, -1)),
   338  		},
   339  		{
   340  			// NOTES.txt
   341  			path:    filepath.Join(cdir, TemplatesDir, NotesName),
   342  			content: []byte(strings.Replace(defaultNotes, "<CHARTNAME>", chartfile.Name, -1)),
   343  		},
   344  		{
   345  			// _helpers.tpl
   346  			path:    filepath.Join(cdir, TemplatesDir, HelpersName),
   347  			content: []byte(strings.Replace(defaultHelpers, "<CHARTNAME>", chartfile.Name, -1)),
   348  		},
   349  	}
   350  
   351  	for _, file := range files {
   352  		if _, err := os.Stat(file.path); err == nil {
   353  			// File exists and is okay. Skip it.
   354  			continue
   355  		}
   356  		if err := ioutil.WriteFile(file.path, file.content, 0644); err != nil {
   357  			return cdir, err
   358  		}
   359  	}
   360  	return cdir, nil
   361  }