Merge pull request #857 from migmartri/689-template-rules

Templates directory lint rules
pull/883/head
Michelle Noorali 9 years ago committed by GitHub
commit 1dc95be105

@ -1,19 +1,19 @@
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
name: {{.Release.Name}}-{{.Chart.Name}} name: "{{.Release.Name}}-{{.Chart.Name}}"
labels: labels:
# The "heritage" label is used to track which tool deployed a given chart. # The "heritage" label is used to track which tool deployed a given chart.
# It is useful for admins who want to see what releases a particular tool # It is useful for admins who want to see what releases a particular tool
# is responsible for. # is responsible for.
heritage: {{.Release.Service}} heritage: {{.Release.Service | quote }}
# The "release" convention makes it easy to tie a release to all of the # The "release" convention makes it easy to tie a release to all of the
# Kubernetes resources that were created as part of that release. # Kubernetes resources that were created as part of that release.
release: {{.Release.Name}} release: {{.Release.Name | quote }}
# This makes it easy to audit chart usage. # This makes it easy to audit chart usage.
chart: {{.Chart.Name}}-{{.Chart.Version}} chart: "{{.Chart.Name}}-{{.Chart.Version}}"
annotations: annotations:
"helm.sh/created": "{{.Release.Time.Seconds}}" "helm.sh/created": {{.Release.Time.Seconds | quote }}
spec: spec:
# This shows how to use a simple value. This will look for a passed-in value # This shows how to use a simple value. This will look for a passed-in value
# called restartPolicy. If it is not found, it will use the default value. # called restartPolicy. If it is not found, it will use the default value.

@ -5,11 +5,11 @@ kind: ConfigMap
metadata: metadata:
name: {{template "fullname" .}} name: {{template "fullname" .}}
labels: labels:
release: {{.Release.Name}} release: {{ .Release.Name | quote }}
app: {{template "fullname" .}} app: {{template "fullname" .}}
heritage: {{.Release.Service}} heritage: {{.Release.Service | quote }}
data: data:
# When the config map is mounted as a volume, these will be created as # When the config map is mounted as a volume, these will be created as
# files. # files.
index.html: {{default "Hello" .index | squote}} index.html: {{ default "Hello" .index | quote }}
test.txt: test test.txt: test

@ -9,18 +9,18 @@ metadata:
# The "heritage" label is used to track which tool deployed a given chart. # The "heritage" label is used to track which tool deployed a given chart.
# It is useful for admins who want to see what releases a particular tool # It is useful for admins who want to see what releases a particular tool
# is responsible for. # is responsible for.
heritage: {{.Release.Service}} heritage: {{ .Release.Service | quote }}
# This makes it easy to search for all components of a release using kubectl. # This makes it easy to search for all components of a release using kubectl.
release: {{.Release.Name}} release: {{ .Release.Name | quote }}
# This makes it easy to audit chart usage. # This makes it easy to audit chart usage.
chart: {{.Chart.Name}}-{{.Chart.Version}} chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec: spec:
replicas: {{default 1 .replicaCount}} replicas: {{ default 1 .replicaCount | quote }}
template: template:
metadata: metadata:
labels: labels:
app: {{template "fullname" .}} app: {{template "fullname" .}}
release: {{.Release.Name}} release: {{.Release.Name | quote }}
spec: spec:
containers: containers:
- name: {{template "fullname" .}} - name: {{template "fullname" .}}

@ -5,12 +5,12 @@ kind: Service
metadata: metadata:
name: {{template "fullname" .}} name: {{template "fullname" .}}
labels: labels:
heritage: {{.Release.Service}} heritage: {{ .Release.Service | quote }}
release: {{.Release.Name}} release: {{ .Release.Name | quote }}
chart: {{.Chart.Name}}-{{.Chart.Version}} chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec: spec:
ports: ports:
- port: {{default 80 .httpPort}} - port: {{ default 80 .httpPort | quote }}
targetPort: 80 targetPort: 80
protocol: TCP protocol: TCP
name: http name: http

