1
0
mirror of https://github.com/drone/drone-cli.git synced 2024-11-23 09:21:56 +01:00

new builder package

This commit is contained in:
Brad Rydzewski 2015-03-09 20:15:52 -07:00
parent 4e57f9106c
commit e0b10ec19e
16 changed files with 574 additions and 466 deletions

@ -0,0 +1,124 @@
package ambassador
import (
"errors"
"io"
"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 {
name string
client dockerclient.Client
}
// CreateContainer creates a container.
func (c *Ambassador) CreateContainer(config *dockerclient.ContainerConfig, name string) (string, error) {
return c.client.CreateContainer(config, name)
}
// InspectContainer returns container details.
func (c *Ambassador) InspectContainer(id string) (*dockerclient.ContainerInfo, error) {
return c.client.InspectContainer(id)
}
// ContainerLogs returns an io.ReadCloser for reading the
// container logs.
func (c *Ambassador) ContainerLogs(id string, options *dockerclient.LogOptions) (io.ReadCloser, error) {
return c.client.ContainerLogs(id, options)
}
// 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, config *dockerclient.HostConfig) error {
config.VolumesFrom = append(config.VolumesFrom, "container:"+c.name)
if len(config.NetworkMode) == 0 {
config.NetworkMode = "container:" + c.name
}
return c.client.StartContainer(id, config)
}
// StopContainer stops a container.
func (c *Ambassador) StopContainer(id string, timeout int) error {
return c.client.StopContainer(id, timeout)
}
// PullImage pulls an image.
func (c *Ambassador) PullImage(name string, auth *dockerclient.AuthConfig) error {
return c.client.PullImage(name, auth)
}
// RemoveContainer removes a container.
func (c *Ambassador) RemoveContainer(id string, force, volumes bool) error {
return c.client.RemoveContainer(id, force, volumes)
}
// KillContainer kills a running container.
func (c *Ambassador) KillContainer(id, signal string) error {
return c.client.KillContainer(id, signal)
}
//
// methods below are not implemented
//
// Info returns a no-op error
func (c *Ambassador) Info() (*dockerclient.Info, error) {
return nil, errNop
}
// ListContainers returns a no-op error
func (c *Ambassador) ListContainers(all bool, size bool, filters string) ([]dockerclient.Container, error) {
return nil, errNop
}
// RestartContainer returns a no-op error
func (c *Ambassador) RestartContainer(id string, timeout int) error {
return errNop
}
// StartMonitorEvents returns a no-op error
func (c *Ambassador) StartMonitorEvents(cb dockerclient.Callback, ec chan error, args ...interface{}) {
}
// StopAllMonitorEvents returns a no-op error
func (c *Ambassador) StopAllMonitorEvents() {
}
// Version returns a no-op error
func (c *Ambassador) Version() (*dockerclient.Version, error) {
return nil, errNop
}
// ListImages returns a no-op error
func (c *Ambassador) ListImages() ([]*dockerclient.Image, error) {
return nil, errNop
}
// RemoveImage returns a no-op error
func (c *Ambassador) RemoveImage(name string) error {
return errNop
}
// PauseContainer returns a no-op error
func (c *Ambassador) PauseContainer(name string) error {
return errNop
}
// UnpauseContainer returns a no-op error
func (c *Ambassador) UnpauseContainer(name string) error {
return errNop
}
// Exec returns a no-op error
func (c *Ambassador) Exec(config *dockerclient.ExecConfig) (string, error) {
var empty string
return empty, errNop
}

35
builder/build.go Normal file

