github.com/danielqsj/helm@v2.0.0-alpha.4.0.20160908204436-976e0ba5199b+incompatible/pkg/lint/rules/template.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     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 rules
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strings"
    27  	"text/template"
    28  
    29  	"github.com/Masterminds/sprig"
    30  	"gopkg.in/yaml.v2"
    31  	"k8s.io/helm/pkg/chartutil"
    32  	"k8s.io/helm/pkg/engine"
    33  	"k8s.io/helm/pkg/lint/support"
    34  	"k8s.io/helm/pkg/timeconv"
    35  )
    36  
    37  // Templates lints the templates in the Linter.
    38  func Templates(linter *support.Linter) {
    39  	path := "templates/"
    40  	templatesPath := filepath.Join(linter.ChartDir, path)
    41  
    42  	templatesDirExist := linter.RunLinterRule(support.WarningSev, path, validateTemplatesDir(templatesPath))
    43  
    44  	// Templates directory is optional for now
    45  	if !templatesDirExist {
    46  		return
    47  	}
    48  
    49  	// Load chart and parse templates, based on tiller/release_server
    50  	chart, err := chartutil.Load(linter.ChartDir)
    51  
    52  	chartLoaded := linter.RunLinterRule(support.ErrorSev, path, err)
    53  
    54  	if !chartLoaded {
    55  		return
    56  	}
    57  
    58  	options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"}
    59  	valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options)
    60  	if err != nil {
    61  		// FIXME: This seems to generate a duplicate, but I can't find where the first
    62  		// error is coming from.
    63  		//linter.RunLinterRule(support.ErrorSev, err)
    64  		return
    65  	}
    66  	renderedContentMap, err := engine.New().Render(chart, valuesToRender)
    67  
    68  	renderOk := linter.RunLinterRule(support.ErrorSev, path, err)
    69  
    70  	if !renderOk {
    71  		return
    72  	}
    73  
    74  	/* Iterate over all the templates to check:
    75  	   - It is a .yaml file
    76  		 - All the values in the template file is defined
    77  		 - {{}} include | quote
    78  		 - Generated content is a valid Yaml file
    79  		 - Metadata.Namespace is not set
    80  	*/
    81  	for _, template := range chart.Templates {
    82  		fileName, preExecutedTemplate := template.Name, template.Data
    83  		path = fileName
    84  
    85  		linter.RunLinterRule(support.ErrorSev, path, validateAllowedExtension(fileName))
    86  
    87  		// We only apply the following lint rules to yaml files
    88  		if filepath.Ext(fileName) != ".yaml" {
    89  			continue
    90  		}
    91  
    92  		// Check that all the templates have a matching value
    93  		linter.RunLinterRule(support.WarningSev, path, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate))
    94  
    95  		// NOTE, disabled for now, Refs https://github.com/kubernetes/helm/issues/1037
    96  		// linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate)))
    97  
    98  		renderedContent := renderedContentMap[fileName]
    99  		var yamlStruct K8sYamlStruct
   100  		// Even though K8sYamlStruct only defines Metadata namespace, an error in any other
   101  		// key will be raised as well
   102  		err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct)
   103  
   104  		validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err))
   105  
   106  		if !validYaml {
   107  			continue
   108  		}
   109  
   110  		linter.RunLinterRule(support.ErrorSev, path, validateNoNamespace(yamlStruct))
   111  	}
   112  }
   113  
   114  // Validation functions
   115  func validateTemplatesDir(templatesPath string) error {
   116  	if fi, err := os.Stat(templatesPath); err != nil {
   117  		return errors.New("directory not found")
   118  	} else if err == nil && !fi.IsDir() {
   119  		return errors.New("not a directory")
   120  	}
   121  	return nil
   122  }
   123  
   124  // Validates that go template tags include the quote pipelined function
   125  // i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }}
   126  // {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
   127  func validateQuotes(templateContent string) error {
   128  	// {{ .Foo.bar }}
   129  	r, _ := regexp.Compile(`(?m)(:|-)\s+{{[\w|\.|\s|\']+}}\s*$`)
   130  	functions := r.FindAllString(templateContent, -1)
   131  
   132  	for _, str := range functions {
   133  		if match, _ := regexp.MatchString("quote", str); !match {
   134  			result := strings.Replace(str, "}}", " | quote }}", -1)
   135  			return fmt.Errorf("wrap substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", str, result)
   136  		}
   137  	}
   138  
   139  	// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
   140  	r, _ = regexp.Compile(`(?m)({{(\w|\.|\s|\')+}}(\s|-)*)+\s*$`)
   141  	functions = r.FindAllString(templateContent, -1)
   142  
   143  	for _, str := range functions {
   144  		result := strings.Replace(str, str, fmt.Sprintf("\"%s\"", str), -1)
   145  		return fmt.Errorf("wrap substitution functions in quotes: %s -> %s", str, result)
   146  	}
   147  	return nil
   148  }
   149  
   150  func validateAllowedExtension(fileName string) error {
   151  	ext := filepath.Ext(fileName)
   152  	validExtensions := []string{".yaml", ".tpl", ".txt"}
   153  
   154  	for _, b := range validExtensions {
   155  		if b == ext {
   156  			return nil
   157  		}
   158  	}
   159  
   160  	return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .tpl, or .txt", ext)
   161  }
   162  
   163  // validateNoMissingValues checks that all the {{}} functions returns a non empty value (<no value> or "")
   164  // and return an error otherwise.
   165  func validateNoMissingValues(templatesPath string, chartValues chartutil.Values, templateContent []byte) error {
   166  	// 1 - Load Main and associated templates
   167  	// Main template that we will parse dynamically
   168  	tmpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
   169  	// If the templatesPath includes any *.tpl files we will parse and import them as associated templates
   170  	associatedTemplates, err := filepath.Glob(filepath.Join(templatesPath, "*.tpl"))
   171  
   172  	if len(associatedTemplates) > 0 {
   173  		tmpl, err = tmpl.ParseFiles(associatedTemplates...)
   174  		if err != nil {
   175  			return err
   176  		}
   177  	}
   178  
   179  	var buf bytes.Buffer
   180  	var emptyValues []string
   181  
   182  	// 2 - Extract every function and execute them agains the loaded values
   183  	// Supported {{ .Chart.Name }}, {{ .Chart.Name | quote }}
   184  	r, _ := regexp.Compile(`{{[\w|\.|\s|\|\"|\']+}}`)
   185  	functions := r.FindAllString(string(templateContent), -1)
   186  
   187  	// Iterate over the {{ FOO }} templates, executing them against the chartValues
   188  	// We do individual templates parsing so we keep the reference for the key (str) that we want it to be interpolated.
   189  	for _, str := range functions {
   190  		newtmpl, err := tmpl.Parse(str)
   191  		if err != nil {
   192  			return err
   193  		}
   194  
   195  		err = newtmpl.ExecuteTemplate(&buf, "tpl", chartValues)
   196  
   197  		if err != nil {
   198  			return err
   199  		}
   200  
   201  		renderedValue := buf.String()
   202  
   203  		if renderedValue == "<no value>" || renderedValue == "" {
   204  			emptyValues = append(emptyValues, str)
   205  		}
   206  		buf.Reset()
   207  	}
   208  
   209  	if len(emptyValues) > 0 {
   210  		return fmt.Errorf("these substitution functions are returning no value: %v", emptyValues)
   211  	}
   212  	return nil
   213  }
   214  
   215  func validateYamlContent(err error) error {
   216  	if err != nil {
   217  		return fmt.Errorf("unable to parse YAML\n\t%s", err)
   218  	}
   219  	return nil
   220  }
   221  
   222  func validateNoNamespace(yamlStruct K8sYamlStruct) error {
   223  	if yamlStruct.Metadata.Namespace != "" {
   224  		return errors.New("namespace option is currently NOT supported")
   225  	}
   226  	return nil
   227  }
   228  
   229  // K8sYamlStruct stubs a Kubernetes YAML file.
   230  // Need to access for now to Namespace only
   231  type K8sYamlStruct struct {
   232  	Metadata struct {
   233  		Namespace string
   234  	}
   235  }