diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go new file mode 100644 index 000000000..1210d15d0 --- /dev/null +++ b/cmd/helm/lint.go @@ -0,0 +1,39 @@ +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) + } +} diff --git a/pkg/lint/chartfile.go b/pkg/lint/chartfile.go new file mode 100644 index 000000000..b8a33a30b --- /dev/null +++ b/pkg/lint/chartfile.go @@ -0,0 +1,45 @@ +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 +} diff --git a/pkg/lint/chartfile_test.go b/pkg/lint/chartfile_test.go new file mode 100644 index 000000000..c8c3be911 --- /dev/null +++ b/pkg/lint/chartfile_test.go @@ -0,0 +1,22 @@ +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) + } +} diff --git a/pkg/lint/doc.go b/pkg/lint/doc.go new file mode 100644 index 000000000..f2cc19670 --- /dev/null +++ b/pkg/lint/doc.go @@ -0,0 +1,6 @@ +/*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 diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go new file mode 100644 index 000000000..07a31acea --- /dev/null +++ b/pkg/lint/lint.go @@ -0,0 +1,7 @@ +package lint + +func All(basedir string) []Message { + out := Chartfile(basedir) + out = append(out, Templates(basedir)...) + return out +} diff --git a/pkg/lint/message.go b/pkg/lint/message.go new file mode 100644 index 000000000..a0acda0ca --- /dev/null +++ b/pkg/lint/message.go @@ -0,0 +1,27 @@ +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) +} diff --git a/pkg/lint/message_test.go b/pkg/lint/message_test.go new file mode 100644 index 000000000..1d083b369 --- /dev/null +++ b/pkg/lint/message_test.go @@ -0,0 +1,20 @@ +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()) + } +} diff --git a/pkg/lint/template.go b/pkg/lint/template.go new file mode 100644 index 000000000..f3bb559d0 --- /dev/null +++ b/pkg/lint/template.go @@ -0,0 +1,63 @@ +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 +} diff --git a/pkg/lint/template_test.go b/pkg/lint/template_test.go new file mode 100644 index 000000000..b8c7eadd4 --- /dev/null +++ b/pkg/lint/template_test.go @@ -0,0 +1,20 @@ +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]) + } +} diff --git a/pkg/lint/testdata/albatross/Chart.yaml b/pkg/lint/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..4aa22d376 --- /dev/null +++ b/pkg/lint/testdata/albatross/Chart.yaml @@ -0,0 +1,3 @@ +name: albatross +description: testing chart +version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/lint/testdata/albatross/templates/albatross.yaml b/pkg/lint/testdata/albatross/templates/albatross.yaml new file mode 100644 index 000000000..6c2ceb8db --- /dev/null +++ b/pkg/lint/testdata/albatross/templates/albatross.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{.name | default "foo" | title}} diff --git a/pkg/lint/testdata/albatross/templates/fail.yaml b/pkg/lint/testdata/albatross/templates/fail.yaml new file mode 100644 index 000000000..a11e0e90e --- /dev/null +++ b/pkg/lint/testdata/albatross/templates/fail.yaml @@ -0,0 +1 @@ +{{ deliberateSyntaxError }} diff --git a/pkg/lint/testdata/albatross/values.toml b/pkg/lint/testdata/albatross/values.toml new file mode 100644 index 000000000..388764d49 --- /dev/null +++ b/pkg/lint/testdata/albatross/values.toml @@ -0,0 +1 @@ +name = "mariner" diff --git a/pkg/lint/testdata/badchartfile/Chart.yaml b/pkg/lint/testdata/badchartfile/Chart.yaml new file mode 100644 index 000000000..dbb4a1501 --- /dev/null +++ b/pkg/lint/testdata/badchartfile/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +version: 0.0.0 +home: "" diff --git a/pkg/lint/testdata/badchartfile/values.toml b/pkg/lint/testdata/badchartfile/values.toml new file mode 100644 index 000000000..d6bba222c --- /dev/null +++ b/pkg/lint/testdata/badchartfile/values.toml @@ -0,0 +1,4 @@ +# 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"