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