@ -17,70 +17,226 @@ 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( 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 )
chartLoaded := linter . RunLinterRule ( support . ErrorSev , validateNoError ( err ) )
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
linter . RunLinterRule ( support . ErrorSev , validateAllowedExtension ( fileName ) )
// We only apply the following lint rules to yaml files
if filepath . Ext ( fileName ) != ".yaml" {
continue
}
// Check that all the templates have a matching value
linter . RunLinterRule ( support . WarningSev , validateNonMissingValues ( fileName , templatesPath , 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 {
continue
}
linter . RunLinterRule ( support . ErrorSev , validateNoNamespace ( fileName , yamlStruct ) )
}
}
}
func validateTemplatesExistence ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
// Validation functions
if _ , err := os . Stat ( templatesPath ) ; err != nil {
func validateTemplatesDir ( templatesPath string ) ( lintError support . LintError ) {
if fi , err := os . Stat ( templatesPath ) ; err != nil {
lintError = fmt . Errorf ( "Templates directory not found" )
lintError = fmt . Errorf ( "Templates directory not found" )
} else if err == nil && ! fi . IsDir ( ) {
lintError = fmt . Errorf ( "'templates' is not a directory" )
}
}
return
return
}
}
func validateTemplatesDir ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
// Validates that go template tags include the quote pipelined function
fi , err := os . Stat ( templatesPath )
// i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }}
if err == nil && ! fi . IsDir ( ) {
// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
lintError = fmt . Errorf ( "'templates' is not a directory" )
func validateQuotes ( templateName string , templateContent string ) ( lintError support . LintError ) {
// {{ .Foo.bar }}
r , _ := regexp . Compile ( ` (?m)(:|-)\s+ {{ [ \ w | \ . | \ s | \ ' ] + }} \s*$ ` )
functions := r . FindAllString ( templateContent , - 1 )
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 )
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
return
}
}
func validateTemplatesParseable ( linter * support . Linter , templatesPath string ) ( lintError support . LintError ) {
func validateAllowedExtension ( fileName string ) ( lintError support . LintError ) {
tpl := template . New ( "tpl" ) . Funcs ( sprig . TxtFuncMap ( ) )
ext := filepath . Ext ( fileName )
validExtensions := [ ] string { ".yaml" , ".tpl" }
lintError = filepath . Walk ( templatesPath , func ( name string , fi os . FileInfo , e error ) error {
for _ , b := range validExtensions {
if e != nil {
if b == ext {
return e
return
}
}
if fi . IsDir ( ) {
}
return nil
lintError = fmt . Errorf ( "templates: \"%s\" needs to use .yaml or .tpl extensions" , 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 , templatesPath string , chartValues chartutil . Values , templateContent [ ] byte ) ( lintError support . LintError ) {
// 1 - Load Main and associated templates
// Main template that we will parse dynamically
tmpl := template . New ( "tpl" ) . Funcs ( sprig . TxtFuncMap ( ) )
// If the templatesPath includes any *.tpl files we will parse and import them as associated templates
associatedTemplates , err := filepath . Glob ( filepath . Join ( templatesPath , "*.tpl" ) )
if len ( associatedTemplates ) > 0 {
tmpl , err = tmpl . ParseFiles ( associatedTemplates ... )
if err != nil {
return err
}
}
}
var buf bytes . Buffer
var emptyValues [ ] string
// 2 - Extract every function and execute them agains the loaded values
// Supported {{ .Chart.Name }}, {{ .Chart.Name | quote }}
r , _ := regexp . Compile ( ` {{ [ \ w | \ . | \ s | \ | \ " | \ ' ] + }} ` )
functions := r . FindAllString ( string ( templateContent ) , - 1 )
data , err := ioutil . ReadFile ( name )
// 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 . ExecuteTemplate ( & buf , "tpl" , chartValues )
if err != nil {
if err != nil {
lintError = fmt . Errorf ( "error processing %s: %s" , name , err )
return err
return lintError
}
}
tpl = newtpl
return nil
} )
renderedValue := buf . String ( )
if renderedValue == "<no value>" || renderedValue == "" {
emptyValues = append ( emptyValues , str )
}
buf . Reset ( )
}
if len ( emptyValues ) > 0 {
lintError = fmt . Errorf ( "templates: %s: The following functions are not returning any value %v" , fileName , emptyValues )
}
return
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
}
// Need to access for now to Namespace only
type K8sYamlStruct struct {
Metadata struct {
Namespace string
}
}