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 }