add yaml parser, build execution code. not yet hooked up

This commit is contained in:
Brad Rydzewski 2015-04-16 00:10:17 -07:00
parent cd18645784
commit 5816f1156e
18 changed files with 1767 additions and 0 deletions

153
builder/build.go Normal file
View file

@ -0,0 +1,153 @@
package builder
import (
"io"
"sync"
"time"
"github.com/drone/drone/common"
"github.com/samalba/dockerclient"
)
// B is a type passed to build nodes. B implements an io.Writer
// and will accumulate build output during execution.
type B struct {
sync.Mutex
Repo *common.Repo
Build *common.Build
Task *common.Task
Clone *common.Clone
client dockerclient.Client
writer io.Writer
exitCode int
start time.Time // Time build started
duration time.Duration
timerOn bool
containers []string
}
// NewB returns a new Build context.
func NewB(client dockerclient.Client, w io.Writer) *B {
return &B{
client: client,
writer: w,
}
}
// Run creates and runs a Docker container.
func (b *B) Run(conf *dockerclient.ContainerConfig) (string, error) {
b.Lock()
defer b.Unlock()
name, err := b.client.CreateContainer(conf, "")
if err != nil {
// on error try to pull the Docker image.
// note that this may not be the cause of
// the error, but we'll try just in case.
b.client.PullImage(conf.Image, nil)
// then try to re-create
name, err = b.client.CreateContainer(conf, "")
if err != nil {
return name, err
}
}
b.containers = append(b.containers, name)
err = b.client.StartContainer(name, &conf.HostConfig)
if err != nil {
return name, err
}
return name, nil
}
// Inspect inspects the running Docker container and returns
// the contianer runtime information and state.
func (b *B) Inspect(name string) (*dockerclient.ContainerInfo, error) {
return b.client.InspectContainer(name)
}
// Remove stops and removes the named Docker container.
func (b *B) Remove(name string) {
b.client.StopContainer(name, 5)
b.client.KillContainer(name, "9")
b.client.RemoveContainer(name, true, true)
}
// RemoveAll stops and removes all Docker containers that were
// created and started during the build process.
func (b *B) RemoveAll() {
b.Lock()
defer b.Unlock()
for i := len(b.containers) - 1; i >= 0; i-- {
b.Remove(b.containers[i])
}
}
// Logs returns an io.ReadCloser for reading the build stream of
// the named Docker container.
func (b *B) Logs(name string) (io.ReadCloser, error) {
opts := dockerclient.LogOptions{
Follow: true,
Stderr: true,
Stdout: true,
Timestamps: false,
}
return b.client.ContainerLogs(name, &opts)
}
// StartTimer starts timing a build. This function is called automatically
// before a build starts, but it can also used to resume timing after
// a call to StopTimer.
func (b *B) StartTimer() {
b.Lock()
defer b.Unlock()
if !b.timerOn {
b.start = time.Now()
b.timerOn = true
}
}
// StopTimer stops timing a build. This can be used to pause the timer
// while performing complex initialization that you don't want to measure.
func (b *B) StopTimer() {
b.Lock()
defer b.Unlock()
if b.timerOn {
b.duration += time.Now().Sub(b.start)
b.timerOn = false
}
}
// Write writes the build stdout and stderr to the result.
func (b *B) Write(p []byte) (n int, err error) {
return b.writer.Write(p)
}
// Exit writes the function as having failed but continues execution.
func (b *B) Exit(code int) {
b.Lock()
defer b.Unlock()
if code != 0 { // never override non-zero exit
b.exitCode = code
}
}
// ExitCode reports the build exit code. A non-zero value indicates
// the build exited with errors.
func (b *B) ExitCode() int {
b.Lock()
defer b.Unlock()
return b.exitCode
}

81
builder/builder.go Normal file
View file

