helm/pkg/cmd/template.go

268 lines
8.9 KiB
Go
Raw Normal View History

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
release "helm.sh/helm/v4/pkg/release/v1"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
const templateDesc = `
Render chart templates locally and display the output.
Any values that would normally be looked up or retrieved in-cluster will be
faked locally. Additionally, none of the server-side testing of chart validity
(e.g. whether an API is supported) is done.
`
func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var validate bool
var includeCrds bool
var skipTests bool
client := action.NewInstall(cfg)
valueOpts := &values.Options{}
var kubeVersion string
var extraAPIs []string
var showFiles []string
cmd := &cobra.Command{
Use: "template [NAME] [CHART]",
Short: "locally render templates",
Long: templateDesc,
Args: require.MinimumNArgs(1),
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(cmd *cobra.Command, args []string) error {
if kubeVersion != "" {
parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true)
if err != nil {
return err
}
if validate {
// Mimic deprecated --validate flag behavior by enabling server dry run
dryRunStrategy = action.DryRunServer
}
client.DryRunStrategy = dryRunStrategy
client.ReleaseName = "release-name"
client.Replace = true // Skip the name check
client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)
if err != nil && !settings.Debug {
if rel != nil {
return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err)
}
return err
}
// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
if err == nil {
fileWritten[m.Path] = true
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
}
}
}
}
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
}
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
fix(helm3): `helm template` output should include hooks by default This fixes `helm template [--no-hooks]` to work like proposed in #6443 Manually tested with running `helm template` against `stable/mysql` chart with helm 2, helm 3.0.0-beta.3, and helm 3 after this fix. The manual test report follows. Assume `helmv3` is 3.0.0-beta.3 and `helmv3-nohooksfix` is the binary build from this PR. ``` $ helmv3 fetch stable/mysql $ helmv3 template mysql-1.3.1.tgz > helm-template-mysql-helm-3.yaml $ helmv3-nohooksfix template mysql-1.3.1.tgz > helm-template-mysql-helm-3-with-fix.yaml $ helmv3-nohooksfix template --no-hooks mysql-1.3.1.tgz > helm-template-mysql-helm-3-with-fix-nohooks-enabled.yaml ``` The example below shows that this fix changes `helm template` to output hooks by default: ``` $ diff --unified helm-template-mysql-helm-3{,-with-fix}.yaml --- helm-template-mysql-helm-3.yaml 2019-09-17 22:21:38.000000000 +0900 +++ helm-template-mysql-helm-3-with-fix.yaml 2019-09-17 22:21:53.000000000 +0900 @@ -13,10 +13,10 @@ type: Opaque data: - mysql-root-password: "VGtybWh5N3JnWA==" + mysql-root-password: "aGpHN2VEbnhvVA==" - mysql-password: "OTNQSXdNVURBYw==" + mysql-password: "UmpwQkVuMHpoQQ==" --- # Source: mysql/templates/tests/test-configmap.yaml apiVersion: v1 @@ -167,3 +167,48 @@ claimName: RELEASE-NAME-mysql # - name: extras # emptyDir: {} +--- +# Source: mysql/templates/tests/test.yaml +apiVersion: v1 +kind: Pod +metadata: + name: RELEASE-NAME-mysql-test + namespace: default + labels: + app: RELEASE-NAME-mysql + chart: "mysql-1.3.1" + heritage: "Helm" + release: "RELEASE-NAME" + annotations: + "helm.sh/hook": test-success +spec: + initContainers: + - name: test-framework + image: "dduportal/bats:0.4.0" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: RELEASE-NAME-test + image: "mysql:5.7.14" + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: RELEASE-NAME-mysql-test + - name: tools + emptyDir: {} + restartPolicy: Never ``` The example below shows that `helm template --no-hooks` can be used for excluding hooks: ``` $ diff --unified helm-template-mysql-helm-3{,-with-fix-nohooks-enabled}.yaml --- helm-template-mysql-helm-3.yaml 2019-09-17 22:21:38.000000000 +0900 +++ helm-template-mysql-helm-3-with-fix-nohooks-enabled.yaml 2019-09-17 22:22:03.000000000 +0900 @@ -13,10 +13,10 @@ type: Opaque data: - mysql-root-password: "VGtybWh5N3JnWA==" + mysql-root-password: "Zk1LYUd6OWgzaQ==" - mysql-password: "OTNQSXdNVURBYw==" + mysql-password: "OTZPZU9hdlFORg==" --- # Source: mysql/templates/tests/test-configmap.yaml apiVersion: v1 ``` Fixes #6443 Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>
2019-09-17 09:20:49 -04:00
}
return err
},
}
f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "deprecated")
f.MarkDeprecated("validate", "use '--dry-run=server' instead")
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
f.String(
"dry-run",
"client",
`simulates the operation either client-side or server-side. Must be either: "client", or "server". '--dry-run=client simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates/validates the operation on the server, requiring cluster connectivity.`)
f.Lookup("dry-run").NoOptDefVal = "unset"
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
cmd.MarkFlagsMutuallyExclusive("validate", "dry-run")
return cmd
}
func isTestHook(h *release.Hook) bool {
return slices.Contains(h.Events, release.HookTest)
}
// The following functions (writeToFile, createOrOpenFile, and ensureDirectoryForFile)
chore: Spelling (#9410) * spelling: annotate Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: asserts Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: behavior Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: binary Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: contain Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: copied Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: dependency Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: depending Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: deprecated Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: doesn't Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: donot Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: github Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: inputting Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: iteration Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: jabberwocky Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: kubernetes Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: length Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: mismatch Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: multiple Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: nonexistent Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: outputs Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: panicking Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: plugins Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: parsing Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: porthos Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: regular Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: resource Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: repositories Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: something Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: strict Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: string Signed-off-by: Josh Soref <jsoref@users.noreply.github.com> * spelling: unknown Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2021-03-15 21:11:57 -04:00
// are copied from the actions package. This is part of a change to correct a
// bug introduced by #8156. As part of the todo to refactor renderResources
// this duplicate code should be removed. It is added here so that the API
// surface area is as minimally impacted as possible in fixing the issue.
func writeToFile(outputDir string, name string, data string, appendData bool) error {
outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
err := ensureDirectoryForFile(outfileName)
if err != nil {
return err
}
f, err := createOrOpenFile(outfileName, appendData)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, data)
if err != nil {
return err
}
fmt.Printf("wrote %s\n", outfileName)
return nil
}
func createOrOpenFile(filename string, appendData bool) (*os.File, error) {
if appendData {
return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
}
return os.Create(filename)
}
func ensureDirectoryForFile(file string) error {
baseDir := filepath.Dir(file)
_, err := os.Stat(baseDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
return os.MkdirAll(baseDir, 0755)
}