github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"regexp"
    27  	"sort"
    28  	"strings"
    29  
    30  	"github.com/stefanmcshane/helm/pkg/release"
    31  
    32  	"github.com/spf13/cobra"
    33  
    34  	"github.com/stefanmcshane/helm/cmd/helm/require"
    35  	"github.com/stefanmcshane/helm/pkg/action"
    36  	"github.com/stefanmcshane/helm/pkg/chartutil"
    37  	"github.com/stefanmcshane/helm/pkg/cli/values"
    38  	"github.com/stefanmcshane/helm/pkg/releaseutil"
    39  )
    40  
    41  const templateDesc = `
    42  Render chart templates locally and display the output.
    43  
    44  Any values that would normally be looked up or retrieved in-cluster will be
    45  faked locally. Additionally, none of the server-side testing of chart validity
    46  (e.g. whether an API is supported) is done.
    47  `
    48  
    49  func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
    50  	var validate bool
    51  	var includeCrds bool
    52  	var skipTests bool
    53  	client := action.NewInstall(cfg)
    54  	valueOpts := &values.Options{}
    55  	var kubeVersion string
    56  	var extraAPIs []string
    57  	var showFiles []string
    58  
    59  	cmd := &cobra.Command{
    60  		Use:   "template [NAME] [CHART]",
    61  		Short: "locally render templates",
    62  		Long:  templateDesc,
    63  		Args:  require.MinimumNArgs(1),
    64  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    65  			return compInstall(args, toComplete, client)
    66  		},
    67  		RunE: func(_ *cobra.Command, args []string) error {
    68  			if kubeVersion != "" {
    69  				parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
    70  				if err != nil {
    71  					return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
    72  				}
    73  				client.KubeVersion = parsedKubeVersion
    74  			}
    75  
    76  			client.DryRun = true
    77  			client.ReleaseName = "release-name"
    78  			client.Replace = true // Skip the name check
    79  			client.ClientOnly = !validate
    80  			client.APIVersions = chartutil.VersionSet(extraAPIs)
    81  			client.IncludeCRDs = includeCrds
    82  			rel, err := runInstall(args, client, valueOpts, out)
    83  
    84  			if err != nil && !settings.Debug {
    85  				if rel != nil {
    86  					return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err)
    87  				}
    88  				return err
    89  			}
    90  
    91  			// We ignore a potential error here because, when the --debug flag was specified,
    92  			// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
    93  			if rel != nil {
    94  				var manifests bytes.Buffer
    95  				fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
    96  				if !client.DisableHooks {
    97  					fileWritten := make(map[string]bool)
    98  					for _, m := range rel.Hooks {
    99  						if skipTests && isTestHook(m) {
   100  							continue
   101  						}
   102  						if client.OutputDir == "" {
   103  							fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
   104  						} else {
   105  							newDir := client.OutputDir
   106  							if client.UseReleaseName {
   107  								newDir = filepath.Join(client.OutputDir, client.ReleaseName)
   108  							}
   109  							err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
   110  							if err != nil {
   111  								return err
   112  							}
   113  							fileWritten[m.Path] = true
   114  						}
   115  
   116  					}
   117  				}
   118  
   119  				// if we have a list of files to render, then check that each of the
   120  				// provided files exists in the chart.
   121  				if len(showFiles) > 0 {
   122  					// This is necessary to ensure consistent manifest ordering when using --show-only
   123  					// with globs or directory names.
   124  					splitManifests := releaseutil.SplitManifests(manifests.String())
   125  					manifestsKeys := make([]string, 0, len(splitManifests))
   126  					for k := range splitManifests {
   127  						manifestsKeys = append(manifestsKeys, k)
   128  					}
   129  					sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
   130  
   131  					manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
   132  					var manifestsToRender []string
   133  					for _, f := range showFiles {
   134  						missing := true
   135  						// Use linux-style filepath separators to unify user's input path
   136  						f = filepath.ToSlash(f)
   137  						for _, manifestKey := range manifestsKeys {
   138  							manifest := splitManifests[manifestKey]
   139  							submatch := manifestNameRegex.FindStringSubmatch(manifest)
   140  							if len(submatch) == 0 {
   141  								continue
   142  							}
   143  							manifestName := submatch[1]
   144  							// manifest.Name is rendered using linux-style filepath separators on Windows as
   145  							// well as macOS/linux.
   146  							manifestPathSplit := strings.Split(manifestName, "/")
   147  							// manifest.Path is connected using linux-style filepath separators on Windows as
   148  							// well as macOS/linux
   149  							manifestPath := strings.Join(manifestPathSplit, "/")
   150  
   151  							// if the filepath provided matches a manifest path in the
   152  							// chart, render that manifest
   153  							if matched, _ := filepath.Match(f, manifestPath); !matched {
   154  								continue
   155  							}
   156  							manifestsToRender = append(manifestsToRender, manifest)
   157  							missing = false
   158  						}
   159  						if missing {
   160  							return fmt.Errorf("could not find template %s in chart", f)
   161  						}
   162  					}
   163  					for _, m := range manifestsToRender {
   164  						fmt.Fprintf(out, "---\n%s\n", m)
   165  					}
   166  				} else {
   167  					fmt.Fprintf(out, "%s", manifests.String())
   168  				}
   169  			}
   170  
   171  			return err
   172  		},
   173  	}
   174  
   175  	f := cmd.Flags()
   176  	addInstallFlags(cmd, f, client, valueOpts)
   177  	f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
   178  	f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
   179  	f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
   180  	f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
   181  	f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
   182  	f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
   183  	f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
   184  	f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
   185  	f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
   186  	bindPostRenderFlag(cmd, &client.PostRenderer)
   187  
   188  	return cmd
   189  }
   190  
   191  func isTestHook(h *release.Hook) bool {
   192  	for _, e := range h.Events {
   193  		if e == release.HookTest {
   194  			return true
   195  		}
   196  	}
   197  	return false
   198  }
   199  
   200  // The following functions (writeToFile, createOrOpenFile, and ensureDirectoryForFile)
   201  // are copied from the actions package. This is part of a change to correct a
   202  // bug introduced by #8156. As part of the todo to refactor renderResources
   203  // this duplicate code should be removed. It is added here so that the API
   204  // surface area is as minimally impacted as possible in fixing the issue.
   205  func writeToFile(outputDir string, name string, data string, append bool) error {
   206  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   207  
   208  	err := ensureDirectoryForFile(outfileName)
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	f, err := createOrOpenFile(outfileName, append)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	defer f.Close()
   219  
   220  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
   221  
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	fmt.Printf("wrote %s\n", outfileName)
   227  	return nil
   228  }
   229  
   230  func createOrOpenFile(filename string, append bool) (*os.File, error) {
   231  	if append {
   232  		return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
   233  	}
   234  	return os.Create(filename)
   235  }
   236  
   237  func ensureDirectoryForFile(file string) error {
   238  	baseDir := path.Dir(file)
   239  	_, err := os.Stat(baseDir)
   240  	if err != nil && !os.IsNotExist(err) {
   241  		return err
   242  	}
   243  
   244  	return os.MkdirAll(baseDir, 0755)
   245  }