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 }