@ -0,0 +1,35 @@
package builder
import (
"encoding/json"
"github.com/drone/drone-cli/common"
"github.com/samalba/dockerclient"
)
// Build represents a build request.
type Build struct {
Repo *common.Repo
Commit *common.Repo
Config *common.Config
Clone *common.Clone
Client dockerclient.Client
}
// BuildPayload represents the payload of a plugin
// that is serialized and sent to the plugin in JSON
// format via stdin or arg[1].
type BuildPayload struct {
Repo *common.Repo `json:"repo"`
Commit *common.Repo `json:"commit"`
Clone *common.Clone `json:"clone"`
Config map[string]interface{} `json:"vargs"`
}
// Encode encodes the payload in JSON format.
func (b *BuildPayload) Encode() string {
out, _ := json.Marshal(b)
return string(out)
}

@ -1,120 +1,118 @@
package builder
import (
"fmt"
"io"
type Builder struct {
handlers []Handler
}
"github.com/drone/drone-cli/common/uuid"
)
// Handle adds a build step handler to be processed
// when running the build.
func (b *Builder) Handle(h Handler) {
b.handlers = append(b.handlers, h)
}
const (
ImageInit = "drone/drone-init"
ImageClone = "drone/drone-clone-git"
)
func Run(req *Request, resp ResponseWriter) error {
var containers []*Container
defer func() {
for i := len(containers) - 1; i >= 0; i-- {
container := containers[i]
container.Stop()
container.Kill()
container.Remove()
}
}()
// temporary name for the build container
//name := fmt.Sprintf("build-init-%s", createUID())
net := req.Config.Docker.Net
uid := uuid.CreateUUID()
cmd := []string{req.Encode()}
// init container
containers = append(containers, &Container{
Name: fmt.Sprintf("drone-%s-init", uid),
Image: ImageInit,
Volumes: []string{"/drone"},
Cmd: cmd,
})
// clone container
containers = append(containers, &Container{
Name: fmt.Sprintf("drone-%s-clone", uid),
Image: ImageClone,
VolumesFrom: []string{containers[0].Name},
Cmd: cmd,
})
// attached service containers
for i, service := range req.Config.Services {
containers = append(containers, &Container{
Name: fmt.Sprintf("drone-%s-service-%v", uid, i),
Image: service,
Env: req.Config.Env,
NetworkMode: net,
Detached: true,
})
if i == 0 && len(net) == 0 {
net = fmt.Sprintf("container:drone-%s-service-%v", uid, i)
}
}
// build container
containers = append(containers, &Container{
Name: fmt.Sprintf("drone-%s-build", uid),
Image: req.Config.Image,
Env: req.Config.Env,
Cmd: []string{"/drone/bin/build.sh"},
Entrypoint: []string{"/bin/bash"},
WorkingDir: req.Clone.Dir,
NetworkMode: net,
Privileged: req.Config.Docker.Privileged,
VolumesFrom: []string{containers[0].Name},
})
//
// create the notify, publish, deploy containers
//
// loop through and create containers
for _, container := range containers {
container.SetClient(req.Client)
if err := container.Create(); err != nil {
return err
}
}
// loop through and start containers
for _, container := range containers {
if err := container.Start(); err != nil {
return err
}
if container.Detached { // if a detached (daemon) just continue
continue
}
r, err := container.Logs()
if err != nil {
return err
}
io.Copy(resp, r)
r.Close()
info, err := container.Inspect()
if err != nil {
return err
}
if info.State.Running != false {
fmt.Println("ERROR: container still running")
}
resp.WriteExitCode(info.State.ExitCode)
if info.State.ExitCode != 0 {
// Build runs all build step handlers.
func (b *Builder) Build(r *Result) (err error) {
for _, h := range b.handlers {
err = h.Build(r)
if err != nil || r.exitCode != 0 {
break
}
}
return nil
}
// Cancel cancels any running build processes and
// removes and build containers.
func (b *Builder) Cancel() {
for _, h := range b.handlers {
h.Cancel() // TODO use channel to signal cancel
}
}
// type Builder struct {
// containers []*Container
// client dockerclient.Client
// }
//
// // New creates a new builder
// func New(client dockerclient.Client) *Builder {
// return &Builder{client: client}
// }
//
// func (r *Builder) Add(c *Container) {
// r.containers = append(r.containers, c)
// }
//
// func (r *Builder) Cancel() {
// for _, c := range r.containers {
// // TODO cancel using environment
// if c != nil {
// // TODO remove
// }
// }
// }
//
// func (b *Builder) Build(res *Result) error {
// for _, c := range b.containers {
// err := b.run(c, res)
// if err != nil {
// return err
// }
// if res.ExitCode() != 0 {
// return nil
// }
// }
// return nil
// }
//
// // helper function to run a single build container.
// func (b *Builder) run(c *Container, res *Result) error {
// // create container
// // err: failed creating
// // start continer
// // err: failed starting container
// if c.Detached == false {
// return nil
// }
// // get log reader
// // err: failed getting logs
//
// // copy logs to writer
// // err: failed copying logs
//
// // write response
// return nil
// }
/*
func NewBuilder(build *Build) *Builder {
b := New(build.Client)
for _, step := range build.Config.Compose {
b.Add(fromCompose(build, &step))
}
b.Add(fromSetup(build, &build.Config.Build))
b.Add(fromPlugin(build, &build.Config.Clone))
b.Add(fromBuild(build, &build.Config.Build))
return b
}
func NewDeployer(build *Build) *Builder {
b := New(build.Client)
for _, step := range build.Config.Publish {
b.Add(fromPlugin(build, &step))
}
for _, step := range build.Config.Deploy {
b.Add(fromPlugin(build, &step))
}
return b
}
func NewNotifier(build *Build) *Builder {
b := New(build.Client)
for _, step := range build.Config.Notify {
b.Add(fromPlugin(build, &step))
}
return b
}
*/

@ -1,33 +0,0 @@
package config
import (
"sort"
"strings"
"gopkg.in/yaml.v1"
)
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
}
func InjectSafe(raw string, params map[string]string) string {
before, _ := Parse(raw)
after, _ := Parse(Inject(raw, params))
after.Script = before.Script
scrubbed, _ := yaml.Marshal(after)
return string(scrubbed)
}

@ -1,65 +0,0 @@
package config
import (
"github.com/franela/goblin"
"testing"
)
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["my_service"].(map[interface{}]interface{})["token"]).Equal("FOO")
g.Assert(c.Deploy["my_service"].(map[interface{}]interface{})["secret"]).Equal("BAR")
})
g.It("Should not replace vars in script section", func() {
g.Assert(c.Script[0]).Equal("echo $$TOKEN")
g.Assert(c.Script[1]).Equal("echo $$SECRET")
})
})
}
var yml = `
image: foo
script:
- echo $$TOKEN
- echo $$SECRET
deploy:
my_service:
token: $$TOKEN
secret: $$SECRET
`

