2017-08-08 14:38:38 -04:00
/ *
2018-08-24 15:03:55 -04:00
Copyright The Helm Authors .
2017-08-08 14:38:38 -04:00
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 .
* /
2025-02-24 10:11:54 -05:00
package cmd
2017-08-08 14:38:38 -04:00
import (
2019-10-10 09:24:06 -04:00
"bytes"
2024-11-18 12:54:09 -05:00
"errors"
2017-08-08 14:38:38 -04:00
"fmt"
"io"
2024-11-18 12:54:09 -05:00
"io/fs"
2020-07-28 09:52:39 -04:00
"os"
2019-10-10 09:24:06 -04:00
"path/filepath"
"regexp"
2025-01-06 03:20:22 -05:00
"slices"
2020-04-02 17:09:45 -04:00
"sort"
2017-08-08 14:38:38 -04:00
"strings"
2025-02-26 09:04:32 -05:00
release "helm.sh/helm/v4/pkg/release/v1"
2020-10-14 10:25:42 -04:00
2017-08-08 14:38:38 -04:00
"github.com/spf13/cobra"
2018-01-05 15:30:26 -05:00
2024-12-26 16:33:51 -05:00
"helm.sh/helm/v4/pkg/action"
2025-09-01 17:46:14 -04:00
"helm.sh/helm/v4/pkg/chart/common"
2024-12-26 16:33:51 -05:00
"helm.sh/helm/v4/pkg/cli/values"
2025-02-24 10:11:54 -05:00
"helm.sh/helm/v4/pkg/cmd/require"
2025-08-31 08:48:15 -04:00
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
2017-12-24 09:08:49 -05:00
)
2017-11-21 14:10:27 -05:00
2017-08-08 14:38:38 -04:00
const templateDesc = `
Render chart templates locally and display the output .
2019-08-07 14:08:08 -04:00
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 .
2017-08-08 14:38:38 -04:00
`
2019-07-19 15:27:14 -04:00
func newTemplateCmd ( cfg * action . Configuration , out io . Writer ) * cobra . Command {
var validate bool
2019-12-03 14:06:55 -05:00
var includeCrds bool
2020-09-08 05:16:16 -04:00
var skipTests bool
2019-07-19 15:27:14 -04:00
client := action . NewInstall ( cfg )
2019-08-01 17:40:52 -04:00
valueOpts := & values . Options { }
2020-11-20 04:32:29 -05:00
var kubeVersion string
2019-10-07 15:23:42 -04:00
var extraAPIs [ ] string
2019-10-11 02:05:27 -04:00
var showFiles [ ] string
2017-08-08 14:38:38 -04:00
cmd := & cobra . Command {
2019-03-19 01:56:18 -04:00
Use : "template [NAME] [CHART]" ,
2020-02-26 07:24:17 -05:00
Short : "locally render templates" ,
2017-08-08 14:38:38 -04:00
Long : templateDesc ,
2019-03-19 01:56:18 -04:00
Args : require . MinimumNArgs ( 1 ) ,
2024-03-11 17:13:34 -04:00
ValidArgsFunction : func ( _ * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
2020-04-11 14:06:56 -04:00
return compInstall ( args , toComplete , client )
} ,
2025-05-04 15:41:40 -04:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-11-20 04:32:29 -05:00
if kubeVersion != "" {
2025-09-01 17:46:14 -04:00
parsedKubeVersion , err := common . ParseKubeVersion ( kubeVersion )
2020-11-20 04:32:29 -05:00
if err != nil {
return fmt . Errorf ( "invalid kube version '%s': %s" , kubeVersion , err )
}
client . KubeVersion = parsedKubeVersion
}
2023-06-07 02:24:02 -04:00
registryClient , err := newRegistryClient ( client . CertFile , client . KeyFile , client . CaFile ,
2024-02-05 08:54:21 -05:00
client . InsecureSkipTLSverify , client . PlainHTTP , client . Username , client . Password )
2022-12-19 16:52:20 -05:00
if err != nil {
return fmt . Errorf ( "missing registry client: %w" , err )
}
client . SetRegistryClient ( registryClient )
2025-05-04 15:41:40 -04:00
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
2023-04-08 19:43:01 -04:00
}
2025-05-04 15:41:40 -04:00
client . DryRunStrategy = dryRunStrategy
2021-03-25 00:09:28 -04:00
client . ReleaseName = "release-name"
2019-02-08 19:02:57 -05:00
client . Replace = true // Skip the name check
2025-09-01 17:46:14 -04:00
client . APIVersions = common . VersionSet ( extraAPIs )
2020-01-20 16:31:26 -05:00
client . IncludeCRDs = includeCrds
2019-08-01 11:04:36 -04:00
rel , err := runInstall ( args , client , valueOpts , out )
2019-10-10 09:24:06 -04:00
2020-02-12 14:28:46 -05:00
if err != nil && ! settings . Debug {
if rel != nil {
return fmt . Errorf ( "%w\n\nUse --debug flag to render out invalid YAML" , err )
}
2018-05-03 02:35:36 -04:00
return err
}
2019-10-10 09:24:06 -04:00
2020-02-12 14:28:46 -05:00
// 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.
2020-02-07 13:52:58 -05:00
if rel != nil {
var manifests bytes . Buffer
fmt . Fprintln ( & manifests , strings . TrimSpace ( rel . Manifest ) )
2020-07-28 09:52:39 -04:00
if ! client . DisableHooks {
fileWritten := make ( map [ string ] bool )
for _ , m := range rel . Hooks {
2020-10-14 10:25:42 -04:00
if skipTests && isTestHook ( m ) {
continue
}
2020-07-28 09:52:39 -04:00
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 )
}
2022-10-29 10:05:17 -04:00
_ , err := os . Stat ( filepath . Join ( newDir , m . Path ) )
if err == nil {
fileWritten [ m . Path ] = true
}
2020-07-28 09:52:39 -04:00
err = writeToFile ( newDir , m . Path , m . Manifest , fileWritten [ m . Path ] )
if err != nil {
return err
}
}
}
}
2019-12-03 14:06:55 -05:00
2020-02-07 13:52:58 -05:00
// 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 {
2020-04-02 17:09:45 -04:00
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
2020-02-07 13:52:58 -05:00
splitManifests := releaseutil . SplitManifests ( manifests . String ( ) )
2020-04-02 17:09:45 -04:00
manifestsKeys := make ( [ ] string , 0 , len ( splitManifests ) )
for k := range splitManifests {
manifestsKeys = append ( manifestsKeys , k )
}
sort . Sort ( releaseutil . BySplitManifestsOrder ( manifestsKeys ) )
2020-02-07 13:52:58 -05:00
manifestNameRegex := regexp . MustCompile ( "# Source: [^/]+/(.+)" )
var manifestsToRender [ ] string
for _ , f := range showFiles {
missing := true
2020-06-09 01:57:43 -04:00
// Use linux-style filepath separators to unify user's input path
f = filepath . ToSlash ( f )
2020-04-02 17:09:45 -04:00
for _ , manifestKey := range manifestsKeys {
manifest := splitManifests [ manifestKey ]
2020-02-07 13:52:58 -05:00
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 , "/" )
2020-06-09 01:57:43 -04:00
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings . Join ( manifestPathSplit , "/" )
2020-02-07 13:52:58 -05:00
// if the filepath provided matches a manifest path in the
// chart, render that manifest
2020-04-02 17:09:45 -04:00
if matched , _ := filepath . Match ( f , manifestPath ) ; ! matched {
continue
2020-02-07 13:52:58 -05:00
}
2020-04-02 17:09:45 -04:00
manifestsToRender = append ( manifestsToRender , manifest )
missing = false
2019-10-10 09:24:06 -04:00
}
2020-02-07 13:52:58 -05:00
if missing {
return fmt . Errorf ( "could not find template %s in chart" , f )
2019-10-10 09:24:06 -04:00
}
}
2020-02-07 13:52:58 -05:00
for _ , m := range manifestsToRender {
fmt . Fprintf ( out , "---\n%s\n" , m )
2019-10-10 09:24:06 -04:00
}
2020-02-07 13:52:58 -05:00
} else {
fmt . Fprintf ( out , "%s" , manifests . String ( ) )
2020-01-30 06:24:09 -05:00
}
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
}
2020-02-07 13:52:58 -05:00
return err
2018-05-03 02:35:36 -04:00
} ,
2017-08-08 14:38:38 -04:00
}
2019-07-19 15:27:14 -04:00
f := cmd . Flags ( )
2020-06-12 08:24:55 -04:00
addInstallFlags ( cmd , f , client , valueOpts )
2019-10-11 02:05:27 -04:00
f . StringArrayVarP ( & showFiles , "show-only" , "s" , [ ] string { } , "only show manifests rendered from the given templates" )
2019-07-19 15:27:14 -04:00
f . StringVar ( & client . OutputDir , "output-dir" , "" , "writes the executed templates to files in output-dir instead of stdout" )
2025-05-04 15:41:40 -04:00
f . BoolVar ( & validate , "validate" , false , "deprecated" )
f . MarkDeprecated ( "validate" , "use '--dry-run=server' instead" )
2019-12-03 14:06:55 -05:00
f . BoolVar ( & includeCrds , "include-crds" , false , "include CRDs in the templated output" )
2020-10-14 10:25:42 -04:00
f . BoolVar ( & skipTests , "skip-tests" , false , "skip tests from templated output" )
2019-11-22 10:27:08 -05:00
f . BoolVar ( & client . IsUpgrade , "is-upgrade" , false , "set .Release.IsUpgrade instead of .Release.IsInstall" )
2020-11-20 04:32:29 -05:00
f . StringVar ( & kubeVersion , "kube-version" , "" , "Kubernetes version used for Capabilities.KubeVersion" )
2025-06-09 02:11:05 -04:00
f . StringSliceVarP ( & extraAPIs , "api-versions" , "a" , [ ] string { } , "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)" )
2020-02-04 11:27:38 -05:00
f . BoolVar ( & client . UseReleaseName , "release-name" , false , "use release name in the output-dir path." )
2025-05-04 15:41:40 -04:00
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"
2025-08-20 17:17:16 -04:00
bindPostRenderFlag ( cmd , & client . PostRenderer , settings )
2025-05-04 15:41:40 -04:00
cmd . MarkFlagsMutuallyExclusive ( "validate" , "dry-run" )
2017-08-08 14:38:38 -04:00
return cmd
}
2020-07-28 09:52:39 -04:00
2020-10-14 10:25:42 -04:00
func isTestHook ( h * release . Hook ) bool {
2025-01-06 03:20:22 -05:00
return slices . Contains ( h . Events , release . HookTest )
2020-10-14 10:25:42 -04:00
}
2020-07-28 09:52:39 -04:00
// The following functions (writeToFile, createOrOpenFile, and ensureDirectoryForFile)
2021-03-15 21:11:57 -04:00
// are copied from the actions package. This is part of a change to correct a
2020-07-28 09:52:39 -04:00
// 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.
2024-12-10 12:40:37 -05:00
func writeToFile ( outputDir string , name string , data string , appendData bool ) error {
2020-07-28 09:52:39 -04:00
outfileName := strings . Join ( [ ] string { outputDir , name } , string ( filepath . Separator ) )
err := ensureDirectoryForFile ( outfileName )
if err != nil {
return err
}
2024-12-10 12:40:37 -05:00
f , err := createOrOpenFile ( outfileName , appendData )
2020-07-28 09:52:39 -04:00
if err != nil {
return err
}
defer f . Close ( )
2025-04-14 04:49:49 -04:00
_ , err = fmt . Fprintf ( f , "---\n# Source: %s\n%s\n" , name , data )
2020-07-28 09:52:39 -04:00
if err != nil {
return err
}
fmt . Printf ( "wrote %s\n" , outfileName )
return nil
}
2024-12-10 12:40:37 -05:00
func createOrOpenFile ( filename string , appendData bool ) ( * os . File , error ) {
if appendData {
2020-07-28 09:52:39 -04:00
return os . OpenFile ( filename , os . O_APPEND | os . O_WRONLY , 0600 )
}
return os . Create ( filename )
}
func ensureDirectoryForFile ( file string ) error {
2025-09-02 23:20:09 -04:00
baseDir := filepath . Dir ( file )
2020-07-28 09:52:39 -04:00
_ , err := os . Stat ( baseDir )
2024-11-18 12:54:09 -05:00
if err != nil && ! errors . Is ( err , fs . ErrNotExist ) {
2020-07-28 09:52:39 -04:00
return err
}
return os . MkdirAll ( baseDir , 0755 )
}