Commit cf9c8ebe authored by Matt Butcher's avatar Matt Butcher

feat(helm): add a very basic lint command

This adds a lint command that just checks to see if the chart is
parseable and if the templates are paresable.
parent 0029740d
package main
import (
"fmt"
"github.com/deis/tiller/pkg/lint"
"github.com/spf13/cobra"
)
var longLintHelp = `
This command takes a path to a chart and runs a series of tests to verify that
the chart is well-formed.
If the linter encounters things that will cause the chart to fail installation,
it will emit [ERROR] messages. If it encounters issues that break with convention
or recommendation, it will emit [WARNING] messages.
`
var lintCommand = &cobra.Command{
Use: "lint [flags] PATH",
Short: "Examines a chart for possible issues",
Long: longLintHelp,
Run: lintCmd,
}
func init() {
RootCommand.AddCommand(lintCommand)
}
func lintCmd(cmd *cobra.Command, args []string) {
path := "."
if len(args) > 0 {
path = args[0]
}
issues := lint.All(path)
for _, i := range issues {
fmt.Printf("%s\n", i)
}
}
package lint
import (
"os"
"path/filepath"
chartutil "github.com/deis/tiller/pkg/chart"
)
func Chartfile(basepath string) (m []Message) {
m = []Message{}
path := filepath.Join(basepath, "Chart.yaml")
if fi, err := os.Stat(path); err != nil {
m = append(m, Message{Severity: ErrorSev, Text: "No Chart.yaml file"})
return
} else if fi.IsDir() {
m = append(m, Message{Severity: ErrorSev, Text: "Chart.yaml is a directory."})
return
}
cf, err := chartutil.LoadChartfile(path)
if err != nil {
m = append(m, Message{
Severity: ErrorSev,
Text: err.Error(),
})
return
}
if cf.Name == "" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'name' is required",
})
}
if cf.Version == "" || cf.Version == "0.0.0" {
m = append(m, Message{
Severity: ErrorSev,
Text: "Chart.yaml: 'version' is required, and must be greater than 0.0.0",
})
}
return
}
package lint
import (
"testing"
)
const badchartfile = "testdata/badchartfile"
func TestChartfile(t *testing.T) {
msgs := Chartfile(badchartfile)
if len(msgs) != 2 {
t.Errorf("Expected 2 errors, got %d", len(msgs))
}
if msgs[0].Text != "Chart.yaml: 'name' is required" {
t.Errorf("Unexpected message 0: %s", msgs[0].Text)
}
if msgs[1].Text != "Chart.yaml: 'version' is required, and must be greater than 0.0.0" {
t.Errorf("Unexpected message 1: %s", msgs[1].Text)
}
}
/*Package lint contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package lint
package lint
func All(basedir string) []Message {
out := Chartfile(basedir)
out = append(out, Templates(basedir)...)
return out
}
package lint
import "fmt"
type Severity int
const (
UnknownSev = iota
WarningSev
ErrorSev
)
var sev = []string{"INFO", "WARNING", "ERROR"}
type Message struct {
// Severity is one of the *Sev constants
Severity int
// Text contains the message text
Text string
}
// String prints a string representation of this Message.
//
// Implements fmt.Stringer.
func (m Message) String() string {
return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text)
}
package lint
import (
"fmt"
"testing"
)
var _ fmt.Stringer = Message{}
func TestMessage(t *testing.T) {
m := Message{ErrorSev, "Foo"}
if m.String() != "[ERROR] Foo" {
t.Errorf("Unexpected output: %s", m.String())
}
m = Message{WarningSev, "Bar"}
if m.String() != "[WARNING] Bar" {
t.Errorf("Unexpected output: %s", m.String())
}
}
package lint
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/Masterminds/sprig"
)
func Templates(basepath string) (messages []Message) {
messages = []Message{}
path := filepath.Join(basepath, "templates")
if fi, err := os.Stat(path); err != nil {
messages = append(messages, Message{Severity: WarningSev, Text: "No templates"})
return
} else if !fi.IsDir() {
messages = append(messages, Message{Severity: ErrorSev, Text: "'templates' is not a directory"})
return
}
tpl := template.New("tpl").Funcs(sprig.TxtFuncMap())
err := filepath.Walk(basepath, func(name string, fi os.FileInfo, e error) error {
// If an error is returned, we fail. Non-fatal errors should just be
// added directly to messages.
if e != nil {
return e
}
if fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(name)
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("cannot read %s: %s", name, err),
})
return nil
}
// An error rendering a file should emit a warning.
newtpl, err := tpl.Parse(string(data))
if err != nil {
messages = append(messages, Message{
Severity: ErrorSev,
Text: fmt.Sprintf("error processing %s: %s", name, err),
})
return nil
}
tpl = newtpl
return nil
})
if err != nil {
messages = append(messages, Message{Severity: ErrorSev, Text: err.Error()})
}
return
}
package lint
import (
"strings"
"testing"
)
const templateTestBasedir = "./testdata/albatross"
func TestTemplate(t *testing.T) {
res := Templates(templateTestBasedir)
if len(res) != 1 {
t.Fatalf("Expected one error, got %d", len(res))
}
if !strings.Contains(res[0].Text, "deliberateSyntaxError") {
t.Errorf("Unexpected error: %s", res[0])
}
}
name: albatross
description: testing chart
version: 199.44.12345-Alpha.1+cafe009
metadata:
name: {{.name | default "foo" | title}}
description: A Helm chart for Kubernetes
version: 0.0.0
home: ""
# Default values for badchartfile.
# This is a TOML-formatted file. https://github.com/toml-lang/toml
# Declare name/value pairs to be passed into your templates.
# name = "value"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment