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