diff --git a/compiler/compiler.go b/compiler/compiler.go index a20d4fe..a431d06 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1 +1,77 @@ package compiler + +import "github.com/drone/drone-cli/common" + +// Flags holds flags for loading a builder. +type Flags struct { + Lint bool + Safe bool +} + +// Builder represents a build execution tree. +type Builder struct { + Build Node + Deploy Node + Notify Node +} + +// Run runs the build, deploy and notify nodes +// in the build tree. +func (b *Builder) Run() error { + var err error + err = b.RunBuild() + if err != nil { + return err + } + err = b.RunDeploy() + if err != nil { + return err + } + return b.RunNotify() +} + +// RunBuild runs only the build node. +func (b *Builder) RunBuild() error { + return nil +} + +// RunDeploy runs only the deploy node. +func (b *Builder) RunDeploy() error { + return nil +} + +// RunNotify runs on the notify node. +func (b *Builder) RunNotify() error { + return nil +} + +// Load loads a build configuration file. +func Load(conf *common.Config, flags *Flags) *Builder { + var ( + builds []Node + deploys []Node + notifys []Node + ) + + for _, step := range conf.Compose { + builds = append(builds, &batchNode{step}) // compose + } + builds = append(builds, &batchNode{}) // setup + builds = append(builds, &batchNode{}) // clone + builds = append(builds, &batchNode{}) // build + + for _, step := range conf.Publish { + deploys = append(deploys, &batchNode{step}) // publish + } + for _, step := range conf.Deploy { + deploys = append(deploys, &batchNode{step}) // deploy + } + for _, step := range conf.Notify { + notifys = append(notifys, &batchNode{step}) // notify + } + return &Builder{ + serialNode(builds), + serialNode(deploys), + serialNode(notifys), + } +} diff --git a/compiler/inject/inject.go b/compiler/inject/inject.go deleted file mode 100644 index 26752a9..0000000 --- a/compiler/inject/inject.go +++ /dev/null @@ -1,54 +0,0 @@ -package inject - -import ( - "sort" - "strings" - - "github.com/drone/drone-cli/common" - "gopkg.in/yaml.v2" -) - -// Inject injects a map of parameters into a raw string and returns -// the resulting string. -// -// Parameters are represented in the string using $$ notation, similar -// to how environment variables are defined in Makefiles. -func Inject(raw string, params map[string]string) string { - if params == nil { - return raw - } - keys := []string{} - for k := range params { - keys = append(keys, k) - } - sort.Sort(sort.Reverse(sort.StringSlice(keys))) - injected := raw - for _, k := range keys { - v := params[k] - injected = strings.Replace(injected, "$$"+k, v, -1) - } - return injected -} - -// InjectSafe attempts to safely inject parameters without leaking -// parameters in the Build or Compose section of the yaml file. -// -// The intended use case for this function are public pull requests. -// We want to avoid a malicious pull request that allows someone -// to inject and print private variables. -func InjectSafe(raw string, params map[string]string) string { - before, _ := parse(raw) - after, _ := parse(Inject(raw, params)) - before.Notify = after.Notify - before.Publish = after.Publish - before.Deploy = after.Deploy - result, _ := yaml.Marshal(before) - return string(result) -} - -// helper funtion to parse a yaml configuration file. -func parse(raw string) (*common.Config, error) { - cfg := common.Config{} - err := yaml.Unmarshal([]byte(raw), &cfg) - return &cfg, err -} diff --git a/compiler/inject/inject_test.go b/compiler/inject/inject_test.go deleted file mode 100644 index 9726635..0000000 --- a/compiler/inject/inject_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package inject - -import ( - "testing" - - "github.com/franela/goblin" -) - -func Test_Inject(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Inject params", func() { - - g.It("Should replace vars with $$", func() { - s := "echo $$FOO $BAR" - m := map[string]string{} - m["FOO"] = "BAZ" - g.Assert("echo BAZ $BAR").Equal(Inject(s, m)) - }) - - g.It("Should not replace vars with single $", func() { - s := "echo $FOO $BAR" - m := map[string]string{} - m["FOO"] = "BAZ" - g.Assert(s).Equal(Inject(s, m)) - }) - - g.It("Should not replace vars in nil map", func() { - s := "echo $$FOO $BAR" - g.Assert(s).Equal(Inject(s, nil)) - }) - }) -} - -func Test_InjectSafe(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Safely Inject params", func() { - - m := map[string]string{} - m["TOKEN"] = "FOO" - m["SECRET"] = "BAR" - c, _ := parse(InjectSafe(yml, m)) - - g.It("Should replace vars in notify section", func() { - g.Assert(c.Deploy["digital_ocean"].Config["token"]).Equal("FOO") - g.Assert(c.Deploy["digital_ocean"].Config["secret"]).Equal("BAR") - }) - - g.It("Should not replace vars in script section", func() { - g.Assert(c.Build.Config["commands"].([]interface{})[0]).Equal("echo $$TOKEN") - g.Assert(c.Build.Config["commands"].([]interface{})[1]).Equal("echo $$SECRET") - }) - }) -} - -var yml = ` -build: - image: foo - commands: - - echo $$TOKEN - - echo $$SECRET -deploy: - digital_ocean: - token: $$TOKEN - secret: $$SECRET -` diff --git a/compiler/linter/linter.go b/compiler/linter/linter.go deleted file mode 100644 index 5b2a298..0000000 --- a/compiler/linter/linter.go +++ /dev/null @@ -1,105 +0,0 @@ -package linter - -import ( - "fmt" - "strings" - - "github.com/drone/drone-cli/common" -) - -// lintRule defines a function that runs lint -// checks against a Yaml Config file. If the rule -// fails it should return an error message. -type lintRule func(*common.Config) error - -var lintRules = [...]lintRule{ - expectBuild, - expectImage, - expectCommand, - expectTrustedSetup, - expectTrustedClone, - expectTrustedPublish, - expectTrustedDeploy, - expectTrustedNotify, -} - -// Lint runs all lint rules against the Yaml Config. -func Lint(c *common.Config) error { - for _, rule := range lintRules { - err := rule(c) - if err != nil { - return err - } - } - return nil -} - -// lint rule that fails when no build is defined -func expectBuild(c *common.Config) error { - if c.Build == nil { - return fmt.Errorf("Yaml must define a build section") - } - return nil -} - -// lint rule that fails when no build image is defined -func expectImage(c *common.Config) error { - if len(c.Build.Image) == 0 { - return fmt.Errorf("Yaml must define a build image") - } - return nil -} - -// lint rule that fails when no build commands are defined -func expectCommand(c *common.Config) error { - if c.Build.Config == nil || c.Build.Config["commands"] == nil { - return fmt.Errorf("Yaml must define build commands") - } - return nil -} - -// lint rule that fails when a non-trusted clone plugin is used. -func expectTrustedClone(c *common.Config) error { - if c.Clone != nil && strings.Contains(c.Clone.Image, "/") { - return fmt.Errorf("Yaml must use trusted clone plugins") - } - return nil -} - -// lint rule that fails when a non-trusted clone plugin is used. -func expectTrustedSetup(c *common.Config) error { - if c.Setup != nil && strings.Contains(c.Setup.Image, "/") { - return fmt.Errorf("Yaml must use trusted setup plugins") - } - return nil -} - -// lint rule that fails when a non-trusted publish plugin is used. -func expectTrustedPublish(c *common.Config) error { - for _, step := range c.Publish { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted publish plugins") - } - } - return nil -} - -// lint rule that fails when a non-trusted deploy plugin is used. -func expectTrustedDeploy(c *common.Config) error { - for _, step := range c.Deploy { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted deploy plugins") - } - } - return nil -} - -// lint rule that fails when a non-trusted notify plugin is used. -func expectTrustedNotify(c *common.Config) error { - for _, step := range c.Notify { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted notify plugins") - } - } - return nil -} diff --git a/compiler/linter/linter_test.go b/compiler/linter/linter_test.go deleted file mode 100644 index 62ce781..0000000 --- a/compiler/linter/linter_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package linter - -import ( - "testing" - - "github.com/drone/drone-cli/common" - "github.com/franela/goblin" -) - -func Test_Linter(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Linter", func() { - - g.It("Should fail when nil build", func() { - c := &common.Config{} - g.Assert(expectBuild(c) != nil).IsTrue() - }) - - g.It("Should fail when no image", func() { - c := &common.Config{ - Build: &common.Step{}, - } - g.Assert(expectImage(c) != nil).IsTrue() - }) - - g.It("Should fail when no commands", func() { - c := &common.Config{ - Build: &common.Step{}, - } - g.Assert(expectCommand(c) != nil).IsTrue() - }) - - g.It("Should pass when proper Build provided", func() { - c := &common.Config{ - Build: &common.Step{ - Config: map[string]interface{}{ - "commands": []string{"echo hi"}, - }, - }, - } - g.Assert(expectImage(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted setup image", func() { - c := &common.Config{Setup: &common.Step{Image: "foo/bar"}} - g.Assert(expectTrustedSetup(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted clone image", func() { - c := &common.Config{Clone: &common.Step{Image: "foo/bar"}} - g.Assert(expectTrustedClone(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted publish image", func() { - c := &common.Config{} - c.Publish = map[string]*common.Step{} - c.Publish["docker"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedPublish(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted deploy image", func() { - c := &common.Config{} - c.Deploy = map[string]*common.Step{} - c.Deploy["amazon"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedDeploy(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted notify image", func() { - c := &common.Config{} - c.Notify = map[string]*common.Step{} - c.Notify["hipchat"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedNotify(c) != nil).IsTrue() - }) - - g.It("Should pass linter when build properly setup", func() { - c := &common.Config{} - c.Build = &common.Step{} - c.Build.Image = "golang" - c.Build.Config = map[string]interface{}{} - c.Build.Config["commands"] = []string{"go build", "go test"} - c.Publish = map[string]*common.Step{} - c.Publish["docker"] = &common.Step{Image: "docker"} - c.Deploy = map[string]*common.Step{} - c.Deploy["kubernetes"] = &common.Step{Image: "kubernetes"} - c.Notify = map[string]*common.Step{} - c.Notify["email"] = &common.Step{Image: "email"} - g.Assert(Lint(c) == nil).IsTrue() - }) - - }) -} diff --git a/compiler/matrix/matrix.go b/compiler/matrix/matrix.go deleted file mode 100644 index baa2a3b..0000000 --- a/compiler/matrix/matrix.go +++ /dev/null @@ -1,109 +0,0 @@ -package parser - -import ( - "strings" - - "gopkg.in/yaml.v2" -) - -const ( - limitTags = 10 - limitAxis = 25 -) - -// Matrix represents the build matrix. -type Matrix map[string][]string - -// Axis represents a single permutation of entries -// from the build matrix. -type Axis map[string]string - -// String returns a string representation of an Axis as -// a comma-separated list of environment variables. -func (a Axis) String() string { - var envs []string - for k, v := range a { - envs = append(envs, k+"="+v) - } - return strings.Join(envs, ",") -} - -// Parse parses the Matrix section of the yaml file and -// returns a list of axis. -func Parse(raw string) ([]Axis, error) { - matrix, err := parseMatrix(raw) - if err != nil { - return nil, err - } - - // if not a matrix build return an array - // with just the single axis. - if len(matrix) == 0 { - return nil, nil - } - - return Calc(matrix), nil -} - -// Calc calculates the permutations for th build matrix. -// -// Note that this method will cap the number of permutations -// to 25 to prevent an overly expensive calculation. -func Calc(matrix Matrix) []Axis { - // calculate number of permutations and - // extract the list of tags - // (ie go_version, redis_version, etc) - var perm int - var tags []string - for k, v := range matrix { - perm *= len(v) - if perm == 0 { - perm = len(v) - } - tags = append(tags, k) - } - - // structure to hold the transformed - // result set - axisList := []Axis{} - - // for each axis calculate the uniqe - // set of values that should be used. - for p := 0; p < perm; p++ { - axis := map[string]string{} - decr := perm - for i, tag := range tags { - elems := matrix[tag] - decr = decr / len(elems) - elem := p / decr % len(elems) - axis[tag] = elems[elem] - - // enforce a maximum number of tags - // in the build matrix. - if i > limitTags { - break - } - } - - // append to the list of axis. - axisList = append(axisList, axis) - - // enforce a maximum number of axis - // that should be calculated. - if p > limitAxis { - break - } - } - - return axisList -} - -// helper function to parse the Matrix data from -// the raw yaml file. -func parseMatrix(raw string) (Matrix, error) { - data := struct { - Matrix map[string][]string - }{} - err := yaml.Unmarshal([]byte(raw), &data) - return data.Matrix, err -} diff --git a/compiler/matrix/matrix_test.go b/compiler/matrix/matrix_test.go deleted file mode 100644 index e11d47d..0000000 --- a/compiler/matrix/matrix_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/franela/goblin" -) - -func Test_Matrix(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Calculate matrix", func() { - - m := map[string][]string{} - m["go_version"] = []string{"go1", "go1.2"} - m["python_version"] = []string{"3.2", "3.3"} - m["django_version"] = []string{"1.7", "1.7.1", "1.7.2"} - m["redis_version"] = []string{"2.6", "2.8"} - axis := Calc(m) - - g.It("Should calculate permutations", func() { - g.Assert(len(axis)).Equal(24) - }) - - g.It("Should not duplicate permutations", func() { - set := map[string]bool{} - for _, perm := range axis { - set[perm.String()] = true - } - g.Assert(len(set)).Equal(24) - }) - }) -} diff --git a/compiler/node.go b/compiler/node.go new file mode 100644 index 0000000..fb3b4a0 --- /dev/null +++ b/compiler/node.go @@ -0,0 +1,110 @@ +package compiler + +import ( + "sync" + + "github.com/drone/drone-cli/common" + "github.com/samalba/dockerclient" +) + +// Node is an element in the build execution tree. +type Node interface { + Run(*State, *ResultWriter) error +} + +// parallelNode runs a set of build nodes in parallel. +type parallelNode []Node + +func (n parallelNode) Run(s *State, rw *ResultWriter) error { + var wg sync.WaitGroup + for _, node := range n { + wg.Add(1) + + go func(node Node) { + defer wg.Done() + node.Run(s, rw) + }(node) + } + wg.Wait() + return nil +} + +// serialNode runs a set of build nodes in sequential order. +type serialNode []Node + +func (n serialNode) Run(s *State, rw *ResultWriter) error { + for _, node := range n { + err := node.Run(s, rw) + if err != nil { + return err + } + if rw.ExitCode() != 0 { + return nil + } + } + return nil +} + +// batchNode runs a container and blocks until complete. +type batchNode struct { + step *common.Step +} + +func (n *batchNode) Run(s *State, rw *ResultWriter) error { + host := toHostConfig(n.step) + conf := toContainerConfig(n.step) + if n.step.Config != nil { + conf.Cmd = toCommand(s, n.step) + conf.Entrypoint = []string{} + } + + return nil +} + +// serviceNode runs a container, blocking, writes output, uses config section +type serviceNode struct { + step *common.Step +} + +func (n *serviceNode) Run(s *State, rw *ResultWriter) error { + host := toHostConfig(n.step) + conf := toContainerConfig(n.step) + + return nil +} + +// cloneNode runs a clone container, blocking, uses build section +type cloneNode struct { + step *common.Step +} + +func (n *cloneNode) Run(s *State, rw *ResultWriter) error { + return nil +} + +// buildNode runs a build container, discards the step.config section +type buildNode struct { + host *dockerclient.HostConfig + conf *dockerclient.ContainerConfig +} + +func newBuildNode(step *common.Step) *buildNode { + host := toHostConfig(step) + conf := toContainerConfig(step) + conf.Entrypoint = []string{"/bin/bash"} + conf.Cmd = []string{"/drone/bin/build.sh"} + return &buildNode{host, conf} +} + +func (n *buildNode) Run(s *State, rw *ResultWriter) error { + return nil +} + +// setupNode container, discards the step.config section +type setupNode struct { + step *common.Step +} + +func (n *setupNode) Run(s *State, rw *ResultWriter) error { + return nil +} diff --git a/compiler/parse/node.go b/compiler/parse/node.go new file mode 100644 index 0000000..b070f8a --- /dev/null +++ b/compiler/parse/node.go @@ -0,0 +1,99 @@ +package parser + +import ( + "sync" + + "github.com/drone/drone-cli/common" +) + +// parallelNode runs a set of build nodes in parallel. +type parallelNode struct { + nodes []Node +} + +func (n *parallelNode) Run(s *State, rw *ResultWriter) error { + for _, node := range n.nodes { + err := node.Run(s, rw) + if err != nil { + return err + } + if rw.ExitCode() != 0 { + return nil + } + } + return nil +} + +// serialNode runs a set of build nodes in sequential order. +type serialNode struct { + nodes []Node +} + +func (n *serialNode) Run(s *State, rw *ResultWriter) error { + var wg sync.WaitGroup + for _, task := range n.nodes { + wg.Add(1) + + go func(task Node) { + defer wg.Done() + task.Run(s, rw) + }(task) + } + wg.Wait() + return nil +} + +// batchNode runs a container and blocks until complete. +type batchNode struct { + step *common.Step +} + +func (n *batchNode) Run(s *State, rw *ResultWriter) error { + host := toHostConfig(n.step) + conf := toContainerConfig(n.step) + if n.step.Config != nil { + conf.Cmd = toCommand(s, n.step) + conf.Entrypoint = []string{} + } + + return nil +} + +// serviceNode runs a container, blocking, writes output, uses config section +type serviceNode struct { + step *common.Step +} + +func (n *serviceNode) Run(s *State, rw *ResultWriter) error { + host := toHostConfig(n.step) + conf := toContainerConfig(n.step) + + return nil +} + +// cloneNode runs a clone container, blocking, uses build section +type cloneNode struct { + step *common.Step +} + +func (n *cloneNode) Run(s *State, rw *ResultWriter) error { + return nil +} + +// buildNode runs a build container, discards the step.config section +type buildNode struct { + step *common.Step +} + +func (n *buildNode) Run(s *State, rw *ResultWriter) error { + return nil +} + +// setupNode container, discards the step.config section +type setupNode struct { + step *common.Step +} + +func (n *setupNode) Run(s *State, rw *ResultWriter) error { + return nil +} diff --git a/parser/parser.go b/compiler/parse/parse.go similarity index 92% rename from parser/parser.go rename to compiler/parse/parse.go index f52ca54..39f50d8 100644 --- a/parser/parser.go +++ b/compiler/parse/parse.go @@ -3,8 +3,6 @@ package parser import ( "github.com/drone/drone-cli/common" "github.com/drone/drone-cli/common/matrix" - "github.com/drone/drone-cli/parser/inject" - "gopkg.in/yaml.v2" ) @@ -29,7 +27,7 @@ func Parse(raw string) ([]*common.Config, error) { for _, ax := range axis { // inject the matrix values into the raw script - injected := inject.Inject(raw, ax) + injected := Inject(raw, ax) conf, err := parse(injected) if err != nil { return nil, err diff --git a/compiler/parse/result.go b/compiler/parse/result.go new file mode 100644 index 0000000..e5295ba --- /dev/null +++ b/compiler/parse/result.go @@ -0,0 +1,14 @@ +package parser + +// ResultWriter represents the result from a build request. +type ResultWriter interface { + + // Write writes the build stdout and stderr to the result. + Write([]byte) (int, error) + + // WriteExitCode writes the build exit status to the result. + WriteExitCode(int) + + // ExitCode returns the build exit status. + ExitCode() int +} diff --git a/compiler/parse/tree.go b/compiler/parse/tree.go new file mode 100644 index 0000000..13c6437 --- /dev/null +++ b/compiler/parse/tree.go @@ -0,0 +1,8 @@ +package parser + +type Tree struct { + Build Node + Publish Node + Deploy Node + Notify Node +} diff --git a/compiler/result.go b/compiler/result.go new file mode 100644 index 0000000..e45ee7a --- /dev/null +++ b/compiler/result.go @@ -0,0 +1,31 @@ +package compiler + +import "io" + +// ResultWriter represents the result from a build request. +type ResultWriter struct { + writer io.Writer + exitCode int +} + +// NewResultWriter returns a new ResultWriter. +func NewResultWriter(w io.Writer) *ResultWriter { + return &ResultWriter{writer: w} +} + +// Write writes the build stdout and stderr to the result. +func (r *ResultWriter) Write(p []byte) (n int, err error) { + return r.writer.Write(p) +} + +// WriteExitCode writes the build exit status to the result. +func (r *ResultWriter) WriteExitCode(code int) { + if code != 0 { + r.exitCode = code + } +} + +// ExitCode returns the build exit status. +func (r *ResultWriter) ExitCode() int { + return r.exitCode +} diff --git a/compiler/state.go b/compiler/state.go new file mode 100644 index 0000000..f5541b6 --- /dev/null +++ b/compiler/state.go @@ -0,0 +1,35 @@ +package compiler + +import ( + "io" + + "github.com/drone/drone-cli/common" + "github.com/samalba/dockerclient" +) + +// State represents the execution state of the +// running build. +type State struct { + Repo *common.Repo + Commit *common.Repo + Config *common.Config + Clone *common.Clone + + Client dockerclient.Client +} + +func (*State) Run() (string, error) { + return "", nil +} + +func (*State) Remove(string) error { + return nil +} + +func (*State) RemoveAll() error { + return nil +} + +func (*State) Logs(string) (io.ReadCloser, error) { + return nil, nil +} diff --git a/compiler/util.go b/compiler/util.go new file mode 100644 index 0000000..6b9f6c3 --- /dev/null +++ b/compiler/util.go @@ -0,0 +1,68 @@ +package compiler + +import ( + "encoding/json" + + "github.com/drone/drone-cli/common" + "github.com/samalba/dockerclient" +) + +// helper function to encode the build step to +// a json string. Primarily used for plugins, which +// expect a json encoded string in stdin or arg[1]. +func toCommand(state *State, step *common.Step) []string { + p := payload{ + state.Repo, + state.Commit, + state.Clone, + step.Config, + } + return []string{p.Encode()} +} + +// helper function that converts a build step to +// a hostConfig for use with the dockerclient +func toHostConfig(step *common.Step) *dockerclient.HostConfig { + return &dockerclient.HostConfig{ + Privileged: step.Privileged, + NetworkMode: step.NetworkMode, + } +} + +// helper function that converts the build step to +// a containerConfig for use with the dockerclient +func toContainerConfig(step *common.Step) *dockerclient.ContainerConfig { + config := &dockerclient.ContainerConfig{ + Image: step.Image, + Env: step.Environment, + Cmd: step.Command, + Entrypoint: step.Entrypoint, + WorkingDir: step.WorkingDir, + } + + if len(step.Volumes) != 0 { + config.Volumes = map[string]struct{}{} + for _, path := range step.Volumes { + config.Volumes[path] = struct{}{} + } + } + + return config +} + +// payload represents the payload of a plugin +// that is serialized and sent to the plugin in JSON +// format via stdin or arg[1]. +type payload struct { + Repo *common.Repo `json:"repo"` + Commit *common.Repo `json:"commit"` + Clone *common.Clone `json:"clone"` + + Config map[string]interface{} `json:"vargs"` +} + +// Encode encodes the payload in JSON format. +func (p *payload) Encode() string { + out, _ := json.Marshal(p) + return string(out) +} diff --git a/parser/linter/linter.go b/parser/linter/linter.go deleted file mode 100644 index 5b2a298..0000000 --- a/parser/linter/linter.go +++ /dev/null @@ -1,105 +0,0 @@ -package linter - -import ( - "fmt" - "strings" - - "github.com/drone/drone-cli/common" -) - -// lintRule defines a function that runs lint -// checks against a Yaml Config file. If the rule -// fails it should return an error message. -type lintRule func(*common.Config) error - -var lintRules = [...]lintRule{ - expectBuild, - expectImage, - expectCommand, - expectTrustedSetup, - expectTrustedClone, - expectTrustedPublish, - expectTrustedDeploy, - expectTrustedNotify, -} - -// Lint runs all lint rules against the Yaml Config. -func Lint(c *common.Config) error { - for _, rule := range lintRules { - err := rule(c) - if err != nil { - return err - } - } - return nil -} - -// lint rule that fails when no build is defined -func expectBuild(c *common.Config) error { - if c.Build == nil { - return fmt.Errorf("Yaml must define a build section") - } - return nil -} - -// lint rule that fails when no build image is defined -func expectImage(c *common.Config) error { - if len(c.Build.Image) == 0 { - return fmt.Errorf("Yaml must define a build image") - } - return nil -} - -// lint rule that fails when no build commands are defined -func expectCommand(c *common.Config) error { - if c.Build.Config == nil || c.Build.Config["commands"] == nil { - return fmt.Errorf("Yaml must define build commands") - } - return nil -} - -// lint rule that fails when a non-trusted clone plugin is used. -func expectTrustedClone(c *common.Config) error { - if c.Clone != nil && strings.Contains(c.Clone.Image, "/") { - return fmt.Errorf("Yaml must use trusted clone plugins") - } - return nil -} - -// lint rule that fails when a non-trusted clone plugin is used. -func expectTrustedSetup(c *common.Config) error { - if c.Setup != nil && strings.Contains(c.Setup.Image, "/") { - return fmt.Errorf("Yaml must use trusted setup plugins") - } - return nil -} - -// lint rule that fails when a non-trusted publish plugin is used. -func expectTrustedPublish(c *common.Config) error { - for _, step := range c.Publish { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted publish plugins") - } - } - return nil -} - -// lint rule that fails when a non-trusted deploy plugin is used. -func expectTrustedDeploy(c *common.Config) error { - for _, step := range c.Deploy { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted deploy plugins") - } - } - return nil -} - -// lint rule that fails when a non-trusted notify plugin is used. -func expectTrustedNotify(c *common.Config) error { - for _, step := range c.Notify { - if strings.Contains(step.Image, "/") { - return fmt.Errorf("Yaml must use trusted notify plugins") - } - } - return nil -} diff --git a/parser/linter/linter_test.go b/parser/linter/linter_test.go deleted file mode 100644 index 62ce781..0000000 --- a/parser/linter/linter_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package linter - -import ( - "testing" - - "github.com/drone/drone-cli/common" - "github.com/franela/goblin" -) - -func Test_Linter(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Linter", func() { - - g.It("Should fail when nil build", func() { - c := &common.Config{} - g.Assert(expectBuild(c) != nil).IsTrue() - }) - - g.It("Should fail when no image", func() { - c := &common.Config{ - Build: &common.Step{}, - } - g.Assert(expectImage(c) != nil).IsTrue() - }) - - g.It("Should fail when no commands", func() { - c := &common.Config{ - Build: &common.Step{}, - } - g.Assert(expectCommand(c) != nil).IsTrue() - }) - - g.It("Should pass when proper Build provided", func() { - c := &common.Config{ - Build: &common.Step{ - Config: map[string]interface{}{ - "commands": []string{"echo hi"}, - }, - }, - } - g.Assert(expectImage(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted setup image", func() { - c := &common.Config{Setup: &common.Step{Image: "foo/bar"}} - g.Assert(expectTrustedSetup(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted clone image", func() { - c := &common.Config{Clone: &common.Step{Image: "foo/bar"}} - g.Assert(expectTrustedClone(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted publish image", func() { - c := &common.Config{} - c.Publish = map[string]*common.Step{} - c.Publish["docker"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedPublish(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted deploy image", func() { - c := &common.Config{} - c.Deploy = map[string]*common.Step{} - c.Deploy["amazon"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedDeploy(c) != nil).IsTrue() - }) - - g.It("Should fail when untrusted notify image", func() { - c := &common.Config{} - c.Notify = map[string]*common.Step{} - c.Notify["hipchat"] = &common.Step{Image: "foo/bar"} - g.Assert(expectTrustedNotify(c) != nil).IsTrue() - }) - - g.It("Should pass linter when build properly setup", func() { - c := &common.Config{} - c.Build = &common.Step{} - c.Build.Image = "golang" - c.Build.Config = map[string]interface{}{} - c.Build.Config["commands"] = []string{"go build", "go test"} - c.Publish = map[string]*common.Step{} - c.Publish["docker"] = &common.Step{Image: "docker"} - c.Deploy = map[string]*common.Step{} - c.Deploy["kubernetes"] = &common.Step{Image: "kubernetes"} - c.Notify = map[string]*common.Step{} - c.Notify["email"] = &common.Step{Image: "email"} - g.Assert(Lint(c) == nil).IsTrue() - }) - - }) -}