@ -17,70 +17,200 @@ limitations under the License.
package rules
package rules
import (
import (
"bytes"
"fmt"
"fmt"
"github.com/Masterminds/sprig"
"github.com/Masterminds/sprig"
"io/ioutil"
"gopkg.in/yaml.v2"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/engine"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/timeconv"
"os"
"os"
"path/filepath"
"path/filepath"
"regexp"
"strings"
"text/template"
"text/template"
)
)
// Templates lints a chart's templates.
func Templates ( linter * support . Linter ) {
func Templates ( linter * support . Linter ) {
templates p ath := filepath . Join ( linter . ChartDir , "templates" )
templates P ath := filepath . Join ( linter . ChartDir , "templates" )
templates Exist := linter . RunLinterRule ( support . WarningSev , validateTemplates Existence( linter , templatesp ath) )
templates Dir Exist := linter . RunLinterRule ( support . WarningSev , validateTemplates Dir( linter , templatesP ath) )
// Templates directory is optional for now
// Templates directory is optional for now
if ! templates Exist {
if ! templates Dir Exist {
return
return
}
}
linter . RunLinterRule ( support . ErrorSev , validateTemplatesDir ( linter , templatespath ) )
// Load chart and parse templates, based on tiller/release_server
linter . RunLinterRule ( support . ErrorSev , validateTemplatesParseable ( linter , templatespath ) )
chart , err := chartutil . Load ( linter . ChartDir )
}
func validateTemplatesExistence ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
chartLoaded := linter . RunLinterRule ( support . ErrorSev , validateNoError ( err ) )
if _ , err := os . Stat ( templatesPath ) ; err != nil {
lintError = fmt . Errorf ( "Templates directory not found" )
if ! chartLoaded {
return
}
// Based on cmd/tiller/release_server.go
overrides := map [ string ] interface { } {
"Release" : map [ string ] interface { } {
"Name" : "testRelease" ,
"Service" : "Tiller" ,
"Time" : timeconv . Now ( ) ,
} ,
"Chart" : chart . Metadata ,
}
chartValues , _ := chartutil . CoalesceValues ( chart , chart . Values , overrides )
renderedContentMap , err := engine . New ( ) . Render ( chart , chartValues )
renderOk := linter . RunLinterRule ( support . ErrorSev , validateNoError ( err ) )
if ! renderOk {
return
}
/ * Iterate over all the templates to check :
- It is a . yaml file
- All the values in the template file is defined
- { { } } include | quote
- Generated content is a valid Yaml file
- Metadata . Namespace is not set
* /
for _ , template := range chart . Templates {
fileName , preExecutedTemplate := template . Name , template . Data
yamlFile := linter . RunLinterRule ( support . ErrorSev , validateYamlExtension ( linter , fileName ) )
if ! yamlFile {
return
}
// Check that all the templates have a matching value
linter . RunLinterRule ( support . WarningSev , validateNonMissingValues ( fileName , chartValues , preExecutedTemplate ) )
linter . RunLinterRule ( support . WarningSev , validateQuotes ( fileName , string ( preExecutedTemplate ) ) )
renderedContent := renderedContentMap [ fileName ]
var yamlStruct K8sYamlStruct
// Even though K8sYamlStruct only defines Metadata namespace, an error in any other
// key will be raised as well
err := yaml . Unmarshal ( [ ] byte ( renderedContent ) , & yamlStruct )
validYaml := linter . RunLinterRule ( support . ErrorSev , validateYamlContent ( fileName , err ) )
if ! validYaml {
return
}
linter . RunLinterRule ( support . ErrorSev , validateNoNamespace ( fileName , yamlStruct ) )
}
}
return
}
}
// Validation functions
func validateTemplatesDir ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
func validateTemplatesDir ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
fi , err := os . Stat ( templatesPath )
if fi , err := os . Stat ( templatesPath ) ; err != nil {
if err == nil && ! fi . IsDir ( ) {
lintError = fmt . Errorf ( "Templates directory not found" )
} else if err == nil && ! fi . IsDir ( ) {
lintError = fmt . Errorf ( "'templates' is not a directory" )
lintError = fmt . Errorf ( "'templates' is not a directory" )
}
}
return
return
}
}
func validateTemplatesParseable ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
// Validates that go template tags include the quote pipelined function
tpl := template . New ( "tpl" ) . Funcs ( sprig . TxtFuncMap ( ) )
// i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }}
// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
lintError = filepath . Walk ( templatesPath , func ( name string , fi os . FileInfo , e error ) error {
func validateQuotes ( templateName string , templateContent string ) ( lintError support . LintError ) {
if e != nil {
// {{ .Foo.bar }}
return e
r , _ := regexp . Compile ( ` (?m)(:|-)\s+ {{ [ \ w | \ . | \ s | \ ' ] + }} \s*$ ` )
}
functions := r . FindAllString ( templateContent , - 1 )
if fi . IsDir ( ) {
return nil
for _ , str := range functions {
if match , _ := regexp . MatchString ( "quote" , str ) ; ! match {
result := strings . Replace ( str , "}}" , " | quote }}" , - 1 )
lintError = fmt . Errorf ( "templates: \"%s\". add \"| quote\" to your substitution functions: %s -> %s" , templateName , str , result )
return
}
}
}
// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
r , _ = regexp . Compile ( ` (?m)( {{ ( \ w | \ . | \ s | \ ' ) + }} (\s|-)*)+\s*$ ` )
functions = r . FindAllString ( templateContent , - 1 )
data , err := ioutil . ReadFile ( name )
for _ , str := range functions {
result := strings . Replace ( str , str , fmt . Sprintf ( "\"%s\"" , str ) , - 1 )
lintError = fmt . Errorf ( "templates: \"%s\". wrap your substitution functions in double quotes: %s -> %s" , templateName , str , result )
return
}
return
}
func validateYamlExtension ( linter * support . Linter , fileName string ) ( lintError support . LintError ) {
if filepath . Ext ( fileName ) != ".yaml" {
lintError = fmt . Errorf ( "templates: \"%s\" needs to use the .yaml extension" , fileName )
}
return
}
// validateNonMissingValues checks that all the {{}} functions returns a non empty value (<no value> or "")
// and return an error otherwise.
func validateNonMissingValues ( fileName string , chartValues chartutil . Values , templateContent [ ] byte ) ( lintError support . LintError ) {
tmpl := template . New ( "tpl" ) . Funcs ( sprig . TxtFuncMap ( ) )
var buf bytes . Buffer
var emptyValues [ ] string
// Supported {{ .Chart.Name }}, {{ .Chart.Name | quote }}
r , _ := regexp . Compile ( ` {{ ( [ \ w ] | \ . * | \ s | \ | ) + }} ` )
functions := r . FindAllString ( string ( templateContent ) , - 1 )
// Iterate over the {{ FOO }} templates, executing them against the chartValues
// We do individual templates parsing so we keep the reference for the key (str) that we want it to be interpolated.
for _ , str := range functions {
newtmpl , err := tmpl . Parse ( str )
if err != nil {
if err != nil {
lintError = fmt . Errorf ( "cannot read %s: %s" , name , err )
lintError = fmt . Errorf ( " templates: %s", err . Error ( ) )
return lintError
return
}
}
newtpl , err := tpl . Parse ( string ( data ) )
err = newtmpl . Execute ( & buf , chartValues )
if err != nil {
renderedValue := buf . String ( )
lintError = fmt . Errorf ( "error processing %s: %s" , name , err )
return lintError
if renderedValue == "<no value>" || renderedValue == "" {
emptyValues = append ( emptyValues , str )
}
}
tpl = newtpl
buf . Reset ( )
return nil
}
} )
if len ( emptyValues ) > 0 {
lintError = fmt . Errorf ( "templates: %s: The following functions are not returning eny value %v" , fileName , emptyValues )
}
return
}
func validateNoError ( readError error ) ( lintError support . LintError ) {
if readError != nil {
lintError = fmt . Errorf ( "templates: %s" , readError . Error ( ) )
}
return
}
func validateYamlContent ( filePath string , err error ) ( lintError support . LintError ) {
if err != nil {
lintError = fmt . Errorf ( "templates: \"%s\". Wrong YAML content." , filePath )
}
return
}
func validateNoNamespace ( filePath string , yamlStruct K8sYamlStruct ) ( lintError support . LintError ) {
if yamlStruct . Metadata . Namespace != "" {
lintError = fmt . Errorf ( "templates: \"%s\". namespace option is currently NOT supported." , filePath )
}
return
return
}
}
// Need to access for now to Namespace only
type K8sYamlStruct struct {
Metadata struct {
Namespace string
}
}