├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── glide.lock ├── glide.yaml ├── install-binary.sh ├── main.go └── plugin.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | _scratch 2 | tpl 3 | vendor/ 4 | _dist/ 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Helm Template Plugin 2 | Copyright (C) 2016, Matt Butcher 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HELM_HOME ?= $(shell helm home) 2 | HELM_PLUGIN_DIR ?= $(HELM_HOME)/plugins/helm-template 3 | HAS_GLIDE := $(shell command -v glide;) 4 | VERSION := $(shell sed -n -e 's/version:[ "]*\([^"]*\).*/\1/p' plugin.yaml) 5 | DIST := $(CURDIR)/_dist 6 | LDFLAGS := "-X main.version=${VERSION}" 7 | 8 | .PHONY: install 9 | install: bootstrap build 10 | cp tpl $(HELM_PLUGIN_DIR) 11 | cp plugin.yaml $(HELM_PLUGIN_DIR) 12 | 13 | .PHONY: hookInstall 14 | hookInstall: bootstrap build 15 | 16 | .PHONY: build 17 | build: 18 | go build -o tpl -ldflags $(LDFLAGS) ./main.go 19 | 20 | .PHONY: dist 21 | dist: 22 | mkdir -p $(DIST) 23 | GOOS=linux GOARCH=amd64 go build -o tpl -ldflags $(LDFLAGS) ./main.go 24 | tar -zcvf $(DIST)/helm-template-linux-$(VERSION).tgz tpl README.md LICENSE.txt plugin.yaml 25 | GOOS=darwin GOARCH=amd64 go build -o tpl -ldflags $(LDFLAGS) ./main.go 26 | tar -zcvf $(DIST)/helm-template-macos-$(VERSION).tgz tpl README.md LICENSE.txt plugin.yaml 27 | GOOS=windows GOARCH=amd64 go build -o tpl.exe -ldflags $(LDFLAGS) ./main.go 28 | tar -zcvf $(DIST)/helm-template-windows-$(VERSION).tgz tpl.exe README.md LICENSE.txt plugin.yaml 29 | 30 | .PHONY: bootstrap 31 | bootstrap: 32 | ifndef HAS_GLIDE 33 | go get -u github.com/Masterminds/glide 34 | endif 35 | glide install --strip-vendor 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **If you are using a recent version of Helm, you do not need this anymore!** 2 | 3 | `helm template` is now a built-in part of Helm. Just run `helm template --help` with your existing Helm. 4 | 5 | ---- 6 | 7 | # Helm Template Plugin 8 | 9 | This is a Helm plugin to help chart developers debug their charts. It works like 10 | `helm install --dry-run --debug`, except that it runs locally, has more output 11 | options, and is quite a bit faster. 12 | 13 | 14 | 15 | ## Usage 16 | 17 | Render chart templates locally and display the output. 18 | 19 | This does not require Tiller. However, any values that would normally be 20 | looked up or retrieved in-cluster will be faked locally. Additionally, none 21 | of the server-side testing of chart validity (e.g. whether an API is supported) 22 | is done. 23 | 24 | ``` 25 | $ helm template [flags] CHART 26 | ``` 27 | 28 | ### Flags: 29 | 30 | ``` 31 | --notes show the computed NOTES.txt file as well. 32 | --set string set values on the command line. See 'helm install -h' 33 | -f, --values valueFiles specify one or more YAML files of values (default []) 34 | -v, --verbose show the computed YAML values as well. 35 | ``` 36 | 37 | 38 | ## Install 39 | 40 | ``` 41 | $ helm plugin install https://github.com/technosophos/helm-template 42 | ``` 43 | 44 | The above will fetch the latest binary release of `helm template` and install it. 45 | 46 | ### Developer (From Source) Install 47 | 48 | If you would like to handle the build yourself, instead of fetching a binary, 49 | this is how recommend doing it. 50 | 51 | First, set up your environment: 52 | 53 | - You need to have [Go](http://golang.org) installed. Make sure to set `$GOPATH` 54 | - If you don't have [Glide](http://glide.sh) installed, this will install it into 55 | `$GOPATH/bin` for you. 56 | 57 | Clone this repo into your `$GOPATH`. You can use `go get -d github.com/technosophos/helm-template` 58 | for that. 59 | 60 | ``` 61 | $ cd $GOPATH/src/github.com/technosophos/helm-template 62 | $ make bootstrap build 63 | $ SKIP_BIN_INSTALL=1 helm plugin install $GOPATH/src/github.com/technosophos/helm-template 64 | ``` 65 | 66 | That last command will skip fetching the binary install and use the one you 67 | built. 68 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 0b1e82c6e57deeeb877eee55cd3def35a5d95a89b410873960e53483b49b8922 2 | updated: 2017-07-31T09:30:02.493522293-06:00 3 | imports: 4 | - name: github.com/aokoli/goutils 5 | version: 9c37978a95bd5c709a15883b6242714ea6709e64 6 | - name: github.com/BurntSushi/toml 7 | version: b26d9c308763d68093482582cea63d69be07a0f0 8 | - name: github.com/facebookgo/symwalk 9 | version: 42004b9f322246749dd73ad71008b1f3160c0052 10 | - name: github.com/ghodss/yaml 11 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 12 | - name: github.com/gobwas/glob 13 | version: bea32b9cd2d6f55753d94a28e959b13f0244797a 14 | subpackages: 15 | - compiler 16 | - match 17 | - syntax 18 | - syntax/ast 19 | - syntax/lexer 20 | - util/runes 21 | - util/strings 22 | - name: github.com/golang/protobuf 23 | version: 2bba0603135d7d7f5cb73b2125beeda19c09f4ef 24 | subpackages: 25 | - proto 26 | - ptypes/any 27 | - ptypes/timestamp 28 | - name: github.com/huandu/xstrings 29 | version: 3959339b333561bf62a38b424fd41517c2c90f40 30 | - name: github.com/imdario/mergo 31 | version: 6633656539c1639d9d78127b7d47c622b5d7b6dc 32 | - name: github.com/inconshreveable/mousetrap 33 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 34 | - name: github.com/Masterminds/semver 35 | version: 3f0ab6d4ab4bed1c61caf056b63a6e62190c7801 36 | - name: github.com/Masterminds/sprig 37 | version: 9526be0327b26ad31aa70296a7b10704883976d5 38 | - name: github.com/satori/go.uuid 39 | version: 879c5887cd475cd7864858769793b2ceb0d44feb 40 | - name: github.com/spf13/cobra 41 | version: f62e98d28ab7ad31d707ba837a966378465c7b57 42 | - name: github.com/spf13/pflag 43 | version: 5ccb023bc27df288a957c5e994cd44fd19619465 44 | - name: golang.org/x/crypto 45 | version: 1f22c0103821b9390939b6776727195525381532 46 | subpackages: 47 | - pbkdf2 48 | - scrypt 49 | - name: gopkg.in/yaml.v2 50 | version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 51 | - name: k8s.io/apimachinery 52 | version: fbd6803372f831e58b86c78d07421637a64ad768 53 | subpackages: 54 | - pkg/version 55 | - name: k8s.io/helm 56 | version: 7cf31e8d9a026287041bae077b09165be247ae66 57 | subpackages: 58 | - pkg/chartutil 59 | - pkg/engine 60 | - pkg/ignore 61 | - pkg/proto/hapi/chart 62 | - pkg/proto/hapi/version 63 | - pkg/strvals 64 | - pkg/timeconv 65 | testImports: [] 66 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/technosophos/helm-template 2 | import: 3 | - package: github.com/spf13/cobra 4 | - package: k8s.io/helm 5 | version: ^2.5.0 6 | subpackages: 7 | - pkg/chartutil 8 | - pkg/engine 9 | - package: github.com/ghodss/yaml 10 | -------------------------------------------------------------------------------- /install-binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Combination of the Glide and Helm scripts, with my own tweaks. 4 | 5 | PROJECT_NAME="helm-template" 6 | PROJECT_GH="technosophos/$PROJECT_NAME" 7 | 8 | : ${HELM_PLUGIN_PATH:="$(helm home)/plugins/helm-template"} 9 | 10 | # Convert the HELM_PLUGIN_PATH to unix if cygpath is 11 | # available. This is the case when using MSYS2 or Cygwin 12 | # on Windows where helm returns a Windows path but we 13 | # need a Unix path 14 | if type cygpath > /dev/null; then 15 | HELM_PLUGIN_PATH=$(cygpath -u $HELM_PLUGIN_PATH) 16 | fi 17 | 18 | if [[ $SKIP_BIN_INSTALL == "1" ]]; then 19 | echo "Skipping binary install" 20 | exit 21 | fi 22 | 23 | # initArch discovers the architecture for this system. 24 | initArch() { 25 | ARCH=$(uname -m) 26 | case $ARCH in 27 | armv5*) ARCH="armv5";; 28 | armv6*) ARCH="armv6";; 29 | armv7*) ARCH="armv7";; 30 | aarch64) ARCH="arm64";; 31 | x86) ARCH="386";; 32 | x86_64) ARCH="amd64";; 33 | i686) ARCH="386";; 34 | i386) ARCH="386";; 35 | esac 36 | } 37 | 38 | # initOS discovers the operating system for this system. 39 | initOS() { 40 | OS=$(echo `uname`|tr '[:upper:]' '[:lower:]') 41 | 42 | case "$OS" in 43 | # Msys support 44 | msys*) OS='windows';; 45 | # Minimalist GNU for Windows 46 | mingw*) OS='windows';; 47 | darwin) OS='macos';; 48 | esac 49 | } 50 | 51 | # verifySupported checks that the os/arch combination is supported for 52 | # binary builds. 53 | verifySupported() { 54 | local supported="linux-amd64\nmacos-amd64\nwindows-amd64" 55 | if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then 56 | echo "No prebuild binary for ${OS}-${ARCH}." 57 | exit 1 58 | fi 59 | 60 | if ! type "curl" > /dev/null && ! type "wget" > /dev/null; then 61 | echo "Either curl or wget is required" 62 | exit 1 63 | fi 64 | } 65 | 66 | # getDownloadURL checks the latest available version. 67 | getDownloadURL() { 68 | # Use the GitHub API to find the latest version for this project. 69 | local latest_url="https://api.github.com/repos/$PROJECT_GH/releases/latest" 70 | if type "curl" > /dev/null; then 71 | DOWNLOAD_URL=$(curl -s $latest_url | grep $OS | awk '/\"browser_download_url\":/{gsub( /[,\"]/,"", $2); print $2}') 72 | elif type "wget" > /dev/null; then 73 | DOWNLOAD_URL=$(wget -q -O - $latest_url | awk '/\"browser_download_url\":/{gsub( /[,\"]/,"", $2); print $2}') 74 | fi 75 | } 76 | 77 | # downloadFile downloads the latest binary package and also the checksum 78 | # for that binary. 79 | downloadFile() { 80 | PLUGIN_TMP_FILE="/tmp/${PROJECT_NAME}.tgz" 81 | echo "Downloading $DOWNLOAD_URL" 82 | if type "curl" > /dev/null; then 83 | curl -L "$DOWNLOAD_URL" -o "$PLUGIN_TMP_FILE" 84 | elif type "wget" > /dev/null; then 85 | wget -q -O "$PLUGIN_TMP_FILE" "$DOWNLOAD_URL" 86 | fi 87 | } 88 | 89 | # installFile verifies the SHA256 for the file, then unpacks and 90 | # installs it. 91 | installFile() { 92 | HELM_TMP="/tmp/$PROJECT_NAME" 93 | mkdir -p "$HELM_TMP" 94 | tar xf "$PLUGIN_TMP_FILE" -C "$HELM_TMP" 95 | HELM_TMP_BIN="$HELM_TMP/tpl" 96 | echo "Preparing to install into ${HELM_PLUGIN_PATH}" 97 | # Use * to also copy the file withe the exe suffix on Windows 98 | cp "$HELM_TMP_BIN"* "$HELM_PLUGIN_PATH" 99 | } 100 | 101 | # fail_trap is executed if an error occurs. 102 | fail_trap() { 103 | result=$? 104 | if [ "$result" != "0" ]; then 105 | echo "Failed to install $PROJECT_NAME" 106 | echo "\tFor support, go to https://github.com/kubernetes/helm." 107 | fi 108 | exit $result 109 | } 110 | 111 | # testVersion tests the installed client to make sure it is working. 112 | testVersion() { 113 | set +e 114 | echo "$PROJECT_NAME installed into $HELM_PLUGIN_PATH/$PROJECT_NAME" 115 | # To avoid to keep track of the Windows suffix, 116 | # call the plugin assuming it is in the PATH 117 | PATH=$PATH:$HELM_PLUGIN_PATH 118 | tpl -h 119 | set -e 120 | } 121 | 122 | # Execution 123 | 124 | #Stop execution on any error 125 | trap "fail_trap" EXIT 126 | set -e 127 | initArch 128 | initOS 129 | verifySupported 130 | getDownloadURL 131 | downloadFile 132 | installFile 133 | testVersion 134 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/ghodss/yaml" 13 | "github.com/spf13/cobra" 14 | 15 | "k8s.io/helm/pkg/chartutil" 16 | "k8s.io/helm/pkg/engine" 17 | "k8s.io/helm/pkg/proto/hapi/chart" 18 | "k8s.io/helm/pkg/strvals" 19 | "k8s.io/helm/pkg/timeconv" 20 | ) 21 | 22 | const globalUsage = ` 23 | Render chart templates locally and display the output. 24 | 25 | This does not require Tiller. However, any values that would normally be 26 | looked up or retrieved in-cluster will be faked locally. Additionally, none 27 | of the server-side testing of chart validity (e.g. whether an API is supported) 28 | is done. 29 | 30 | To render just one template in a chart, use '-x': 31 | 32 | $ helm template mychart -x mychart/templates/deployment.yaml 33 | ` 34 | 35 | var ( 36 | setVals []string 37 | valsFiles valueFiles 38 | flagVerbose bool 39 | showNotes bool 40 | releaseName string 41 | namespace string 42 | renderFiles []string 43 | ) 44 | 45 | var version = "DEV" 46 | 47 | func main() { 48 | cmd := &cobra.Command{ 49 | Use: "template [flags] CHART", 50 | Short: fmt.Sprintf("locally render templates (helm-template %s)", version), 51 | RunE: run, 52 | } 53 | 54 | f := cmd.Flags() 55 | f.StringArrayVar(&setVals, "set", []string{}, "set values on the command line. See 'helm install -h'") 56 | f.VarP(&valsFiles, "values", "f", "specify one or more YAML files of values") 57 | f.BoolVarP(&flagVerbose, "verbose", "v", false, "show the computed YAML values as well.") 58 | f.BoolVar(&showNotes, "notes", false, "show the computed NOTES.txt file as well.") 59 | f.StringVarP(&releaseName, "release", "r", "RELEASE-NAME", "release name") 60 | f.StringVarP(&namespace, "namespace", "n", "NAMESPACE", "namespace") 61 | f.StringArrayVarP(&renderFiles, "execute", "x", []string{}, "only execute the given templates.") 62 | 63 | if err := cmd.Execute(); err != nil { 64 | os.Exit(1) 65 | } 66 | } 67 | 68 | func run(cmd *cobra.Command, args []string) error { 69 | if len(args) < 1 { 70 | return errors.New("chart is required") 71 | } 72 | c, err := chartutil.Load(args[0]) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | vv, err := vals() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | config := &chart.Config{Raw: string(vv), Values: map[string]*chart.Value{}} 83 | 84 | if flagVerbose { 85 | fmt.Println("---\n# merged values") 86 | fmt.Println(string(vv)) 87 | } 88 | 89 | options := chartutil.ReleaseOptions{ 90 | Name: releaseName, 91 | Time: timeconv.Now(), 92 | Namespace: namespace, 93 | //Revision: 1, 94 | //IsInstall: true, 95 | } 96 | 97 | // Set up engine. 98 | renderer := engine.New() 99 | 100 | vals, err := chartutil.ToRenderValues(c, config, options) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | out, err := renderer.Render(c, vals) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | in := func(needle string, haystack []string) bool { 111 | for _, h := range haystack { 112 | if h == needle { 113 | return true 114 | } 115 | } 116 | return false 117 | } 118 | 119 | sortedKeys := make([]string, 0, len(out)) 120 | for key := range out { 121 | sortedKeys = append(sortedKeys, key) 122 | } 123 | sort.Strings(sortedKeys) 124 | 125 | // If renderFiles is set, we ONLY print those. 126 | if len(renderFiles) > 0 { 127 | for _, name := range sortedKeys { 128 | data := out[name] 129 | if in(name, renderFiles) { 130 | fmt.Printf("---\n# Source: %s\n", name) 131 | fmt.Println(data) 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | for _, name := range sortedKeys { 138 | data := out[name] 139 | b := filepath.Base(name) 140 | if !showNotes && b == "NOTES.txt" { 141 | continue 142 | } 143 | if strings.HasPrefix(b, "_") { 144 | continue 145 | } 146 | fmt.Printf("---\n# Source: %s\n", name) 147 | fmt.Println(data) 148 | } 149 | return nil 150 | } 151 | 152 | // liberally borrows from Helm 153 | func vals() ([]byte, error) { 154 | base := map[string]interface{}{} 155 | 156 | // User specified a values files via -f/--values 157 | for _, filePath := range valsFiles { 158 | currentMap := map[string]interface{}{} 159 | bytes, err := ioutil.ReadFile(filePath) 160 | if err != nil { 161 | return []byte{}, err 162 | } 163 | 164 | if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 165 | return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 166 | } 167 | // Merge with the previous map 168 | base = mergeValues(base, currentMap) 169 | } 170 | 171 | // User specified a value via --set 172 | for _, value := range setVals { 173 | if err := strvals.ParseInto(value, base); err != nil { 174 | return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 175 | } 176 | } 177 | 178 | return yaml.Marshal(base) 179 | } 180 | 181 | // Copied from Helm. 182 | 183 | func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { 184 | for k, v := range src { 185 | // If the key doesn't exist already, then just set the key to that value 186 | if _, exists := dest[k]; !exists { 187 | dest[k] = v 188 | continue 189 | } 190 | nextMap, ok := v.(map[string]interface{}) 191 | // If it isn't another map, overwrite the value 192 | if !ok { 193 | dest[k] = v 194 | continue 195 | } 196 | // If the key doesn't exist already, then just set the key to that value 197 | if _, exists := dest[k]; !exists { 198 | dest[k] = nextMap 199 | continue 200 | } 201 | // Edge case: If the key exists in the destination, but isn't a map 202 | destMap, isMap := dest[k].(map[string]interface{}) 203 | // If the source map has a map for this key, prefer it 204 | if !isMap { 205 | dest[k] = v 206 | continue 207 | } 208 | // If we got to this point, it is a map in both, so merge them 209 | dest[k] = mergeValues(destMap, nextMap) 210 | } 211 | return dest 212 | } 213 | 214 | type valueFiles []string 215 | 216 | func (v *valueFiles) String() string { 217 | return fmt.Sprint(*v) 218 | } 219 | 220 | func (v *valueFiles) Type() string { 221 | return "valueFiles" 222 | } 223 | 224 | func (v *valueFiles) Set(value string) error { 225 | for _, filePath := range strings.Split(value, ",") { 226 | *v = append(*v, filePath) 227 | } 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /plugin.yaml: -------------------------------------------------------------------------------- 1 | name: "template" 2 | # Version is the version of Helm plus the number of official builds for this 3 | # plugin 4 | version: "2.5.1+2" 5 | usage: "render templates on the local client" 6 | description: "Render templates on the local client." 7 | command: "$HELM_PLUGIN_DIR/tpl" 8 | hooks: 9 | install: "$HELM_PLUGIN_DIR/install-binary.sh" 10 | --------------------------------------------------------------------------------