@ -0,0 +1,81 @@
package builder
import "github.com/drone/drone/common"
// Builder represents a build execution tree.
type Builder struct {
builds Node
deploy Node
notify Node
}
// Run runs the build, deploy and notify nodes
// in the build tree.
func (b *Builder) Run(build *B) error {
var err error
err = b.RunBuild(build)
if err != nil {
return err
}
err = b.RunDeploy(build)
if err != nil {
return err
}
return b.RunNotify(build)
}
// RunBuild runs only the build node.
func (b *Builder) RunBuild(build *B) error {
return b.builds.Run(build)
}
// RunDeploy runs only the deploy node.
func (b *Builder) RunDeploy(build *B) error {
return b.notify.Run(build)
}
// RunNotify runs on the notify node.
func (b *Builder) RunNotify(build *B) error {
return b.notify.Run(build)
}
func (b *Builder) HasDeploy() bool {
return len(b.deploy.(serialNode)) != 0
}
func (b *Builder) HasNotify() bool {
return len(b.notify.(serialNode)) != 0
}
// Load loads a build configuration file.
func Load(conf *common.Config) *Builder {
var (
builds []Node
deploys []Node
notifys []Node
)
for _, step := range conf.Compose {
builds = append(builds, &serviceNode{step}) // compose
}
builds = append(builds, &batchNode{conf.Setup}) // setup
if conf.Clone != nil {
builds = append(builds, &batchNode{conf.Clone}) // clone
}
builds = append(builds, &batchNode{conf.Build}) // 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),
}
}

124
builder/copy.go Normal file
View file

@ -0,0 +1,124 @@
package builder
import (
"encoding/binary"
"errors"
"io"
)
const (
StdWriterPrefixLen = 8
StdWriterFdIndex = 0
StdWriterSizeIndex = 4
)
type StdType [StdWriterPrefixLen]byte
var (
Stdin StdType = StdType{0: 0}
Stdout StdType = StdType{0: 1}
Stderr StdType = StdType{0: 2}
)
type StdWriter struct {
io.Writer
prefix StdType
sizeBuf []byte
}
var ErrInvalidStdHeader = errors.New("Unrecognized input header")
// StdCopy is a modified version of io.Copy.
//
// StdCopy will demultiplex `src`, assuming that it contains two streams,
// previously multiplexed together using a StdWriter instance.
// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`.
//
// StdCopy will read until it hits EOF on `src`. It will then return a nil error.
// In other words: if `err` is non nil, it indicates a real underlying error.
//
// `written` will hold the total number of bytes written to `dstout` and `dsterr`.
func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) {
var (
buf = make([]byte, 32*1024+StdWriterPrefixLen+1)
bufLen = len(buf)
nr, nw int
er, ew error
out io.Writer
frameSize int
)
for {
// Make sure we have at least a full header
for nr < StdWriterPrefixLen {
var nr2 int
nr2, er = src.Read(buf[nr:])
nr += nr2
if er == io.EOF {
if nr < StdWriterPrefixLen {
return written, nil
}
break
}
if er != nil {
return 0, er
}
}
// Check the first byte to know where to write
switch buf[StdWriterFdIndex] {
case 0:
fallthrough
case 1:
// Write on stdout
out = dstout
case 2:
// Write on stderr
out = dsterr
default:
return 0, ErrInvalidStdHeader
}
// Retrieve the size of the frame
frameSize = int(binary.BigEndian.Uint32(buf[StdWriterSizeIndex : StdWriterSizeIndex+4]))
// Check if the buffer is big enough to read the frame.
// Extend it if necessary.
if frameSize+StdWriterPrefixLen > bufLen {
buf = append(buf, make([]byte, frameSize+StdWriterPrefixLen-bufLen+1)...)
bufLen = len(buf)
}
// While the amount of bytes read is less than the size of the frame + header, we keep reading
for nr < frameSize+StdWriterPrefixLen {
var nr2 int
nr2, er = src.Read(buf[nr:])
nr += nr2
if er == io.EOF {
if nr < frameSize+StdWriterPrefixLen {
return written, nil
}
break
}
if er != nil {
return 0, er
}
}
// Write the retrieved frame (without header)
nw, ew = out.Write(buf[StdWriterPrefixLen : frameSize+StdWriterPrefixLen])
if ew != nil {
return 0, ew
}
// If the frame has not been fully written: error
if nw != frameSize {
return 0, io.ErrShortWrite
}
written += int64(nw)
// Move the rest of the buffer to the beginning
copy(buf, buf[frameSize+StdWriterPrefixLen:])
// Move the index
nr -= frameSize + StdWriterPrefixLen
}
}

View file

@ -0,0 +1,110 @@
package docker
import (
"errors"
log "github.com/Sirupsen/logrus"
"github.com/samalba/dockerclient"
)
var errNop = errors.New("Operation not supported")
// Ambassador is a wrapper around the Docker client that
// provides a shared volume and network for all containers.
type Ambassador struct {
dockerclient.Client
name string
}
// NewAmbassador creates an ambassador container and wraps the Docker
// client to inject the ambassador volume and network into containers.
func NewAmbassador(client dockerclient.Client) (_ *Ambassador, err error) {
amb := &Ambassador{client, ""}
conf := &dockerclient.ContainerConfig{}
host := &dockerclient.HostConfig{}
conf.Entrypoint = []string{"/bin/sleep"}
conf.Cmd = []string{"86400"}
conf.Image = "busybox"
conf.Volumes = map[string]struct{}{}
conf.Volumes["/drone"] = struct{}{}
// creates the ambassador container
amb.name, err = client.CreateContainer(conf, "")
if err != nil {
log.WithField("ambassador", conf.Image).Errorln(err)
// on failure attempts to pull the image
client.PullImage(conf.Image, nil)
// then attempts to re-create the container
amb.name, err = client.CreateContainer(conf, "")
if err != nil {
log.WithField("ambassador", conf.Image).Errorln(err)
return nil, err
}
}
err = client.StartContainer(amb.name, host)
if err != nil {
log.WithField("ambassador", conf.Image).Errorln(err)
}
return amb, err
}
// Destroy stops and deletes the ambassador container.
func (c *Ambassador) Destroy() error {
c.Client.StopContainer(c.name, 5)
c.Client.KillContainer(c.name, "9")
return c.Client.RemoveContainer(c.name, true, true)
}
// CreateContainer creates a container.
func (c *Ambassador) CreateContainer(conf *dockerclient.ContainerConfig, name string) (string, error) {
log.WithField("image", conf.Image).Infoln("create container")
// add the affinity flag for swarm
conf.Env = append(conf.Env, "affinity:container=="+c.name)
id, err := c.Client.CreateContainer(conf, name)
if err != nil {
log.WithField("image", conf.Image).Errorln(err)
}
return id, err
}
// StartContainer starts a container. The ambassador volume
// is automatically linked. The ambassador network is linked
// iff a network mode is not already specified.
func (c *Ambassador) StartContainer(id string, conf *dockerclient.HostConfig) error {
log.WithField("container", id).Debugln("start container")
conf.VolumesFrom = append(conf.VolumesFrom, c.name)
if len(conf.NetworkMode) == 0 {
conf.NetworkMode = "container:" + c.name
}
err := c.Client.StartContainer(id, conf)
if err != nil {
log.WithField("container", id).Errorln(err)
}
return err
}
// StopContainer stops a container.
func (c *Ambassador) StopContainer(id string, timeout int) error {
log.WithField("container", id).Debugln("stop container")
err := c.Client.StopContainer(id, timeout)
if err != nil {
log.WithField("container", id).Errorln(err)
}
return err
}
// PullImage pulls an image.
func (c *Ambassador) PullImage(name string, auth *dockerclient.AuthConfig) error {
log.WithField("image", name).Debugln("pull image")
err := c.Client.PullImage(name, auth)
if err != nil {
log.WithField("image", name).Errorln(err)
}
return err
}

103
builder/node.go Normal file
View file

@ -0,0 +1,103 @@
package builder
import (
"sync"
"github.com/drone/drone/common"
)
// Node is an element in the build execution tree.
type Node interface {
Run(*B) error
}
// parallelNode runs a set of build nodes in parallel.
type parallelNode []Node
func (n parallelNode) Run(b *B) error {
var wg sync.WaitGroup
for _, node := range n {
wg.Add(1)
go func(node Node) {
defer wg.Done()
node.Run(b)
}(node)
}
wg.Wait()
return nil
}
// serialNode runs a set of build nodes in sequential order.
type serialNode []Node
func (n serialNode) Run(b *B) error {
for _, node := range n {
err := node.Run(b)
if err != nil {
return err
}
if b.ExitCode() != 0 {
return nil
}
}
return nil
}
// batchNode runs a container and blocks until complete.
type batchNode struct {
step *common.Step
}
func (n *batchNode) Run(b *B) error {
// switch {
// case n.step.Condition == nil:
// case n.step.Condition.MatchBranch(b.Commit.Branch) == false:
// return nil
// case n.step.Condition.MatchOwner(b.Repo.Owner) == false:
// return nil
// }
// creates the container conf
conf := toContainerConfig(n.step)
if n.step.Config != nil {
conf.Cmd = toCommand(b, n.step)
}
// inject environment vars
injectEnv(b, conf)
name, err := b.Run(conf)
if err != nil {
return err
}
// streams the logs to the build results
rc, err := b.Logs(name)
if err != nil {
return err
}
StdCopy(b, b, rc)
//io.Copy(b, rc)
// inspects the results and writes the
// build result exit code
info, err := b.Inspect(name)
if err != nil {
return err
}
b.Exit(info.State.ExitCode)
return nil
}
// serviceNode runs a container, blocking, writes output, uses config section
type serviceNode struct {
step *common.Step
}
func (n *serviceNode) Run(b *B) error {
conf := toContainerConfig(n.step)
_, err := b.Run(conf)
return err
}

89
builder/pool/pool.go Normal file
View file

@ -0,0 +1,89 @@
package pool
import (
"sync"
"github.com/samalba/dockerclient"
)
// TODO (bradrydzewski) ability to cancel work.
// TODO (bradrydzewski) ability to remove a worker.
type Pool struct {
sync.Mutex
clients map[dockerclient.Client]bool
clientc chan dockerclient.Client
}
func New() *Pool {
return &Pool{
clients: make(map[dockerclient.Client]bool),
clientc: make(chan dockerclient.Client, 999),
}
}
// Allocate allocates a client to the pool to
// be available to accept work.
func (p *Pool) Allocate(c dockerclient.Client) bool {
if p.IsAllocated(c) {
return false
}
p.Lock()
p.clients[c] = true
p.Unlock()
p.clientc <- c
return true
}
// IsAllocated is a helper function that returns
// true if the client is currently allocated to
// the Pool.
func (p *Pool) IsAllocated(c dockerclient.Client) bool {
p.Lock()
defer p.Unlock()
_, ok := p.clients[c]
return ok
}
// Deallocate removes the worker from the pool of
// available clients. If the client is currently
// reserved and performing work it will finish,
// but no longer be given new work.
func (p *Pool) Deallocate(c dockerclient.Client) {
p.Lock()
defer p.Unlock()
delete(p.clients, c)
}
// List returns a list of all Workers currently
// allocated to the Pool.
func (p *Pool) List() []dockerclient.Client {
p.Lock()
defer p.Unlock()
var clients []dockerclient.Client
for c := range p.clients {
clients = append(clients, c)
}
return clients
}
// Reserve reserves the next available worker to
// start doing work. Once work is complete, the
// worker should be released back to the pool.
func (p *Pool) Reserve() <-chan dockerclient.Client {
return p.clientc
}
// Release releases the worker back to the pool
// of available workers.
func (p *Pool) Release(c dockerclient.Client) bool {
if !p.IsAllocated(c) {
return false
}
p.clientc <- c
return true
}

98
builder/util.go Normal file
View file

@ -0,0 +1,98 @@
package builder
import (
"encoding/json"
"fmt"
"strings"
"github.com/drone/drone/common"
"github.com/samalba/dockerclient"
)
// 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,
HostConfig: dockerclient.HostConfig{
Privileged: step.Privileged,
NetworkMode: step.NetworkMode,
},
}
config.Volumes = map[string]struct{}{}
for _, path := range step.Volumes {
if strings.Index(path, ":") == -1 {
continue
}
parts := strings.Split(path, ":")
config.Volumes[parts[1]] = struct{}{}
config.HostConfig.Binds = append(config.HostConfig.Binds, path)
}
return config
}
// helper function to inject drone-specific environment
// variables into the container.
func injectEnv(b *B, conf *dockerclient.ContainerConfig) {
var branch string
var commit string
if b.Build.Commit != nil {
branch = b.Build.Commit.Ref
commit = b.Build.Commit.Sha
} else {
branch = b.Build.PullRequest.Target.Ref
commit = b.Build.PullRequest.Target.Sha
}
conf.Env = append(conf.Env, "DRONE=true")
conf.Env = append(conf.Env, fmt.Sprintf("DRONE_BRANCH=%s", branch))
conf.Env = append(conf.Env, fmt.Sprintf("DRONE_COMMIT=%s", commit))
// for jenkins campatibility
conf.Env = append(conf.Env, "CI=true")
conf.Env = append(conf.Env, fmt.Sprintf("WORKSPACE=%s", b.Clone.Dir))
conf.Env = append(conf.Env, fmt.Sprintf("JOB_NAME=%s/%s", b.Repo.Owner, b.Repo.Name))
conf.Env = append(conf.Env, fmt.Sprintf("BUILD_ID=%d", b.Build.Number))
conf.Env = append(conf.Env, fmt.Sprintf("BUILD_DIR=%s", b.Clone.Dir))
conf.Env = append(conf.Env, fmt.Sprintf("GIT_BRANCH=%s", branch))
conf.Env = append(conf.Env, fmt.Sprintf("GIT_COMMIT=%s", commit))
}
// 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(b *B, step *common.Step) []string {
p := payload{
b.Repo,
b.Build,
b.Task,
b.Clone,
step.Config,
}
return []string{p.Encode()}
}
// 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"`
Build *common.Build `json:"build"`
Task *common.Task `json:"task"`
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)
}