@ -1,87 +0,0 @@
package config
import (
"fmt"
"github.com/drone/drone-cli/common"
"gopkg.in/yaml.v1"
)
const (
LimitAxis = 10
LimitPerms = 25
)
func Parse(raw string) (*common.Config, error) {
config := common.Config{}
err := yaml.Unmarshal([]byte(raw), &config)
return &config, err
}
func ParseMatrix(raw string) ([]*common.Config, error) {
var matrix []*common.Config
config, err := Parse(raw)
if err != nil {
return matrix, err
}
// if not a matrix build return an array
// with just the single axis.
if len(config.Matrix) == 0 {
matrix = append(matrix, config)
return matrix, nil
}
// calculate number of permutations and
// extract the list of keys.
var perm int
var keys []string
for k, v := range config.Matrix {
perm *= len(v)
if perm == 0 {
perm = len(v)
}
keys = append(keys, k)
}
// for each axis calculate the values the uniqe
// set of values that should be used.
for p := 0; p < perm; p++ {
axis := map[string]string{}
decr := perm
for i, key := range keys {
vals := config.Matrix[key]
decr = decr / len(vals)
item := p / decr % len(vals)
axis[key] = vals[item]
// enforce a maximum number of axis
// in the build matrix.
if i > LimitAxis {
break
}
}
config, err = Parse(Inject(raw, axis))
if err != nil {
return nil, err
}
matrix = append(matrix, config)
// each axis value should also be added
// as an environment variable
for key, val := range axis {
env := fmt.Sprintf("%s=%s", key, val)
config.Env = append(config.Env, env)
}
// enforce a maximum number of permutations
// in the build matrix.
if p > LimitPerms {
break
}
}
return matrix, nil
}

