mirror of https://github.com/helm/helm
Merge pull request #63 from technosophos/feat/helm-lint
feat(helm): add a very basic lint commandpull/613/head
commit
7e317e82db
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1,7 @@
|
||||
package lint
|
||||
|
||||
func All(basedir string) []Message {
|
||||
out := Chartfile(basedir)
|
||||
out = append(out, Templates(basedir)...)
|
||||
return out
|
||||
}
|
@ -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)
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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])
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
name: albatross
|
||||
description: testing chart
|
||||
version: 199.44.12345-Alpha.1+cafe009
|
@ -0,0 +1,2 @@
|
||||
metadata:
|
||||
name: {{.name | default "foo" | title}}
|
@ -0,0 +1 @@
|
||||
{{ deliberateSyntaxError }}
|
@ -0,0 +1 @@
|
||||
name = "mariner"
|
@ -0,0 +1,3 @@
|
||||
description: A Helm chart for Kubernetes
|
||||
version: 0.0.0
|
||||
home: ""
|
@ -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"
|
Loading…
Reference in new issue