mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
7.5 KiB
243 lines
7.5 KiB
/*
|
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package rules
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/Masterminds/sprig"
|
|
"gopkg.in/yaml.v2"
|
|
"k8s.io/helm/pkg/chartutil"
|
|
"k8s.io/helm/pkg/engine"
|
|
"k8s.io/helm/pkg/lint/support"
|
|
"k8s.io/helm/pkg/timeconv"
|
|
)
|
|
|
|
// Templates lints the templates in the Linter.
|
|
func Templates(linter *support.Linter) {
|
|
templatesPath := filepath.Join(linter.ChartDir, "templates")
|
|
|
|
templatesDirExist := linter.RunLinterRule(support.WarningSev, validateTemplatesDir(templatesPath))
|
|
|
|
// Templates directory is optional for now
|
|
if !templatesDirExist {
|
|
return
|
|
}
|
|
|
|
// Load chart and parse templates, based on tiller/release_server
|
|
chart, err := chartutil.Load(linter.ChartDir)
|
|
|
|
chartLoaded := linter.RunLinterRule(support.ErrorSev, validateNoError(err))
|
|
|
|
if !chartLoaded {
|
|
return
|
|
}
|
|
|
|
options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"}
|
|
valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options)
|
|
if err != nil {
|
|
// FIXME: This seems to generate a duplicate, but I can't find where the first
|
|
// error is coming from.
|
|
//linter.RunLinterRule(support.ErrorSev, err)
|
|
return
|
|
}
|
|
renderedContentMap, err := engine.New().Render(chart, valuesToRender)
|
|
|
|
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, valuesToRender, 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))
|
|
}
|
|
}
|
|
|
|
// Validation functions
|
|
func validateTemplatesDir(templatesPath string) (lintError support.LintError) {
|
|
if fi, err := os.Stat(templatesPath); err != nil {
|
|
lintError = fmt.Errorf("Directory 'templates/' not found")
|
|
} else if err == nil && !fi.IsDir() {
|
|
lintError = fmt.Errorf("'templates' is not a directory")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Validates that go template tags include the quote pipelined function
|
|
// i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }}
|
|
// {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}"
|
|
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\". Wrap your substitution functions in quotes or use the sprig \"quote\" function: %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 quotes: %s -> %s", templateName, str, result)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func validateAllowedExtension(fileName string) (lintError support.LintError) {
|
|
ext := filepath.Ext(fileName)
|
|
validExtensions := []string{".yaml", ".tpl"}
|
|
|
|
for _, b := range validExtensions {
|
|
if b == ext {
|
|
return
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// 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 {
|
|
lintError = fmt.Errorf("templates: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
err = newtmpl.ExecuteTemplate(&buf, "tpl", chartValues)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// K8sYamlStruct stubs a Kubernetes YAML file.
|
|
// Need to access for now to Namespace only
|
|
type K8sYamlStruct struct {
|
|
Metadata struct {
|
|
Namespace string
|
|
}
|
|
}
|