@ -1,44 +0,0 @@
package config
import (
"testing"
)
func Test_Parse(t *testing.T) {
confs, err := ParseMatrix(matrix)
if err != nil {
t.Error(err)
return
}
if len(confs) != 24 {
t.Errorf("Expected 24 permutations in matrix, got %d", len(confs))
}
unique := map[string]bool{}
for _, config := range confs {
unique[config.Image] = true
}
if len(unique) != 24 {
t.Errorf("Expected 24 unique permutations in matrix, got %d", len(unique))
}
}
var matrix = `
image: $$python_version $$redis_version $$django_version $$go_version
matrix:
python_version:
- 3.2
- 3.3
redis_version:
- 2.6
- 2.8
django_version:
- 1.7
- 1.7.1
- 1.7.2
go_version:
- go1
- go1.2
`

@ -1 +0,0 @@
package config

@ -1,39 +1,117 @@
package builder
import (
"io"
"io/ioutil"
"github.com/drone/drone-cli/common"
"github.com/samalba/dockerclient"
)
// Container represents a Docker Container used
// to execute a build step.
type Container struct {
Name string
ID string
Image string
Pull bool
Detached bool
Privileged bool
Env []string
Cmd []string
Environment []string
Entrypoint []string
WorkingDir string
NetworkMode string
Command []string
Volumes []string
VolumesFrom []string
Links []string
client dockerclient.Client
info *dockerclient.ContainerInfo
WorkingDir string
NetworkMode string
}
func (c *Container) SetClient(client dockerclient.Client) {
c.client = client
// helper function to create a container from a step.
func fromStep(step *common.Step) *Container {
return &Container{
Image: step.Name,
Pull: step.Pull,
Privileged: step.Privileged,
Volumes: step.Volumes,
WorkingDir: step.WorkingDir,
NetworkMode: step.NetworkMode,
Entrypoint: step.Entrypoint,
Environment: step.Environment,
Command: step.Command,
}
}
func (c *Container) Create() error {
config := dockerclient.ContainerConfig{
// helper function to create a container from a build
// step. The build task will invoke a shell script
// at an expected path.
func fromBuild(build *Build, step *common.Step) *Container {
c := fromStep(step)
c.Entrypoint = []string{"/bin/bash"}
c.Command = []string{"/drone/bin/build.sh"}
return c
}
// helper function to create a container from a setup
// step. This is a special container. It is used to
// bootstrap the environment, create build directories,
// and generate the build script.
//
// see https://github.com/drone-plugins/drone-build
func fromSetup(build *Build, step *common.Step) *Container {
c := fromStep(step)
c.Image = "plugins/drone-build"
c.Entrypoint = []string{"/go/bin/drone-build"}
c.Command = toCommand(build, step)
return c
}
// helper function to create a container from any plugin
// step, including notification, deployment and publish steps.
// It is used to create the plugin payload (JSON) and pass
// to the container as arg[1]
func fromPlugin(build *Build, step *common.Step) *Container {
c := fromStep(step)
c.Entrypoint = []string{}
c.Command = toCommand(build, step)
return c
}
// helper function to create a container from a compose
// step. This creates the container almost verbatim. It only
// adds a --detached flag to the container. This instructure
// the build not to block and wait for this container to
// finish execution.
func fromCompose(build *Build, step *common.Step) *Container {
c := fromStep(step)
c.Detached = true
return c
}
// helper function to encode the container arguments
// in a json string. Primarily used for plugins, which
// expect a json encoded string in stdin or arg[1].
func toCommand(build *Build, step *common.Step) []string {
payload := BuildPayload{
build.Repo,
build.Commit,
build.Clone,
step.Config,
}
return []string{payload.Encode()}
}
// helper function that converts the container to
// a hostConfig for use with the dockerclient
func (c *Container) toHostConfig() *dockerclient.HostConfig {
return &dockerclient.HostConfig{
Privileged: c.Privileged,
NetworkMode: c.NetworkMode,
}
}
// helper function that converts the container to
// a containerConfig for use with the dockerclient
func (c *Container) toContainerConfig() *dockerclient.ContainerConfig {
config := &dockerclient.ContainerConfig{
Image: c.Image,
Env: c.Env,
Cmd: c.Cmd,
Env: c.Environment,
Cmd: c.Command,
Entrypoint: c.Entrypoint,
WorkingDir: c.WorkingDir,
}
@ -45,56 +123,5 @@ func (c *Container) Create() error {
}
}
id, err := c.client.CreateContainer(&config, c.Name)
if err != nil {
return err
}
c.info, err = c.client.InspectContainer(id)
return err
}
func (c *Container) Start() error {
config := dockerclient.HostConfig{
Privileged: c.Privileged,
NetworkMode: c.NetworkMode,
VolumesFrom: c.VolumesFrom,
}
return c.client.StartContainer(c.info.Id, &config)
}
func (c *Container) Stop() error {
return c.client.StopContainer(c.info.Id, 10)
}
func (c *Container) Kill() error {
return c.client.KillContainer(c.info.Id, "SIGKILL")
}
func (c *Container) Remove() error {
return c.client.RemoveContainer(c.info.Id, true, true)
}
func (c *Container) Wait() error {
src, err := c.Logs()
if err != nil {
return err
}
defer src.Close()
_, err = io.Copy(ioutil.Discard, src)
return nil
}
func (c *Container) Logs() (io.ReadCloser, error) {
opts := dockerclient.LogOptions{
Follow: true,
Stderr: true,
Stdout: true,
Tail: 10000,
Timestamps: true,
}
return c.client.ContainerLogs(c.info.Id, &opts)
}
func (c *Container) Inspect() (*dockerclient.ContainerInfo, error) {
return c.client.InspectContainer(c.info.Id)
return config
}

88
builder/handler.go Normal file

@ -0,0 +1,88 @@
package builder
import (
"io"
"github.com/samalba/dockerclient"
)
// Handler defines an interface that can be implemented by
// objects that should be run during the build process.
// to run as part of a build.
type Handler interface {
Build(*Result) error
Cancel()
}
type handler struct {
id string
name string
detach bool
client dockerclient.Client
host *dockerclient.HostConfig
config *dockerclient.ContainerConfig
}
func (h *handler) Build(res *Result) error {
id, err := h.client.CreateContainer(h.config, h.name)
if err != nil {
return err
}
h.id = id
err = h.client.StartContainer(h.id, h.host)
if err != nil {
return err
}
if h.detach {
return nil
}
logs := &dockerclient.LogOptions{
Follow: true,
Stderr: true,
Stdout: true,
Timestamps: true,
}
rc, err := h.client.ContainerLogs(h.id, logs)
if err != nil {
return err
}
io.Copy(res, rc)
info, err := h.client.InspectContainer(h.id)
if err != nil {
return err
}
res.WriteExitCode(info.State.ExitCode)
return nil
}
func (h *handler) Cancel() {
h.client.StopContainer(h.id, 5)
h.client.KillContainer(h.id, "SIGKILL")
h.client.RemoveContainer(h.id, true, false)
}
// BatchHandler returns a handler that runs a build
// task in batch mode. It will block until the task
// comples, writing stdout to the result.
//
// If the task fails the exit status code is written
// to the result as well.
func BatchHandler(client dockerclient.Client, c *Container) Handler {
return &handler{
client: client,
host: c.toHostConfig(),
config: c.toContainerConfig(),
}
}
// DetachedHandler returns a handler that runs a build
// task in detached mode. It will start the container async,
// and immediately exit.
func DetachedHandler(client dockerclient.Client, c *Container) Handler {
return &handler{
detach: true,
client: client,
host: c.toHostConfig(),
config: c.toContainerConfig(),
}
}

@ -1,23 +0,0 @@
package builder
import (
"encoding/json"
"github.com/drone/drone-cli/common"
"github.com/samalba/dockerclient"
)
// A Request represents a build request.
type Request struct {
Repo *common.Repo `json:"repo"`
Commit *common.Repo `json:"commit"`
Config *common.Config `json:"config"`
Clone *common.Clone `json:"clone"`
Client dockerclient.Client `json:"-"`
}
func (r *Request) Encode() string {
out, _ := json.Marshal(r)
return string(out)
}

@ -1,26 +0,0 @@
package builder
import (
"io"
)
type ResponseWriter interface {
// Write writes the build stdout and stderr to the response.
Write([]byte) (int, error)
// WriteExitCode writes the build exit status to the response.
WriteExitCode(int)
}
type Response struct {
Writer io.Writer
ExitCode int
}
func (r *Response) Write(p []byte) (n int, err error) {
return r.Writer.Write(p)
}
func (r *Response) WriteExitCode(code int) {
r.ExitCode = code
}

24
builder/result.go Normal file

@ -0,0 +1,24 @@
package builder
import "io"
// Result represents the result from a build request.
type Result struct {
writer io.Writer
exitCode int
}
// Write writes the build stdout and stderr to the result.
func (r *Result) Write(p []byte) (n int, err error) {
return r.writer.Write(p)
}
// WriteExitCode writes the build exit status to the result.
func (r *Result) WriteExitCode(code int) {
r.exitCode = code
}
// ExitCode returns the build exit status.
func (r *Result) ExitCode() int {
return r.exitCode
}

@ -1,15 +1,17 @@
package common
// A step represents a step in the build process, including
// Step represents a step in the build process, including
// the execution environment and parameters.
type Step struct {
Name string
Image string
Environment []string
Volumes []string
Hostname string
Pull bool
Privileged bool
Net string
Environment []string
Entrypoint []string
Command []string
Volumes []string
WorkingDir string
NetworkMode string
// Config represents the unique configuration details
// for each plugin.

49
runner/builder.go Normal file

@ -0,0 +1,49 @@
package runner
/*
import (
"io"
"github.com/samalba/dockerclient"
)
type Runner struct {
client dockerclient.Client
containers []*Container
}
func (r *Runner) Run(req *Request, resp ResponseWriter) error {
return nil
}
Setup()
Clone()
Build()
Deploy()
Notify()
Teardown()
func (r *Runner) Logs(w io.Writer) {
for _, c := range r.containers {
if c.Detached {
continue
}
r, err := c.Logs()
if err != nil {
continue
}
io.Copy(w, r)
}
}
func (r *Runner) Kill() {
for _, c := range r.containers {
c.Stop()
c.Kill()
c.Remove()
}
}
*/

40
runner/notifier.go Normal file

@ -0,0 +1,40 @@
package runner
/*
func Notify(req *Request, resp ResponseWriter) error {
var containers []*Container
defer func() {
for i := len(containers) - 1; i >= 0; i-- {
container := containers[i]
container.Stop()
container.Kill()
container.Remove()
}
}()
// attached service containers
for _, notification := range req.Config.Notify {
containers = append(containers, &Container{
Image: notification.Image,
Env: notification.Environment,
Cmd: EncodeParams(req, notification.Config),
})
}
// loop through and run containers
for _, container := range containers {
container.SetClient(req.Client)
if err := container.Create(); err != nil {
return err
}
if err := container.Start(); err != nil {
return err
}
container.Wait()
}
return nil
}
*/