View file

@ -74,3 +74,20 @@ type Remote struct {
FullName string `json:"full_name,omitempty"`
Clone string `json:"clone_url,omitempty"`
}
type Clone struct {
Origin string `json:"origin"`
Remote string `json:"remote"`
Branch string `json:"branch"`
Sha string `json:"sha"`
Ref string `json:"ref"`
Dir string `json:"dir"`
Netrc *Netrc `json:"netrc"`
Keypair *Keypair `json:"keypair"`
}
type Netrc struct {
Machine string `json:"machine"`
Login string `json:"login"`
Password string `json:"user"`
}

104
common/config.go Normal file
View file

@ -0,0 +1,104 @@
package common
import (
"path/filepath"
"strings"
)
// Config represents a repository build configuration.
type Config struct {
Setup *Step
Clone *Step
Build *Step
Compose map[string]*Step
Publish map[string]*Step
Deploy map[string]*Step
Notify map[string]*Step
Matrix Matrix
Axis Axis
}
// 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, " ")
}
// Step represents a step in the build process, including
// the execution environment and parameters.
type Step struct {
Image string
Pull bool
Privileged bool
Environment []string
Entrypoint []string
Command []string
Volumes []string
WorkingDir string `yaml:"working_dir"`
NetworkMode string `yaml:"net"`
// Condition represents a set of conditions that must
// be met in order to execute this step.
Condition *Condition `yaml:"when"`
// Config represents the unique configuration details
// for each plugin.
Config map[string]interface{} `yaml:"config,inline"`
}
// Condition represents a set of conditions that must
// be met in order to proceed with a build or build step.
type Condition struct {
Owner string // Indicates the step should run only for this repo (useful for forks)
Branch string // Indicates the step should run only for this branch
// Indicates the step should only run when the following
// matrix values are present for the sub-build.
Matrix map[string]string
}
// MatchBranch is a helper function that returns true
// if all_branches is true. Else it returns false if a
// branch condition is specified, and the branch does
// not match.
func (c *Condition) MatchBranch(branch string) bool {
if len(c.Branch) == 0 {
return true
}
match, _ := filepath.Match(c.Branch, branch)
return match
}
// MatchOwner is a helper function that returns false
// if an owner condition is specified and the repository
// owner does not match.
//
// This is useful when you want to prevent forks from
// executing deployment, publish or notification steps.
func (c *Condition) MatchOwner(owner string) bool {
if len(c.Owner) == 0 {
return true
}
parts := strings.Split(owner, "/")
switch len(parts) {
case 2:
return c.Owner == parts[0]
case 3:
return c.Owner == parts[1]
default:
return c.Owner == owner
}
}

54
parser/inject/inject.go Normal file
View file

@ -0,0 +1,54 @@
package inject
import (
"sort"
"strings"
"github.com/drone/drone/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
}

View file