@ -211,6 +211,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*afile{} files := []*afile{}
topdir += string(filepath.Separator) topdir += string(filepath.Separator)
err = filepath.Walk(topdir, func(name string, fi os.FileInfo, err error) error { err = filepath.Walk(topdir, func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir) n := strings.TrimPrefix(name, topdir)
if err != nil { if err != nil {

@ -33,30 +33,30 @@ import (
func Chartfile(linter *support.Linter) { func Chartfile(linter *support.Linter) {
chartPath := filepath.Join(linter.ChartDir, "Chart.yaml") chartPath := filepath.Join(linter.ChartDir, "Chart.yaml")
linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(linter, chartPath)) linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(chartPath))
linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(linter, chartPath)) linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath) chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(linter, err)) validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(err))
// Guard clause. Following linter rules require a parseable ChartFile // Guard clause. Following linter rules require a parseable ChartFile
if !validChartFile { if !validChartFile {
return return
} }
linter.RunLinterRule(support.ErrorSev, validateChartName(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartName(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter.ChartDir, chartFile))
// Chart metadata // Chart metadata
linter.RunLinterRule(support.ErrorSev, validateChartVersion(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartEngine(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartEngine(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartSources(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartSources(chartFile))
linter.RunLinterRule(support.ErrorSev, validateChartHome(linter, chartFile)) linter.RunLinterRule(support.ErrorSev, validateChartHome(chartFile))
} }
// Auxiliar validation methods // Auxiliar validation methods
func validateChartYamlFileExistence(linter *support.Linter, chartPath string) (lintError support.LintError) { func validateChartYamlFileExistence(chartPath string) (lintError support.LintError) {
_, err := os.Stat(chartPath) _, err := os.Stat(chartPath)
if err != nil { if err != nil {
lintError = fmt.Errorf("Chart.yaml file does not exists") lintError = fmt.Errorf("Chart.yaml file does not exists")
@ -64,7 +64,7 @@ func validateChartYamlFileExistence(linter *support.Linter, chartPath string) (l
return return
} }
func validateChartYamlNotDirectory(linter *support.Linter, chartPath string) (lintError support.LintError) { func validateChartYamlNotDirectory(chartPath string) (lintError support.LintError) {
fi, err := os.Stat(chartPath) fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() { if err == nil && fi.IsDir() {
@ -73,28 +73,28 @@ func validateChartYamlNotDirectory(linter *support.Linter, chartPath string) (li
return return
} }
func validateChartYamlFormat(linter *support.Linter, chartFileError error) (lintError support.LintError) { func validateChartYamlFormat(chartFileError error) (lintError support.LintError) {
if chartFileError != nil { if chartFileError != nil {
lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error()) lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error())
} }
return return
} }
func validateChartName(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartName(cf *chart.Metadata) (lintError support.LintError) {
if cf.Name == "" { if cf.Name == "" {
lintError = fmt.Errorf("Chart.yaml: 'name' is required") lintError = fmt.Errorf("Chart.yaml: 'name' is required")
} }
return return
} }
func validateChartNameDirMatch(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) (lintError support.LintError) {
if cf.Name != filepath.Base(linter.ChartDir) { if cf.Name != filepath.Base(chartDir) {
lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match") lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match")
} }
return return
} }
func validateChartVersion(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartVersion(cf *chart.Metadata) (lintError support.LintError) {
if cf.Version == "" { if cf.Version == "" {
lintError = fmt.Errorf("Chart.yaml: 'version' value is required") lintError = fmt.Errorf("Chart.yaml: 'version' value is required")
return return
@ -117,7 +117,7 @@ func validateChartVersion(linter *support.Linter, cf *chart.Metadata) (lintError
return return
} }
func validateChartEngine(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) {
if cf.Engine == "" { if cf.Engine == "" {
return return
} }
@ -141,7 +141,7 @@ func validateChartEngine(linter *support.Linter, cf *chart.Metadata) (lintError
return return
} }
func validateChartMaintainer(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartMaintainer(cf *chart.Metadata) (lintError support.LintError) {
for _, maintainer := range cf.Maintainers { for _, maintainer := range cf.Maintainers {
if maintainer.Name == "" { if maintainer.Name == "" {
lintError = fmt.Errorf("Chart.yaml: maintainer requires a name") lintError = fmt.Errorf("Chart.yaml: maintainer requires a name")
@ -152,7 +152,7 @@ func validateChartMaintainer(linter *support.Linter, cf *chart.Metadata) (lintEr
return return
} }
func validateChartSources(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartSources(cf *chart.Metadata) (lintError support.LintError) {
for _, source := range cf.Sources { for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) { if source == "" || !govalidator.IsRequestURL(source) {
lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source) lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source)
@ -161,7 +161,7 @@ func validateChartSources(linter *support.Linter, cf *chart.Metadata) (lintError
return return
} }
func validateChartHome(linter *support.Linter, cf *chart.Metadata) (lintError support.LintError) { func validateChartHome(cf *chart.Metadata) (lintError support.LintError) {
if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) { if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) {
lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home) lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home)
} }

@ -17,15 +17,214 @@ limitations under the License.
package rules package rules
import ( import (
"k8s.io/helm/pkg/lint/support" "errors"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/lint/support"
"k8s.io/helm/pkg/proto/hapi/chart"
) )
const badchartfile = "testdata/badchartfile" const badChartDir = "testdata/badchartfile"
const goodChartDir = "testdata/goodone"
var badChartFilePath string = filepath.Join(badChartDir, "Chart.yaml")
var goodChartFilePath string = filepath.Join(goodChartDir, "Chart.yaml")
var nonExistingChartFilePath string = filepath.Join(os.TempDir(), "Chart.yaml")
var badChart, chatLoadRrr = chartutil.LoadChartfile(badChartFilePath)
var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath)
// Validation functions Test
func TestValidateChartYamlFileExistence(t *testing.T) {
err := validateChartYamlFileExistence(nonExistingChartFilePath)
if err == nil {
t.Errorf("validateChartYamlFileExistence to return a linter error, got no error")
}
err = validateChartYamlFileExistence(badChartFilePath)
if err != nil {
t.Errorf("validateChartYamlFileExistence to return no error, got a linter error")
}
}
func TestValidateChartYamlNotDirectory(t *testing.T) {
_ = os.Mkdir(nonExistingChartFilePath, os.ModePerm)
defer os.Remove(nonExistingChartFilePath)
err := validateChartYamlNotDirectory(nonExistingChartFilePath)
if err == nil {
t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error")
}
}
func TestValidateChartYamlFormat(t *testing.T) {
err := validateChartYamlFormat(errors.New("Read error"))
if err == nil {
t.Errorf("validateChartYamlFormat to return a linter error, got no error")
}
err = validateChartYamlFormat(nil)
if err != nil {
t.Errorf("validateChartYamlFormat to return no error, got a linter error")
}
}
func TestValidateChartName(t *testing.T) {
err := validateChartName(badChart)
if err == nil {
t.Errorf("validateChartName to return a linter error, got no error")
}
}
func TestValidateChartNameDirMatch(t *testing.T) {
err := validateChartNameDirMatch(goodChartDir, goodChart)
if err != nil {
t.Errorf("validateChartNameDirMatch to return no error, gor a linter error")
}
// It has not name
err = validateChartNameDirMatch(badChartDir, badChart)
if err == nil {
t.Errorf("validatechartnamedirmatch to return a linter error, got no error")
}
// Wrong path
err = validateChartNameDirMatch(badChartDir, goodChart)
if err == nil {
t.Errorf("validatechartnamedirmatch to return a linter error, got no error")
}
}
func TestValidateChartVersion(t *testing.T) {
var failTest = []struct {
Version string
ErrorMsg string
}{
{"", "'version' value is required"},
{"0", "0 is less than or equal to 0"},
{"waps", "is not a valid SemVer"},
{"-3", "is not a valid SemVer"},
}
var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
for _, test := range failTest {
badChart.Version = test.Version
err := validateChartVersion(badChart)
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg)
}
}
for _, version := range successTest {
badChart.Version = version
err := validateChartVersion(badChart)
if err != nil {
t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version)
}
}
}
func TestValidateChartEngine(t *testing.T) {
var successTest = []string{"", "gotpl"}
for _, engine := range successTest {
badChart.Engine = engine
err := validateChartEngine(badChart)
if err != nil {
t.Errorf("validateChartEngine(%s) to return no error, got a linter error %s", engine, err.Error())
}
}
badChart.Engine = "foobar"
err := validateChartEngine(badChart)
if err == nil || !strings.Contains(err.Error(), "not valid. Valid options are [gotpl") {
t.Errorf("validateChartEngine(%s) to return an error, got no error", badChart.Engine)
}
}
func TestValidateChartMaintainer(t *testing.T) {
var failTest = []struct {
Name string
Email string
ErrorMsg string
}{
{"", "", "maintainer requires a name"},
{"", "test@test.com", "maintainer requires a name"},
{"John Snow", "wrongFormatEmail.com", "maintainer invalid email"},
}
var successTest = []struct {
Name string
Email string
}{
{"John Snow", ""},
{"John Snow", "john@winterfell.com"},
}
for _, test := range failTest {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
err := validateChartMaintainer(badChart)
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg)
}
}
for _, test := range successTest {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
err := validateChartMaintainer(badChart)
if err != nil {
t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error())
}
}
}
func TestValidateChartSources(t *testing.T) {
var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"}
var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"}
for _, test := range failTest {
badChart.Sources = []string{test}
err := validateChartSources(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") {
t.Errorf("validateChartSources(%s) to return \"invalid URL\", got no error", test)
}
}
for _, test := range successTest {
badChart.Sources = []string{test}
err := validateChartSources(badChart)
if err != nil {
t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error())
}
}
}
func TestValidateChartHome(t *testing.T) {
var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"}
var successTest = []string{"", "http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"}
for _, test := range failTest {
badChart.Home = test
err := validateChartHome(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid URL") {
t.Errorf("validateChartHome(%s) to return \"invalid URL\", got no error", test)
}
}
for _, test := range successTest {
badChart.Home = test
err := validateChartHome(badChart)
if err != nil {
t.Errorf("validateChartHome(%s) to return no error, got %s", test, err.Error())
}
}
}
func TestChartfile(t *testing.T) { func TestChartfile(t *testing.T) {
linter := support.Linter{ChartDir: badchartfile} linter := support.Linter{ChartDir: badChartDir}
Chartfile(&linter) Chartfile(&linter)
msgs := linter.Messages msgs := linter.Messages

@ -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) {
templatespath := filepath.Join(linter.ChartDir, "templates") templatesPath := filepath.Join(linter.ChartDir, "templates")
templatesExist := linter.RunLinterRule(support.WarningSev, validateTemplatesExistence(linter, templatespath)) templatesDirExist := linter.RunLinterRule(support.WarningSev, validateTemplatesDir(templatesPath))
// Templates directory is optional for now // Templates directory is optional for now
if !templatesExist { if !templatesDirExist {
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
}
}

@ -24,6 +24,55 @@ import (
const templateTestBasedir = "./testdata/albatross" const templateTestBasedir = "./testdata/albatross"
func TestValidateAllowedExtension(t *testing.T) {
var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"}
for _, test := range failTest {
err := validateAllowedExtension(test)
if err == nil || !strings.Contains(err.Error(), "needs to use .yaml or .tpl extension") {
t.Errorf("validateAllowedExtension('%s') to return \"needs to use .yaml or .tpl extension\", got no error", test)
}
}
var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml"}
for _, test := range successTest {
err := validateAllowedExtension(test)
if err != nil {
t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error())
}
}
}
func TestValidateQuotes(t *testing.T) {
// add `| quote` lint error
var failTest = []string{"foo: {{.Release.Service }}", "foo: {{.Release.Service }}", "- {{.Release.Service }}", "foo: {{default 'Never' .restart_policy}}", "- {{.Release.Service }} "}
for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test)
if err == nil || !strings.Contains(err.Error(), "add \"| quote\" to your substitution functions") {
t.Errorf("validateQuotes('%s') to return \"add | quote error\", got no error", test)
}
}
var successTest = []string{"foo: {{.Release.Service | quote }}", "foo: {{.Release.Service | quote }}", "- {{.Release.Service | quote }}", "foo: {{default 'Never' .restart_policy | quote }}", "foo: \"{{ .Release.Service }}\"", "foo: \"{{ .Release.Service }} {{ .Foo.Bar }}\"", "foo: \"{{ default 'Never' .Release.Service }} {{ .Foo.Bar }}\""}
for _, test := range successTest {
err := validateQuotes("testTemplate.yaml", test)
if err != nil {
t.Errorf("validateQuotes('%s') to return not error and got \"%s\"", test, err.Error())
}
}
// Surrounding quotes
failTest = []string{"foo: {{.Release.Service }}-{{ .Release.Bar }}", "foo: {{.Release.Service }} {{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }} {{ .Release.Baz }}", "foo: {{.Release.Service | default }}-{{ .Release.Bar }}"}
for _, test := range failTest {
err := validateQuotes("testTemplate.yaml", test)
if err == nil || !strings.Contains(err.Error(), "wrap your substitution functions in double quotes") {
t.Errorf("validateQuotes('%s') to return \"wrap your substitution functions in double quotes\", got no error %s", test, err.Error())
}
}
}
func TestTemplate(t *testing.T) { func TestTemplate(t *testing.T) {
linter := support.Linter{ChartDir: templateTestBasedir} linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter) Templates(&linter)

@ -19,8 +19,6 @@ package support
import "fmt" import "fmt"
// Severity indicatest the severity of a Message. // Severity indicatest the severity of a Message.
type Severity int
const ( const (
// UnknownSev indicates that the severity of the error is unknown, and should not stop processing. // UnknownSev indicates that the severity of the error is unknown, and should not stop processing.
UnknownSev = iota UnknownSev = iota
@ -38,7 +36,7 @@ var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"}
// Message is a linting output message // Message is a linting output message
type Message struct { type Message struct {
// Severity is one of the *Sev constants // Severity is one of the *Sev constants
Severity Severity Severity int
// Text contains the message text // Text contains the message text
Text string Text string
} }
@ -52,8 +50,6 @@ type LintError interface {
error error
} }
type ValidationFunc func(*Linter) LintError
// String prints a string representation of this Message. // String prints a string representation of this Message.
// //
// Implements fmt.Stringer. // Implements fmt.Stringer.
@ -62,7 +58,12 @@ func (m Message) String() string {
} }
// Returns true if the validation passed // Returns true if the validation passed
func (l *Linter) RunLinterRule(severity Severity, lintError LintError) bool { func (l *Linter) RunLinterRule(severity int, lintError LintError) bool {
// severity is out of bound
if severity < 0 || severity >= len(sev) {
return false
}
if lintError != nil { if lintError != nil {
l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity}) l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity})
} }

@ -21,7 +21,38 @@ import (
"testing" "testing"
) )
var _ fmt.Stringer = Message{} var linter Linter = Linter{}
var lintError LintError = fmt.Errorf("Foobar")
func TestRunLinterRule(t *testing.T) {
var tests = []struct {
Severity int
LintError error
ExpectedMessages int
ExpectedReturn bool
}{
{ErrorSev, lintError, 1, false},
{WarningSev, lintError, 2, false},
{InfoSev, lintError, 3, false},
// No error so it returns true
{ErrorSev, nil, 3, true},
// Invalid severity values
{4, lintError, 3, false},
{22, lintError, 3, false},
{-1, lintError, 3, false},
}
for _, test := range tests {
isValid := linter.RunLinterRule(test.Severity, test.LintError)
if len(linter.Messages) != test.ExpectedMessages {
t.Errorf("RunLinterRule(%d, %v), linter.Messages should have now %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages))
}
if isValid != test.ExpectedReturn {
t.Errorf("RunLinterRule(%d, %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid)
}
}
}
func TestMessage(t *testing.T) { func TestMessage(t *testing.T) {
m := Message{ErrorSev, "Foo"} m := Message{ErrorSev, "Foo"}

Loading…
Cancel
Save