add yaml parser, build execution code. not yet hooked up
This commit is contained in:
parent
cd18645784
commit
5816f1156e
18 changed files with 1767 additions and 0 deletions
153
builder/build.go
Normal file
153
builder/build.go
Normal 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
81
builder/builder.go
Normal 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
124
builder/copy.go
Normal 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
|
||||
}
|
||||
}
|
110
builder/docker/ambassador.go
Normal file
110
builder/docker/ambassador.go
Normal 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
103
builder/node.go
Normal 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
89
builder/pool/pool.go
Normal 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
98
builder/util.go
Normal 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)
|
||||
}
|
|
@ -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
104
common/config.go
Normal 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
54
parser/inject/inject.go
Normal 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
|
||||
}
|
67
parser/inject/inject_test.go
Normal file
67
parser/inject/inject_test.go
Normal 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
105
parser/lint.go
Normal 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
92
parser/lint_test.go
Normal 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
109
parser/matrix/matrix.go
Normal 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
|
||||
}
|
33
parser/matrix/matrix_test.go
Normal file
33
parser/matrix/matrix_test.go
Normal 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
99
parser/parse.go
Normal 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
183
parser/trans.go
Normal 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
146
parser/trans_test.go
Normal 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")
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue