github.com/latiif/helm@v2.15.0+incompatible/cmd/helm/template.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 main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/spf13/cobra"
    31  
    32  	"k8s.io/apimachinery/pkg/util/validation"
    33  	"k8s.io/helm/pkg/chartutil"
    34  	"k8s.io/helm/pkg/manifest"
    35  	"k8s.io/helm/pkg/proto/hapi/chart"
    36  	"k8s.io/helm/pkg/proto/hapi/release"
    37  	"k8s.io/helm/pkg/renderutil"
    38  	"k8s.io/helm/pkg/tiller"
    39  	"k8s.io/helm/pkg/timeconv"
    40  )
    41  
    42  const defaultDirectoryPermission = 0755
    43  
    44  var (
    45  	whitespaceRegex = regexp.MustCompile(`^\s*$`)
    46  
    47  	// defaultKubeVersion is the default value of --kube-version flag
    48  	defaultKubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor)
    49  )
    50  
    51  const templateDesc = `
    52  Render chart templates locally and display the output.
    53  
    54  This does not require Tiller. However, any values that would normally be
    55  looked up or retrieved in-cluster will be faked locally. Additionally, none
    56  of the server-side testing of chart validity (e.g. whether an API is supported)
    57  is done.
    58  
    59  To render just one template in a chart, use '-x':
    60  
    61  	$ helm template mychart -x templates/deployment.yaml
    62  `
    63  
    64  type templateCmd struct {
    65  	namespace        string
    66  	valueFiles       valueFiles
    67  	chartPath        string
    68  	out              io.Writer
    69  	values           []string
    70  	stringValues     []string
    71  	fileValues       []string
    72  	nameTemplate     string
    73  	showNotes        bool
    74  	releaseName      string
    75  	releaseIsUpgrade bool
    76  	renderFiles      []string
    77  	kubeVersion      string
    78  	apiVersions      []string
    79  	outputDir        string
    80  }
    81  
    82  func newTemplateCmd(out io.Writer) *cobra.Command {
    83  
    84  	t := &templateCmd{
    85  		out: out,
    86  	}
    87  
    88  	cmd := &cobra.Command{
    89  		Use:   "template [flags] CHART",
    90  		Short: "Locally render templates",
    91  		Long:  templateDesc,
    92  		RunE:  t.run,
    93  	}
    94  
    95  	cmd.SetOutput(out)
    96  	f := cmd.Flags()
    97  	f.BoolVar(&t.showNotes, "notes", false, "Show the computed NOTES.txt file as well")
    98  	f.StringVarP(&t.releaseName, "name", "n", "release-name", "Release name")
    99  	f.BoolVar(&t.releaseIsUpgrade, "is-upgrade", false, "Set .Release.IsUpgrade instead of .Release.IsInstall")
   100  	f.StringArrayVarP(&t.renderFiles, "execute", "x", []string{}, "Only execute the given templates")
   101  	f.VarP(&t.valueFiles, "values", "f", "Specify values in a YAML file (can specify multiple)")
   102  	f.StringVar(&t.namespace, "namespace", "", "Namespace to install the release into")
   103  	f.StringArrayVar(&t.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   104  	f.StringArrayVar(&t.stringValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   105  	f.StringArrayVar(&t.fileValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
   106  	f.StringVar(&t.nameTemplate, "name-template", "", "Specify template used to name the release")
   107  	f.StringVar(&t.kubeVersion, "kube-version", defaultKubeVersion, "Kubernetes version used as Capabilities.KubeVersion.Major/Minor")
   108  	f.StringArrayVarP(&t.apiVersions, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
   109  	f.StringVar(&t.outputDir, "output-dir", "", "Writes the executed templates to files in output-dir instead of stdout")
   110  
   111  	return cmd
   112  }
   113  
   114  func (t *templateCmd) run(cmd *cobra.Command, args []string) error {
   115  	if len(args) < 1 {
   116  		return errors.New("chart is required")
   117  	}
   118  	// verify chart path exists
   119  	if _, err := os.Stat(args[0]); err == nil {
   120  		if t.chartPath, err = filepath.Abs(args[0]); err != nil {
   121  			return err
   122  		}
   123  	} else {
   124  		return err
   125  	}
   126  
   127  	// verify that output-dir exists if provided
   128  	if t.outputDir != "" {
   129  		_, err := os.Stat(t.outputDir)
   130  		if os.IsNotExist(err) {
   131  			return fmt.Errorf("output-dir '%s' does not exist", t.outputDir)
   132  		}
   133  	}
   134  
   135  	if t.namespace == "" {
   136  		t.namespace = defaultNamespace()
   137  	}
   138  	// get combined values and create config
   139  	rawVals, err := vals(t.valueFiles, t.values, t.stringValues, t.fileValues, "", "", "")
   140  	if err != nil {
   141  		return err
   142  	}
   143  	config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}}
   144  
   145  	// If template is specified, try to run the template.
   146  	if t.nameTemplate != "" {
   147  		t.releaseName, err = generateName(t.nameTemplate)
   148  		if err != nil {
   149  			return err
   150  		}
   151  	}
   152  
   153  	if msgs := validation.IsDNS1123Subdomain(t.releaseName); t.releaseName != "" && len(msgs) > 0 {
   154  		return fmt.Errorf("release name %s is invalid: %s", t.releaseName, strings.Join(msgs, ";"))
   155  	}
   156  
   157  	// Check chart requirements to make sure all dependencies are present in /charts
   158  	c, err := chartutil.Load(t.chartPath)
   159  	if err != nil {
   160  		return prettyError(err)
   161  	}
   162  
   163  	renderOpts := renderutil.Options{
   164  		ReleaseOptions: chartutil.ReleaseOptions{
   165  			Name:      t.releaseName,
   166  			IsInstall: !t.releaseIsUpgrade,
   167  			IsUpgrade: t.releaseIsUpgrade,
   168  			Time:      timeconv.Now(),
   169  			Namespace: t.namespace,
   170  		},
   171  		KubeVersion: t.kubeVersion,
   172  		APIVersions: t.apiVersions,
   173  	}
   174  
   175  	renderedTemplates, err := renderutil.Render(c, config, renderOpts)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	if settings.Debug {
   181  		rel := &release.Release{
   182  			Name:      t.releaseName,
   183  			Chart:     c,
   184  			Config:    config,
   185  			Version:   1,
   186  			Namespace: t.namespace,
   187  			Info:      &release.Info{LastDeployed: timeconv.Timestamp(time.Now())},
   188  		}
   189  		printRelease(os.Stdout, rel)
   190  	}
   191  
   192  	listManifests := manifest.SplitManifests(renderedTemplates)
   193  	var manifestsToRender []manifest.Manifest
   194  
   195  	// if we have a list of files to render, then check that each of the
   196  	// provided files exists in the chart.
   197  	if len(t.renderFiles) > 0 {
   198  		for _, f := range t.renderFiles {
   199  			missing := true
   200  			if !filepath.IsAbs(f) {
   201  				newF, err := filepath.Abs(filepath.Join(t.chartPath, f))
   202  				if err != nil {
   203  					return fmt.Errorf("could not turn template path %s into absolute path: %s", f, err)
   204  				}
   205  				f = newF
   206  			}
   207  
   208  			for _, manifest := range listManifests {
   209  				// manifest.Name is rendered using linux-style filepath separators on Windows as
   210  				// well as macOS/linux.
   211  				manifestPathSplit := strings.Split(manifest.Name, "/")
   212  				// remove the chart name from the path
   213  				manifestPathSplit = manifestPathSplit[1:]
   214  				toJoin := append([]string{t.chartPath}, manifestPathSplit...)
   215  				manifestPath := filepath.Join(toJoin...)
   216  
   217  				// if the filepath provided matches a manifest path in the
   218  				// chart, render that manifest
   219  				if f == manifestPath {
   220  					manifestsToRender = append(manifestsToRender, manifest)
   221  					missing = false
   222  				}
   223  			}
   224  			if missing {
   225  				return fmt.Errorf("could not find template %s in chart", f)
   226  			}
   227  		}
   228  	} else {
   229  		// no renderFiles provided, render all manifests in the chart
   230  		manifestsToRender = listManifests
   231  	}
   232  
   233  	for _, m := range tiller.SortByKind(manifestsToRender) {
   234  		data := m.Content
   235  		b := filepath.Base(m.Name)
   236  		if !t.showNotes && b == "NOTES.txt" {
   237  			continue
   238  		}
   239  		if strings.HasPrefix(b, "_") {
   240  			continue
   241  		}
   242  
   243  		if t.outputDir != "" {
   244  			// blank template after execution
   245  			if whitespaceRegex.MatchString(data) {
   246  				continue
   247  			}
   248  			err = writeToFile(t.outputDir, m.Name, data, t.out)
   249  			if err != nil {
   250  				return err
   251  			}
   252  			continue
   253  		}
   254  		fmt.Fprintf(t.out, "---\n# Source: %s\n", m.Name)
   255  		fmt.Fprintln(t.out, data)
   256  	}
   257  	return nil
   258  }
   259  
   260  // write the <data> to <output-dir>/<name>
   261  func writeToFile(outputDir string, name string, data string, out io.Writer) error {
   262  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   263  
   264  	err := ensureDirectoryForFile(outfileName)
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	f, err := os.Create(outfileName)
   270  	if err != nil {
   271  		return err
   272  	}
   273  
   274  	defer f.Close()
   275  
   276  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s", name, data))
   277  
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	fmt.Fprintf(out, "wrote %s\n", outfileName)
   283  	return nil
   284  }
   285  
   286  // check if the directory exists to create file. creates if don't exists
   287  func ensureDirectoryForFile(file string) error {
   288  	baseDir := path.Dir(file)
   289  	_, err := os.Stat(baseDir)
   290  	if err != nil && !os.IsNotExist(err) {
   291  		return err
   292  	}
   293  
   294  	return os.MkdirAll(baseDir, defaultDirectoryPermission)
   295  }