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 }