diff --git a/compiler/inject/inject.go b/compiler/inject/inject.go new file mode 100644 index 0000000..26752a9 --- /dev/null +++ b/compiler/inject/inject.go @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..9726635 --- /dev/null +++ b/compiler/inject/inject_test.go @@ -0,0 +1,67 @@ +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/matrix/matrix.go b/compiler/matrix/matrix.go index bb83758..baa2a3b 100644 --- a/compiler/matrix/matrix.go +++ b/compiler/matrix/matrix.go @@ -1,4 +1,4 @@ -package matrix +package parser import ( "strings" @@ -18,9 +18,8 @@ type Matrix map[string][]string // 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. +// 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 { @@ -31,12 +30,8 @@ func (a Axis) String() string { // Parse parses the Matrix section of the yaml file and // returns a list of axis. -// -// Note that this method will cap the result set to -// avoid caclulating permutations on too large of a -// dataset and consuming too many system resources. func Parse(raw string) ([]Axis, error) { - matrix, err := parse(raw) + matrix, err := parseMatrix(raw) if err != nil { return nil, err } @@ -47,6 +42,14 @@ func Parse(raw string) ([]Axis, error) { 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) @@ -75,7 +78,7 @@ func Parse(raw string) ([]Axis, error) { elem := p / decr % len(elems) axis[tag] = elems[elem] - // enforce a maximum number of rows + // enforce a maximum number of tags // in the build matrix. if i > limitTags { break @@ -92,12 +95,12 @@ func Parse(raw string) ([]Axis, error) { } } - return axisList, nil + return axisList } // helper function to parse the Matrix data from // the raw yaml file. -func parse(raw string) (Matrix, error) { +func parseMatrix(raw string) (Matrix, error) { data := struct { Matrix map[string][]string }{} diff --git a/compiler/matrix/matrix_test.go b/compiler/matrix/matrix_test.go index 4c8c6fe..e11d47d 100644 --- a/compiler/matrix/matrix_test.go +++ b/compiler/matrix/matrix_test.go @@ -1,45 +1,33 @@ -package matrix +package parser import ( "testing" + + "github.com/franela/goblin" ) -func Test_Parse(t *testing.T) { - axis, err := Parse(matrix) - if err != nil { - t.Error(err) - return - } +func Test_Matrix(t *testing.T) { - if len(axis) != 24 { - t.Errorf("Expected 24 axis, got %d", len(axis)) - } + g := goblin.Goblin(t) + g.Describe("Calculate matrix", func() { - unique := map[string]bool{} - for _, a := range axis { - unique[a.String()] = true - } + 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) - if len(unique) != 24 { - t.Errorf("Expected 24 unique permutations in matrix, got %d", len(unique)) - } + 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) + }) + }) } - -var matrix = ` -build: - image: $$python_version $$redis_version $$django_version $$go_version -matrix: - python_version: - - 3.2 - - 3.3 - redis_version: - - 2.6 - - 2.8 - django_version: - - 1.7 - - 1.7.1 - - 1.7.2 - go_version: - - go1 - - go1.2 -`