@ -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
`

105
parser/lint.go Normal file
View file

@ -0,0 +1,105 @@
package parser
import (
"fmt"
"strings"
"github.com/drone/drone/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 setup 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
}

92
parser/lint_test.go Normal file
View file

@ -0,0 +1,92 @@
package parser
import (
"testing"
"github.com/drone/drone/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()
})
})
}

109
parser/matrix/matrix.go Normal file
View file

@ -0,0 +1,109 @@
package matrix
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
}

View file

@ -0,0 +1,33 @@
package matrix
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)
})
})
}

99
parser/parse.go Normal file
View file

@ -0,0 +1,99 @@
package parser
import (
"github.com/drone/drone/common"
"github.com/drone/drone/parser/inject"
"github.com/drone/drone/parser/matrix"
"gopkg.in/yaml.v2"
)
// Opts specifies parser options that will permit
// or deny certain Yaml settings.
type Opts struct {
Volumes bool
Network bool
Privileged bool
}
var defaultOpts = &Opts{
Volumes: false,
Network: false,
Privileged: false,
}
// Parse parses a build matrix and returns
// a list of build configurations for each axis
// using the default parsing options.
func Parse(raw string) ([]*common.Config, error) {
return ParseOpts(raw, defaultOpts)
}
// ParseOpts parses a build matrix and returns
// a list of build configurations for each axis
// using the provided parsing options.
func ParseOpts(raw string, opts *Opts) ([]*common.Config, error) {
confs, err := parse(raw)
if err != nil {
return nil, err
}
for _, conf := range confs {
err := Lint(conf)
if err != nil {
return nil, err
}
transformSetup(conf)
transformClone(conf)
transformBuild(conf)
transformImages(conf)
transformDockerPlugin(conf)
if !opts.Network {
rmNetwork(conf)
}
if !opts.Volumes {
rmVolumes(conf)
}
if !opts.Privileged {
rmPrivileged(conf)
}
}
return confs, nil
}
// helper function to parse a matrix configuraiton file.
func parse(raw string) ([]*common.Config, error) {
axis, err := matrix.Parse(raw)
if err != nil {
return nil, err
}
confs := []*common.Config{}
// when no matrix values exist we should return
// a single config value with an empty label.
if len(axis) == 0 {
conf, err := parseYaml(raw)
if err != nil {
return nil, err
}
confs = append(confs, conf)
}
for _, ax := range axis {
// inject the matrix values into the raw script
injected := inject.Inject(raw, ax)
conf, err := parseYaml(injected)
if err != nil {
return nil, err
}
conf.Axis = common.Axis(ax)
confs = append(confs, conf)
}
return confs, nil
}
// helper funtion to parse a yaml configuration file.
func parseYaml(raw string) (*common.Config, error) {
conf := &common.Config{}
err := yaml.Unmarshal([]byte(raw), conf)
return conf, err
}

183
parser/trans.go Normal file
View file

@ -0,0 +1,183 @@
package parser
import (
"strings"
"github.com/drone/drone/common"
)
// transformRule applies a check or transformation rule
// to the build configuration.
type transformRule func(*common.Config)
// Transform executes the default transformers that
// ensure the minimal Yaml configuration is in place
// and correctly configured.
func Transform(c *common.Config) {
transformSetup(c)
transformClone(c)
transformBuild(c)
transformImages(c)
transformDockerPlugin(c)
}
// TransformSafe executes all transformers that remove
// privileged options from the Yaml.
func TransformSafe(c *common.Config) {
rmPrivileged(c)
rmVolumes(c)
rmNetwork(c)
}
// transformSetup is a transformer that adds a default
// setup step if none exists.
func transformSetup(c *common.Config) {
c.Setup = &common.Step{}
c.Setup.Image = "plugins/drone-build"
c.Setup.Config = c.Build.Config
}
// transformClone is a transformer that adds a default
// clone step if none exists.
func transformClone(c *common.Config) {
if c.Clone == nil {
c.Clone = &common.Step{}
}
if len(c.Clone.Image) == 0 {
c.Clone.Image = "plugins/drone-git"
c.Clone.Volumes = nil
c.Clone.NetworkMode = ""
}
if c.Clone.Config == nil {
c.Clone.Config = map[string]interface{}{}
c.Clone.Config["depth"] = 50
c.Clone.Config["recursive"] = true
}
}
// transformBuild is a transformer that removes the
// build configuration vargs. They should have
// already been transferred to the Setup step.
func transformBuild(c *common.Config) {
c.Build.Config = nil
c.Build.Entrypoint = []string{"/bin/bash", "-e"}
c.Build.Command = []string{"/drone/bin/build.sh"}
}
// transformImages is a transformer that ensures every
// step has an image and uses a fully-qualified
// image name.
func transformImages(c *common.Config) {
c.Setup.Image = imageName(c.Setup.Image)
c.Clone.Image = imageName(c.Clone.Image)
for name, step := range c.Publish {
step.Image = imageNameDefault(step.Image, name)
}
for name, step := range c.Deploy {
step.Image = imageNameDefault(step.Image, name)
}
for name, step := range c.Notify {
step.Image = imageNameDefault(step.Image, name)
}
}
// transformDockerPlugin is a transformer that ensures the
// official Docker plugin can runs in privileged mode. It
// will disable volumes and network mode for added protection.
func transformDockerPlugin(c *common.Config) {
for _, step := range c.Publish {
if step.Image == "plugins/drone-docker" {
step.Privileged = true
step.Volumes = nil
step.NetworkMode = ""
step.Entrypoint = []string{}
break
}
}
}
// rmPrivileged is a transformer that ensures every
// step is executed in non-privileged mode.
func rmPrivileged(c *common.Config) {
c.Setup.Privileged = false
c.Clone.Privileged = false
c.Build.Privileged = false
for _, step := range c.Publish {
if step.Image == "plugins/drone-docker" {
continue // the official docker plugin is the only exception here
}
step.Privileged = false
}
for _, step := range c.Deploy {
step.Privileged = false
}
for _, step := range c.Notify {
step.Privileged = false
}
for _, step := range c.Compose {
step.Privileged = false
}
}
// rmVolumes is a transformer that ensures every
// step is executed without volumes.
func rmVolumes(c *common.Config) {
c.Setup.Volumes = nil
c.Clone.Volumes = nil
c.Build.Volumes = nil
for _, step := range c.Publish {
step.Volumes = nil
}
for _, step := range c.Deploy {
step.Volumes = nil
}
for _, step := range c.Notify {
step.Volumes = nil
}
for _, step := range c.Compose {
step.Volumes = nil
}
}
// rmNetwork is a transformer that ensures every
// step is executed with default bridge networking.
func rmNetwork(c *common.Config) {
c.Setup.NetworkMode = ""
c.Clone.NetworkMode = ""
c.Build.NetworkMode = ""
for _, step := range c.Publish {
step.NetworkMode = ""
}
for _, step := range c.Deploy {
step.NetworkMode = ""
}
for _, step := range c.Notify {
step.NetworkMode = ""
}
for _, step := range c.Compose {
step.NetworkMode = ""
}
}
// imageName is a helper function that resolves the
// image name. When using official drone plugins it
// is possible to use an alias name. This converts to
// the fully qualified name.
func imageName(name string) string {
if strings.Contains(name, "/") {
return name
}
name = strings.Replace(name, "_", "-", -1)
name = "plugins/drone-" + name
return name
}
// imageNameDefault is a helper function that resolves
// the image name. If the image name is blank the
// default name is used instead.
func imageNameDefault(name, defaultName string) string {
if len(name) == 0 {
name = defaultName
}
return imageName(name)
}

146
parser/trans_test.go Normal file
View file

@ -0,0 +1,146 @@
package parser
import (
"testing"
"github.com/drone/drone/common"
"github.com/franela/goblin"
)
func Test_Transform(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Transform", func() {
g.It("Should transform setup step", func() {
c := &common.Config{}
c.Build = &common.Step{}
c.Build.Config = map[string]interface{}{}
transformSetup(c)
g.Assert(c.Setup != nil).IsTrue()
g.Assert(c.Setup.Image).Equal("plugins/drone-build")
g.Assert(c.Setup.Config).Equal(c.Build.Config)
})
g.It("Should transform clone step", func() {
c := &common.Config{}
transformClone(c)
g.Assert(c.Clone != nil).IsTrue()
g.Assert(c.Clone.Image).Equal("plugins/drone-git")
})
g.It("Should transform build", func() {
c := &common.Config{}
c.Build = &common.Step{}
c.Build.Config = map[string]interface{}{}
c.Build.Config["commands"] = []string{"echo hello"}
transformBuild(c)
g.Assert(len(c.Build.Config)).Equal(0)
g.Assert(c.Build.Entrypoint[0]).Equal("/bin/bash")
g.Assert(c.Build.Command[0]).Equal("/drone/bin/build.sh")
})
g.It("Should transform images", func() {
c := &common.Config{}
c.Setup = &common.Step{Image: "foo"}
c.Clone = &common.Step{Image: "foo/bar"}
c.Build = &common.Step{Image: "golang"}
c.Publish = map[string]*common.Step{"google_compute": &common.Step{}}
c.Deploy = map[string]*common.Step{"amazon": &common.Step{}}
c.Notify = map[string]*common.Step{"slack": &common.Step{}}
transformImages(c)
g.Assert(c.Setup.Image).Equal("plugins/drone-foo")
g.Assert(c.Clone.Image).Equal("foo/bar")
g.Assert(c.Build.Image).Equal("golang")
g.Assert(c.Publish["google_compute"].Image).Equal("plugins/drone-google-compute")
g.Assert(c.Deploy["amazon"].Image).Equal("plugins/drone-amazon")
g.Assert(c.Notify["slack"].Image).Equal("plugins/drone-slack")
})
g.It("Should transform docker plugin", func() {
c := &common.Config{}
c.Publish = map[string]*common.Step{}
c.Publish["docker"] = &common.Step{Image: "plugins/drone-docker"}
transformDockerPlugin(c)
g.Assert(c.Publish["docker"].Privileged).Equal(true)
})
g.It("Should remove privileged flag", func() {
c := &common.Config{}
c.Setup = &common.Step{Privileged: true}
c.Clone = &common.Step{Privileged: true}
c.Build = &common.Step{Privileged: true}
c.Compose = map[string]*common.Step{"postgres": &common.Step{Privileged: true}}
c.Publish = map[string]*common.Step{"google": &common.Step{Privileged: true}}
c.Deploy = map[string]*common.Step{"amazon": &common.Step{Privileged: true}}
c.Notify = map[string]*common.Step{"slack": &common.Step{Privileged: true}}
rmPrivileged(c)
g.Assert(c.Setup.Privileged).Equal(false)
g.Assert(c.Clone.Privileged).Equal(false)
g.Assert(c.Build.Privileged).Equal(false)
g.Assert(c.Compose["postgres"].Privileged).Equal(false)
g.Assert(c.Publish["google"].Privileged).Equal(false)
g.Assert(c.Deploy["amazon"].Privileged).Equal(false)
g.Assert(c.Notify["slack"].Privileged).Equal(false)
})
g.It("Should not remove docker plugin privileged flag", func() {
c := &common.Config{}
c.Setup = &common.Step{}
c.Clone = &common.Step{}
c.Build = &common.Step{}
c.Publish = map[string]*common.Step{}
c.Publish["docker"] = &common.Step{Image: "plugins/drone-docker"}
transformDockerPlugin(c)
g.Assert(c.Publish["docker"].Privileged).Equal(true)
})
g.It("Should remove volumes", func() {
c := &common.Config{}
c.Setup = &common.Step{Volumes: []string{"/:/tmp"}}
c.Clone = &common.Step{Volumes: []string{"/:/tmp"}}
c.Build = &common.Step{Volumes: []string{"/:/tmp"}}
c.Compose = map[string]*common.Step{"postgres": &common.Step{Volumes: []string{"/:/tmp"}}}
c.Publish = map[string]*common.Step{"google": &common.Step{Volumes: []string{"/:/tmp"}}}
c.Deploy = map[string]*common.Step{"amazon": &common.Step{Volumes: []string{"/:/tmp"}}}
c.Notify = map[string]*common.Step{"slack": &common.Step{Volumes: []string{"/:/tmp"}}}
rmVolumes(c)
g.Assert(len(c.Setup.Volumes)).Equal(0)
g.Assert(len(c.Clone.Volumes)).Equal(0)
g.Assert(len(c.Build.Volumes)).Equal(0)
g.Assert(len(c.Compose["postgres"].Volumes)).Equal(0)
g.Assert(len(c.Publish["google"].Volumes)).Equal(0)
g.Assert(len(c.Deploy["amazon"].Volumes)).Equal(0)
g.Assert(len(c.Notify["slack"].Volumes)).Equal(0)
})
g.It("Should remove network", func() {
c := &common.Config{}
c.Setup = &common.Step{NetworkMode: "host"}
c.Clone = &common.Step{NetworkMode: "host"}
c.Build = &common.Step{NetworkMode: "host"}
c.Compose = map[string]*common.Step{"postgres": &common.Step{NetworkMode: "host"}}
c.Publish = map[string]*common.Step{"google": &common.Step{NetworkMode: "host"}}
c.Deploy = map[string]*common.Step{"amazon": &common.Step{NetworkMode: "host"}}
c.Notify = map[string]*common.Step{"slack": &common.Step{NetworkMode: "host"}}
rmNetwork(c)
g.Assert(c.Setup.NetworkMode).Equal("")
g.Assert(c.Clone.NetworkMode).Equal("")
g.Assert(c.Build.NetworkMode).Equal("")
g.Assert(c.Compose["postgres"].NetworkMode).Equal("")
g.Assert(c.Publish["google"].NetworkMode).Equal("")
g.Assert(c.Deploy["amazon"].NetworkMode).Equal("")
g.Assert(c.Notify["slack"].NetworkMode).Equal("")
})
g.It("Should return full qualified image name", func() {
g.Assert(imageName("microsoft/azure")).Equal("microsoft/azure")
g.Assert(imageName("azure")).Equal("plugins/drone-azure")
g.Assert(imageName("azure_storage")).Equal("plugins/drone-azure-storage")
})
})
}