initial public commit

This commit is contained in:
Brad Rydzewski
2014-02-07 03:10:01 -07:00
commit d5e5797934
183 changed files with 15701 additions and 0 deletions

471
pkg/build/build.go Normal file
View File

@@ -0,0 +1,471 @@
package build
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/drone/drone/pkg/build/buildfile"
"github.com/drone/drone/pkg/build/docker"
"github.com/drone/drone/pkg/build/dockerfile"
"github.com/drone/drone/pkg/build/log"
"github.com/drone/drone/pkg/build/proxy"
"github.com/drone/drone/pkg/build/repo"
"github.com/drone/drone/pkg/build/script"
)
// instance of the Docker client
var client = docker.New()
// BuildState stores information about a build
// process including the Exit status and various
// Runtime statistics (coming soon).
type BuildState struct {
Started int64
Finished int64
ExitCode int
// we may eventually include detailed resource
// usage statistics, including including CPU time,
// Max RAM, Max Swap, Disk space, and more.
}
// Builder represents a build process being prepared
// to run.
type Builder struct {
// Image specifies the Docker Image that will be
// used to virtualize the Build process.
Build *script.Build
// Source specifies the Repository path of the code
// that we are testing.
//
// The source repository may be a local repository
// on the current filesystem, or a remote repository
// on GitHub, Bitbucket, etc.
Repo *repo.Repo
// Key is an identify file, such as an RSA private key, that
// will be copied into the environments ~/.ssh/id_rsa file.
Key []byte
// Timeout is the maximum amount of to will wait for a process
// to exit.
//
// The default is no timeout.
Timeout time.Duration
// Stdout specifies the builds's standard output.
//
// If stdout is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
Stdout io.Writer
// BuildState contains information about an exited build,
// available after a call to Run.
BuildState *BuildState
// Docker image that was created for
// this build.
image *docker.Image
// Docker container was that created
// for this build.
container *docker.Run
// Docker containers created for the
// specified services and linked to
// this build.
services []*docker.Container
}
func (b *Builder) Run() error {
// teardown will remove the Image and stop and
// remove the service containers after the
// build is done running.
defer b.teardown()
// setup will create the Image and supporting
// service containers.
if err := b.setup(); err != nil {
return err
}
// make sure build state is not nil
b.BuildState = &BuildState{}
b.BuildState.ExitCode = 0
b.BuildState.Started = time.Now().UTC().Unix()
c := make(chan error, 1)
go func() {
c <- b.run()
}()
// wait for either a) the job to complete or b) the job to timeout
select {
case err := <-c:
return err
case <-time.After(b.Timeout):
log.Errf("time limit exceeded for build %s", b.Build.Name)
b.BuildState.ExitCode = 124
b.BuildState.Finished = time.Now().UTC().Unix()
return nil
}
return nil
}
func (b *Builder) setup() error {
// temp directory to store all files required
// to generate the Docker image.
dir, err := ioutil.TempDir("", "drone-")
if err != nil {
return err
}
// clean up after our mess.
defer os.RemoveAll(dir)
// make sure the image isn't empty. this would be bad
if len(b.Build.Image) == 0 {
log.Err("Fatal Error, No Docker Image specified")
return fmt.Errorf("Error: missing Docker image")
}
// if we're using an alias for the build name we
// should substitute it now
if alias, ok := builders[b.Build.Image]; ok {
b.Build.Image = alias.Tag
}
// if this is a local repository we should symlink
// to the source code in our temp directory
if b.Repo.IsLocal() {
// this is where we used to use symlinks. We should
// talk to the docker team about this, since copying
// the entire repository is slow :(
//
// see https://github.com/dotcloud/docker/pull/3567
//src := filepath.Join(dir, "src")
//err = os.Symlink(b.Repo.Path, src)
//if err != nil {
// return err
//}
src := filepath.Join(dir, "src")
cmd := exec.Command("cp", "-a", b.Repo.Path, src)
if err := cmd.Run(); err != nil {
return err
}
}
// start all services required for the build
// that will get linked to the container.
for _, service := range b.Build.Services {
image, ok := services[service]
if !ok {
return fmt.Errorf("Error: Invalid or unknown service %s", service)
}
// debugging
log.Infof("starting service container %s", image.Tag)
// Run the contianer
run, err := client.Containers.RunDaemonPorts(image.Tag, image.Ports...)
if err != nil {
return err
}
// Get the container info
info, err := client.Containers.Inspect(run.ID)
if err != nil {
// on error kill the container since it hasn't yet been
// added to the array and would therefore not get
// removed in the defer statement.
client.Containers.Stop(run.ID, 10)
client.Containers.Remove(run.ID)
return err
}
// Add the running service to the list
b.services = append(b.services, info)
}
if err := b.writeIdentifyFile(dir); err != nil {
return err
}
if err := b.writeBuildScript(dir); err != nil {
return err
}
if err := b.writeProxyScript(dir); err != nil {
return err
}
if err := b.writeDockerfile(dir); err != nil {
return err
}
// debugging
log.Info("creating build image")
// check for build container (ie bradrydzewski/go:1.2)
// and download if it doesn't already exist
if _, err := client.Images.Inspect(b.Build.Image); err == docker.ErrNotFound {
// download the image if it doesn't exist
if err := client.Images.Pull(b.Build.Image); err != nil {
return err
}
}
// create the Docker image
id := createUID()
if err := client.Images.Build(id, dir); err != nil {
return err
}
// debugging
log.Infof("copying repository to %s", b.Repo.Dir)
// get the image details
b.image, err = client.Images.Inspect(id)
if err != nil {
// if we have problems with the image make sure
// we remove it before we exit
client.Images.Remove(id)
return err
}
return nil
}
// teardown is a helper function that we can use to
// stop and remove the build container, its supporting image,
// and the supporting service containers.
func (b *Builder) teardown() error {
// stop and destroy the container
if b.container != nil {
// debugging
log.Info("removing build container")
// stop the container, ignore error message
client.Containers.Stop(b.container.ID, 15)
// remove the container, ignore error message
if err := client.Containers.Remove(b.container.ID); err != nil {
log.Errf("failed to delete build container %s", b.container.ID)
}
}
// stop and destroy the container services
for i, container := range b.services {
// debugging
log.Infof("removing service container %s", b.Build.Services[i])
// stop the service container, ignore the error
client.Containers.Stop(container.ID, 15)
// remove the service container, ignore the error
if err := client.Containers.Remove(container.ID); err != nil {
log.Errf("failed to delete service container %s", container.ID)
}
}
// destroy the underlying image
if b.image != nil {
// debugging
log.Info("removing build image")
if _, err := client.Images.Remove(b.image.ID); err != nil {
log.Errf("failed to completely delete build image %s. %s", b.image.ID, err.Error())
}
}
return nil
}
func (b *Builder) run() error {
// create and run the container
conf := docker.Config{
Image: b.image.ID,
AttachStdin: false,
AttachStdout: true,
AttachStderr: true,
}
host := docker.HostConfig{
Privileged: false,
}
// debugging
log.Noticef("starting build %s", b.Build.Name)
// link service containers
for i, service := range b.services {
image, ok := services[b.Build.Services[i]]
if !ok {
continue // THIS SHOULD NEVER HAPPEN
}
// link the service container to our
// build container.
host.Links = append(host.Links, service.Name[1:]+":"+image.Name)
}
// create the container from the image
run, err := client.Containers.Create(&conf)
if err != nil {
return err
}
// cache instance of docker.Run
b.container = run
// attach to the container
go func() {
client.Containers.Attach(run.ID, &writer{b.Stdout})
}()
// start the container
if err := client.Containers.Start(run.ID, &host); err != nil {
b.BuildState.ExitCode = 1
b.BuildState.Finished = time.Now().UTC().Unix()
return err
}
// wait for the container to stop
wait, err := client.Containers.Wait(run.ID)
if err != nil {
b.BuildState.ExitCode = 1
b.BuildState.Finished = time.Now().UTC().Unix()
return err
}
// set completion time
b.BuildState.Finished = time.Now().UTC().Unix()
// get the exit code if possible
b.BuildState.ExitCode = wait.StatusCode //b.container.State.ExitCode
return nil
}
// writeDockerfile is a helper function that generates a
// Dockerfile and writes to the builds temporary directory
// so that it can be used to create the Image.
func (b *Builder) writeDockerfile(dir string) error {
var dockerfile = dockerfile.New(b.Build.Image)
dockerfile.WriteWorkdir(b.Repo.Dir)
dockerfile.WriteAdd("drone", "/usr/local/bin/")
// upload source code if repository is stored
// on the host machine
if b.Repo.IsRemote() == false {
dockerfile.WriteAdd("src", filepath.Join(b.Repo.Dir))
}
switch {
case strings.HasPrefix(b.Build.Image, "bradrydzewski/"),
strings.HasPrefix(b.Build.Image, "drone/"):
// the default user for all official Drone imnage
// is the "ubuntu" user, since all build images
// inherit from the ubuntu cloud ISO
dockerfile.WriteUser("ubuntu")
dockerfile.WriteEnv("HOME", "/home/ubuntu")
dockerfile.WriteEnv("LANG", "en_US.UTF-8")
dockerfile.WriteEnv("LANGUAGE", "en_US:en")
dockerfile.WriteEnv("LOGNAME", "ubuntu")
dockerfile.WriteEnv("TERM", "xterm")
dockerfile.WriteEnv("SHELL", "/bin/bash")
dockerfile.WriteAdd("id_rsa", "/home/ubuntu/.ssh/id_rsa")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /home/ubuntu/.ssh")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /var/cache/drone")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /usr/local/bin/drone")
dockerfile.WriteRun("sudo chmod 600 /home/ubuntu/.ssh/id_rsa")
default:
// all other images are assumed to use
// the root user.
dockerfile.WriteUser("root")
dockerfile.WriteEnv("HOME", "/root")
dockerfile.WriteEnv("LANG", "en_US.UTF-8")
dockerfile.WriteEnv("LANGUAGE", "en_US:en")
dockerfile.WriteEnv("LOGNAME", "root")
dockerfile.WriteEnv("TERM", "xterm")
dockerfile.WriteEnv("SHELL", "/bin/bash")
dockerfile.WriteEnv("GOPATH", "/var/cache/drone")
dockerfile.WriteAdd("id_rsa", "/root/.ssh/id_rsa")
dockerfile.WriteRun("chmod 600 /root/.ssh/id_rsa")
}
dockerfile.WriteAdd("proxy.sh", "/etc/drone.d/")
dockerfile.WriteEntrypoint("/bin/bash -e /usr/local/bin/drone")
// write the Dockerfile to the temporary directory
return ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile.Bytes(), 0700)
}
// writeBuildScript is a helper function that
// will generate the build script file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeBuildScript(dir string) error {
f := buildfile.New()
// if the repository is remote then we should
// add the commands to the build script to
// clone the repository
if b.Repo.IsRemote() {
for _, cmd := range b.Repo.Commands() {
f.WriteCmd(cmd)
}
}
// if the commit is for merging a pull request
// we should only execute the build commands,
// and omit the deploy and publish commands.
if len(b.Repo.PR) == 0 {
b.Build.Write(f)
} else {
// only write the build commands
b.Build.WriteBuild(f)
}
scriptfilePath := filepath.Join(dir, "drone")
return ioutil.WriteFile(scriptfilePath, f.Bytes(), 0700)
}
// writeProxyScript is a helper function that
// will generate the proxy.sh file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeProxyScript(dir string) error {
var proxyfile = proxy.Proxy{}
// loop through services so that we can
// map ip address to localhost
for _, container := range b.services {
// create an entry for each port
for port, _ := range container.NetworkSettings.Ports {
proxyfile.Set(port.Port(), container.NetworkSettings.IPAddress)
}
}
// write the proxyfile to the temp directory
proxyfilePath := filepath.Join(dir, "proxy.sh")
return ioutil.WriteFile(proxyfilePath, proxyfile.Bytes(), 0755)
}
// writeIdentifyFile is a helper function that
// will generate the id_rsa file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeIdentifyFile(dir string) error {
keyfilePath := filepath.Join(dir, "id_rsa")
return ioutil.WriteFile(keyfilePath, b.Key, 0700)
}

View File

@@ -0,0 +1,72 @@
package buildfile
import (
"bytes"
"fmt"
)
type Buildfile struct {
bytes.Buffer
}
func New() *Buildfile {
b := Buildfile{}
b.WriteString(base)
return &b
}
// WriteCmd writes a command to the build file. The
// command will be echoed back as a base16 encoded
// command so that it can be parsed and appended to
// the build output
func (b *Buildfile) WriteCmd(command string) {
// echo the command as an encoded value
b.WriteString(fmt.Sprintf("echo '#DRONE:%x'\n", command))
// and then run the command
b.WriteString(fmt.Sprintf("%s\n", command))
}
// WriteCmdSilent writes a command to the build file
// but does not echo the command.
func (b *Buildfile) WriteCmdSilent(command string) {
b.WriteString(fmt.Sprintf("%s\n", command))
}
// WriteComment adds a comment to the build file. This
// is really only used internally for debugging purposes.
func (b *Buildfile) WriteComment(comment string) {
b.WriteString(fmt.Sprintf("#%s\n", comment))
}
// WriteEnv exports the environment variable as
// part of the script. The environment variables
// are not echoed back to the console, and are
// kept private by default.
func (b *Buildfile) WriteEnv(key, value string) {
b.WriteString(fmt.Sprintf("export %s=%s\n", key, value))
}
// every build script starts with the following
// code at the start.
var base = `
#!/bin/bash
# drone configuration files are stored in /etc/drone.d
# execute these files prior to our build to set global
# environment variables and initialize programs (like rbenv)
if [ -d /etc/drone.d ]; then
for i in /etc/drone.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
# be sure to exit on error and print out
# our bash commands, so we can which commands
# are executing and troubleshoot failures.
set -e
# user-defined commands below ##############################
`

258
pkg/build/docker/client.go Normal file
View File

@@ -0,0 +1,258 @@
package docker
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/dotcloud/docker/pkg/term"
"github.com/dotcloud/docker/utils"
)
const (
APIVERSION = 1.9
DEFAULTHTTPPORT = 4243
DEFAULTUNIXSOCKET = "/var/run/docker.sock"
DEFAULTPROTOCOL = "unix"
DEFAULTTAG = "latest"
VERSION = "0.8.0"
)
// Enables verbose logging to the Terminal window
var Logging = true
// New creates an instance of the Docker Client
func New() *Client {
c := &Client{}
c.proto = DEFAULTPROTOCOL
c.addr = DEFAULTUNIXSOCKET
// if the default socket doesn't exist then
// we'll try to connect to the default tcp address
if _, err := os.Stat(DEFAULTUNIXSOCKET); err != nil {
c.proto = "tcp"
c.addr = "0.0.0.0:4243"
}
c.Images = &ImageService{c}
c.Containers = &ContainerService{c}
return c
}
type Client struct {
proto string
addr string
Images *ImageService
Containers *ContainerService
}
var (
// Returned if the specified resource does not exist.
ErrNotFound = errors.New("Not Found")
// Returned if the caller attempts to make a call or modify a resource
// for which the caller is not authorized.
//
// The request was a valid request, the caller's authentication credentials
// succeeded but those credentials do not grant the caller permission to
// access the resource.
ErrForbidden = errors.New("Forbidden")
// Returned if the call requires authentication and either the credentials
// provided failed or no credentials were provided.
ErrNotAuthorized = errors.New("Unauthorized")
// Returned if the caller submits a badly formed request. For example,
// the caller can receive this return if you forget a required parameter.
ErrBadRequest = errors.New("Bad Request")
)
// helper function used to make HTTP requests to the Docker daemon.
func (c *Client) do(method, path string, in, out interface{}) error {
// if data input is provided, serialize to JSON
var payload io.Reader
if in != nil {
buf, err := json.Marshal(in)
if err != nil {
return err
}
payload = bytes.NewBuffer(buf)
}
// create the request
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), payload)
if err != nil {
return err
}
// set the appropariate headers
req.Header = http.Header{}
req.Header.Set("User-Agent", "Docker-Client/"+VERSION)
req.Header.Set("Content-Type", "application/json")
// dial the host server
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
return err
}
// make the request
conn := httputil.NewClientConn(dial, nil)
resp, err := conn.Do(req)
defer conn.Close()
if err != nil {
return err
}
// Read the bytes from the body (make sure we defer close the body)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// Check for an http error status (ie not 200 StatusOK)
switch resp.StatusCode {
case 404:
return ErrNotFound
case 403:
return ErrForbidden
case 401:
return ErrNotAuthorized
case 400:
return ErrBadRequest
}
// Unmarshall the JSON response
if out != nil {
return json.Unmarshal(body, out)
}
return nil
}
func (c *Client) hijack(method, path string, setRawTerminal bool, out io.Writer) error {
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Docker-Client/"+VERSION)
req.Header.Set("Content-Type", "plain/text")
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?")
}
return err
}
clientconn := httputil.NewClientConn(dial, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
clientconn.Do(req)
// Hijack the connection to read / write
rwc, br := clientconn.Hijack()
defer rwc.Close()
// launch a goroutine to copy the stream
// of build output to the writer.
errStdout := make(chan error, 1)
go func() {
var err error
if setRawTerminal {
_, err = io.Copy(out, br)
} else {
_, err = utils.StdCopy(out, out, br)
}
errStdout <- err
}()
// wait for a response
if err := <-errStdout; err != nil {
return err
}
return nil
}
func (c *Client) stream(method, path string, in io.Reader, out io.Writer, headers http.Header) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader(nil)
}
// setup the request
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), in)
if err != nil {
return err
}
// set default headers
req.Header = headers
req.Header.Set("User-Agent", "Docker-Client/0.6.4")
req.Header.Set("Content-Type", "plain/text")
// dial the host server
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
return err
}
// make the request
conn := httputil.NewClientConn(dial, nil)
resp, err := conn.Do(req)
defer conn.Close()
if err != nil {
return err
}
// make sure we defer close the body
defer resp.Body.Close()
// Check for an http error status (ie not 200 StatusOK)
switch resp.StatusCode {
case 404:
return ErrNotFound
case 403:
return ErrForbidden
case 401:
return ErrNotAuthorized
case 400:
return ErrBadRequest
}
// If no output we exit now with no errors
if out == nil {
return nil
}
// copy the output stream to the writer
if resp.Header.Get("Content-Type") == "application/json" {
var terminalFd = os.Stdin.Fd()
var isTerminal = term.IsTerminal(terminalFd)
// it may not make sense to put this code here, but it works for
// us at the moment, and I don't feel like refactoring
return utils.DisplayJSONMessagesStream(resp.Body, out, terminalFd, isTerminal)
}
// otherwise plain text
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,147 @@
package docker
import (
"fmt"
"io"
)
type ContainerService struct {
*Client
}
// List only running containers.
func (c *ContainerService) List() ([]*Containers, error) {
containers := []*Containers{}
err := c.do("GET", "/containers/json?all=0", nil, &containers)
return containers, err
}
// List all containers
func (c *ContainerService) ListAll() ([]*Containers, error) {
containers := []*Containers{}
err := c.do("GET", "/containers/json?all=1", nil, &containers)
return containers, err
}
// Create a Container
func (c *ContainerService) Create(conf *Config) (*Run, error) {
run, err := c.create(conf)
switch {
// if no error, exit immediately
case err == nil:
return run, nil
// if error we exit, unless it is
// a NOT FOUND error, which means we just
// need to download the Image from the center
// image index
case err != nil && err != ErrNotFound:
return nil, err
}
// attempt to pull the image
if err := c.Images.Pull(conf.Image); err != nil {
return nil, err
}
// now that we have the image, re-try creation
return c.create(conf)
}
func (c *ContainerService) create(conf *Config) (*Run, error) {
run := Run{}
err := c.do("POST", "/containers/create", conf, &run)
return &run, err
}
// Start the container id
func (c *ContainerService) Start(id string, conf *HostConfig) error {
return c.do("POST", fmt.Sprintf("/containers/%s/start", id), &conf, nil)
}
// Stop the container id
func (c *ContainerService) Stop(id string, timeout int) error {
return c.do("POST", fmt.Sprintf("/containers/%s/stop?t=%v", id, timeout), nil, nil)
}
// Remove the container id from the filesystem.
func (c *ContainerService) Remove(id string) error {
return c.do("DELETE", fmt.Sprintf("/containers/%s", id), nil, nil)
}
// Block until container id stops, then returns the exit code
func (c *ContainerService) Wait(id string) (*Wait, error) {
wait := Wait{}
err := c.do("POST", fmt.Sprintf("/containers/%s/wait", id), nil, &wait)
return &wait, err
}
// Attach to the container to stream the stdout and stderr
func (c *ContainerService) Attach(id string, out io.Writer) error {
path := fmt.Sprintf("/containers/%s/attach?&stream=1&stdout=1&stderr=1", id)
return c.hijack("POST", path, false, out)
}
// Stop the container id
func (c *ContainerService) Inspect(id string) (*Container, error) {
container := Container{}
err := c.do("GET", fmt.Sprintf("/containers/%s/json", id), nil, &container)
return &container, err
}
// Run the container
func (c *ContainerService) Run(conf *Config, host *HostConfig, out io.Writer) (*Wait, error) {
// create the container from the image
run, err := c.Create(conf)
if err != nil {
return nil, err
}
// attach to the container
go func() {
c.Attach(run.ID, out)
}()
// start the container
if err := c.Start(run.ID, host); err != nil {
return nil, err
}
// wait for the container to stop
wait, err := c.Wait(run.ID)
if err != nil {
return nil, err
}
return wait, nil
}
// Run the container as a Daemon
func (c *ContainerService) RunDaemon(conf *Config, host *HostConfig) (*Run, error) {
run, err := c.Create(conf)
if err != nil {
return nil, err
}
// start the container
err = c.Start(run.ID, host)
return run, err
}
func (c *ContainerService) RunDaemonPorts(image string, ports ...string) (*Run, error) {
// setup configuration
config := Config{Image: image}
config.ExposedPorts = make(map[Port]struct{})
// host configuration
host := HostConfig{}
host.PortBindings = make(map[Port][]PortBinding)
// loop through and add ports
for _, port := range ports {
config.ExposedPorts[Port(port+"/tcp")] = struct{}{}
host.PortBindings[Port(port+"/tcp")] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}}
}
//127.0.0.1::%s
//map[3306/tcp:{}] map[3306/tcp:[{127.0.0.1 }]]
return c.RunDaemon(&config, &host)
}

124
pkg/build/docker/image.go Normal file
View File

@@ -0,0 +1,124 @@
package docker
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"time"
"github.com/dotcloud/docker/archive"
"github.com/dotcloud/docker/utils"
)
type Images struct {
ID string `json:"Id"`
RepoTags []string `json:",omitempty"`
Created int64
Size int64
VirtualSize int64
ParentId string `json:",omitempty"`
// DEPRECATED
Repository string `json:",omitempty"`
Tag string `json:",omitempty"`
}
type Image struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
Container string `json:"container,omitempty"`
ContainerConfig Config `json:"container_config,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Author string `json:"author,omitempty"`
Config *Config `json:"config,omitempty"`
Architecture string `json:"architecture,omitempty"`
OS string `json:"os,omitempty"`
Size int64
}
type Delete struct {
Deleted string `json:",omitempty"`
Untagged string `json:",omitempty"`
}
type ImageService struct {
*Client
}
// List Images
func (c *ImageService) List() ([]*Images, error) {
images := []*Images{}
err := c.do("GET", "/images/json?all=0", nil, &images)
return images, err
}
// Create an image, either by pull it from the registry or by importing it.
func (c *ImageService) Create(image string) error {
return c.do("POST", fmt.Sprintf("/images/create?fromImage=%s"), nil, nil)
}
func (c *ImageService) Pull(image string) error {
name, tag := utils.ParseRepositoryTag(image)
if len(tag) == 0 {
tag = DEFAULTTAG
}
return c.PullTag(name, tag)
}
func (c *ImageService) PullTag(name, tag string) error {
var out io.Writer
if Logging {
out = os.Stdout
}
path := fmt.Sprintf("/images/create?fromImage=%s&tag=%s", name, tag)
return c.stream("POST", path, nil, out, http.Header{})
}
// Remove the image name from the filesystem
func (c *ImageService) Remove(image string) ([]*Delete, error) {
resp := []*Delete{}
err := c.do("DELETE", fmt.Sprintf("/images/%s", image), nil, &resp)
return resp, err
}
// Inspect the image
func (c *ImageService) Inspect(name string) (*Image, error) {
image := Image{}
err := c.do("GET", fmt.Sprintf("/images/%s/json", name), nil, &image)
return &image, err
}
// Build the Image
func (c *ImageService) Build(tag, dir string) error {
// tar the file
context, err := archive.Tar(dir, archive.Uncompressed)
if err != nil {
return err
}
var body io.Reader
body = ioutil.NopCloser(context)
// Upload the build context
v := url.Values{}
v.Set("t", tag)
v.Set("q", "1")
//v.Set("rm", "1")
// url path
path := fmt.Sprintf("/build?%s", v.Encode())
// set content type to tar file
headers := http.Header{}
headers.Set("Content-Type", "application/tar")
// make the request
return c.stream("POST", path, body, nil, headers)
}

166
pkg/build/docker/structs.go Normal file
View File

@@ -0,0 +1,166 @@
package docker
import (
"fmt"
"strconv"
"strings"
"time"
)
// These are structures copied from the Docker project.
// We avoid importing the libraries due to a CGO
// depenency on libdevmapper that we'd like to avoid.
type KeyValuePair struct {
Key string
Value string
}
type HostConfig struct {
Binds []string
ContainerIDFile string
LxcConf []KeyValuePair
Privileged bool
PortBindings map[Port][]PortBinding
Links []string
PublishAllPorts bool
}
type Top struct {
Titles []string
Processes [][]string
}
type Containers struct {
ID string `json:"Id"`
Image string
Command string
Created int64
Status string
Ports []Port
SizeRw int64
SizeRootFs int64
Names []string
}
type Run struct {
ID string `json:"Id"`
Warnings []string `json:",omitempty"`
}
type Wait struct {
StatusCode int
}
type State struct {
Running bool
Pid int
ExitCode int
StartedAt time.Time
FinishedAt time.Time
Ghost bool
}
type PortBinding struct {
HostIp string
HostPort string
}
// 80/tcp
type Port string
func (p Port) Proto() string {
parts := strings.Split(string(p), "/")
if len(parts) == 1 {
return "tcp"
}
return parts[1]
}
func (p Port) Port() string {
return strings.Split(string(p), "/")[0]
}
func (p Port) Int() int {
i, err := parsePort(p.Port())
if err != nil {
panic(err)
}
return i
}
func parsePort(rawPort string) (int, error) {
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return 0, err
}
return int(port), nil
}
func NewPort(proto, port string) Port {
return Port(fmt.Sprintf("%s/%s", port, proto))
}
type PortMapping map[string]string // Deprecated
type NetworkSettings struct {
IPAddress string
IPPrefixLen int
Gateway string
Bridge string
PortMapping map[string]PortMapping // Deprecated
Ports map[Port][]PortBinding
}
type Config struct {
Hostname string
Domainname string
User string
Memory int64 // Memory limit (in bytes)
MemorySwap int64 // Total memory usage (memory + swap); set `-1' to disable swap
CpuShares int64 // CPU shares (relative weight vs. other containers)
AttachStdin bool
AttachStdout bool
AttachStderr bool
PortSpecs []string // Deprecated - Can be in the format of 8080/tcp
ExposedPorts map[Port]struct{}
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Env []string
Cmd []string
Dns []string
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
Volumes map[string]struct{}
VolumesFrom string
WorkingDir string
Entrypoint []string
NetworkDisabled bool
}
type Container struct {
ID string
Created time.Time
Path string
Args []string
Config *Config
State State
Image string
NetworkSettings *NetworkSettings
SysInitPath string
ResolvConfPath string
HostnamePath string
HostsPath string
Name string
Driver string
Volumes map[string]string
// Store rw/ro in a separate structure to preserve reverse-compatibility on-disk.
// Easier than migrating older container configs :)
VolumesRW map[string]bool
}

View File

@@ -0,0 +1,44 @@
package dockerfile
import (
"bytes"
"fmt"
)
type Dockerfile struct {
bytes.Buffer
}
func New(from string) *Dockerfile {
d := Dockerfile{}
d.WriteFrom(from)
return &d
}
func (d *Dockerfile) WriteAdd(from, to string) {
d.WriteString(fmt.Sprintf("ADD %s %s\n", from, to))
}
func (d *Dockerfile) WriteFrom(from string) {
d.WriteString(fmt.Sprintf("FROM %s\n", from))
}
func (d *Dockerfile) WriteRun(cmd string) {
d.WriteString(fmt.Sprintf("RUN %s\n", cmd))
}
func (d *Dockerfile) WriteUser(user string) {
d.WriteString(fmt.Sprintf("USER %s\n", user))
}
func (d *Dockerfile) WriteEnv(key, val string) {
d.WriteString(fmt.Sprintf("ENV %s %s\n", key, val))
}
func (d *Dockerfile) WriteWorkdir(workdir string) {
d.WriteString(fmt.Sprintf("WORKDIR %s\n", workdir))
}
func (d *Dockerfile) WriteEntrypoint(entrypoint string) {
d.WriteString(fmt.Sprintf("ENTRYPOINT %s\n", entrypoint))
}

238
pkg/build/images.go Normal file
View File

@@ -0,0 +1,238 @@
package build
type image struct {
// default ports the service will run on.
// for example, 3306 for mysql. Note that a service
// may expose multiple prots, for example, Riak
// exposes 8087 and 8089.
Ports []string
// tag of the docker image to pull in order
// to run this service.
Tag string
// display name of the image type
Name string
}
// List of 3rd party services (database, queue, etc) that
// are known to work with this Build utility.
var services = map[string]*image{
// neo4j
"neo4j": {
Ports: []string{"7474"},
Tag: "bradrydzewski/neo4j:1.9",
Name: "neo4j",
},
"neo4j:1.9": {
Ports: []string{"7474"},
Tag: "bradrydzewski/neo4j:1.9",
Name: "neo4j",
},
// elasticsearch servers
"elasticsearch": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.90",
Name: "elasticsearch",
},
"elasticsearch:0.20": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.20",
Name: "elasticsearch",
},
"elasticsearch:0.90": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.90",
Name: "elasticsearch",
},
// redis servers
"redis": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.8",
Name: "redis",
},
"redis:2.8": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.8",
Name: "redis",
},
"redis:2.6": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.6",
Name: "redis",
},
// mysql servers
"mysql": {
Tag: "bradrydzewski/mysql:5.5",
Ports: []string{"3306"},
Name: "mysql",
},
"mysql:5.5": {
Tag: "bradrydzewski/mysql:5.5",
Ports: []string{"3306"},
Name: "mysql",
},
// memcached
"memcached": {
Ports: []string{"11211"},
Tag: "bradrydzewski/memcached",
Name: "memcached",
},
// mongodb
"mongodb": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.4",
Name: "mongodb",
},
"mongodb:2.4": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.4",
Name: "mongodb",
},
"mongodb:2.2": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.2",
Name: "mongodb",
},
// postgres
"postgres": {
Ports: []string{"5432"},
Tag: "bradrydzewski/postgres:9.1",
Name: "postgres",
},
"postgres:9.1": {
Ports: []string{"5432"},
Tag: "bradrydzewski/postgres:9.1",
Name: "postgres",
},
// couchdb
"couchdb": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.0",
Name: "couchdb",
},
"couchdb:1.0": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.0",
Name: "couchdb",
},
"couchdb:1.4": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.4",
Name: "couchdb",
},
"couchdb:1.5": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.5",
Name: "couchdb",
},
// rabbitmq
"rabbitmq": {
Ports: []string{"5672", "15672"},
Tag: "bradrydzewski/rabbitmq:3.2",
Name: "rabbitmq",
},
"rabbitmq:3.2": {
Ports: []string{"5672", "15672"},
Tag: "bradrydzewski/rabbitmq:3.2",
Name: "rabbitmq",
},
// experimental images from 3rd parties
"zookeeper": {
Ports: []string{"2181"},
Tag: "jplock/zookeeper:3.4.5",
Name: "zookeeper",
},
// cassandra
"cassandra": {
Ports: []string{"9042", "7000", "7001", "7199", "9160", "49183"},
Tag: "relateiq/cassandra",
Name: "cassandra",
},
// riak - TESTED
"riak": {
Ports: []string{"8087", "8098"},
Tag: "guillermo/riak",
Name: "riak",
},
}
// List of official Drone build images.
var builders = map[string]*image{
// Clojure build images
"lein": {Tag: "bradrydzewski/lein"},
// Dart build images
"dart": {Tag: "bradrydzewski/dart:stable"},
"dart_stable": {Tag: "bradrydzewski/dart:stable"},
"dart_dev": {Tag: "bradrydzewski/dart:dev"},
// Erlang build images
"erlang": {Tag: "bradrydzewski/erlang:R16B02"},
"erlangR16B": {Tag: "bradrydzewski/erlang:R16B"},
"erlangR16B02": {Tag: "bradrydzewski/erlang:R16B02"},
"erlangR16B01": {Tag: "bradrydzewski/erlang:R16B01"},
// GCC build images
"gcc": {Tag: "bradrydzewski/gcc:4.6"},
"gcc4.6": {Tag: "bradrydzewski/gcc:4.6"},
"gcc4.8": {Tag: "bradrydzewski/gcc:4.8"},
// Golang build images
"go": {Tag: "bradrydzewski/go:1.2"},
"go1": {Tag: "bradrydzewski/go:1.0"},
"go1.1": {Tag: "bradrydzewski/go:1.1"},
"go1.2": {Tag: "bradrydzewski/go:1.2"},
// Haskell build images
"haskell": {Tag: "bradrydzewski/haskell:7.4"},
"haskell7.4": {Tag: "bradrydzewski/haskell:7.4"},
// Java build images
"java": {Tag: "bradrydzewski/java:openjdk7"},
"openjdk6": {Tag: "bradrydzewski/java:openjdk6"},
"openjdk7": {Tag: "bradrydzewski/java:openjdk7"},
"oraclejdk7": {Tag: "bradrydzewski/java:oraclejdk7"},
"oraclejdk8": {Tag: "bradrydzewski/java:oraclejdk8"},
// Node build images
"node": {Tag: "bradrydzewski/node:0.10"},
"node0.10": {Tag: "bradrydzewski/node:0.10"},
"node0.8": {Tag: "bradrydzewski/node:0.8"},
// PHP build images
"php": {Tag: "bradrydzewski/php:5.5"},
"php5.5": {Tag: "bradrydzewski/php:5.5"},
"php5.4": {Tag: "bradrydzewski/php:5.4"},
// Python build images
"python": {Tag: "bradrydzewski/python:2.7"},
"python2.7": {Tag: "bradrydzewski/python:2.7"},
"python3.2": {Tag: "bradrydzewski/python:3.2"},
"python3.3": {Tag: "bradrydzewski/python:3.3"},
"pypy": {Tag: "bradrydzewski/python:pypy"},
// Ruby build images
"ruby": {Tag: "bradrydzewski/ruby:2.0.0"},
"ruby2.0.0": {Tag: "bradrydzewski/ruby:2.0.0"},
"ruby1.9.3": {Tag: "bradrydzewski/ruby:1.9.3"},
// Scala build images
"scala": {Tag: "bradrydzewski/scala:2.10.3"},
"scala2.10.3": {Tag: "bradrydzewski/scala:2.10.3"},
"scala2.9.3": {Tag: "bradrydzewski/scala:2.9.3"},
}

105
pkg/build/log/log.go Normal file
View File

@@ -0,0 +1,105 @@
package log
import (
"fmt"
"io"
"os"
"sync"
)
const (
LOG_EMERG = iota
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
)
var mu sync.Mutex
// the default Log priority
var priority int = LOG_DEBUG
// the default Log output destination
var output io.Writer = os.Stdout
// the log prefix
var prefix string
// the log suffix
var suffix string = "/n"
// SetPriority sets the default log level.
func SetPriority(level int) {
mu.Lock()
defer mu.Unlock()
priority = level
}
// SetOutput sets the output destination.
func SetOutput(w io.Writer) {
mu.Lock()
defer mu.Unlock()
output = w
}
// SetPrefix sets the prefix for the log message.
func SetPrefix(pre string) {
mu.Lock()
defer mu.Unlock()
prefix = pre
}
// SetSuffix sets the suffix for the log message.
func SetSuffix(suf string) {
mu.Lock()
defer mu.Unlock()
suffix = suf
}
func Write(out string, level int) {
mu.Lock()
defer mu.Unlock()
// append the prefix and suffix
out = prefix + out + suffix
if priority >= level {
output.Write([]byte(out))
}
}
func Debug(out string) {
Write(out, LOG_DEBUG)
}
func Debugf(format string, a ...interface{}) {
Debug(fmt.Sprintf(format, a...))
}
func Info(out string) {
Write(out, LOG_INFO)
}
func Infof(format string, a ...interface{}) {
Info(fmt.Sprintf(format, a...))
}
func Err(out string) {
Write(out, LOG_ERR)
}
func Errf(format string, a ...interface{}) {
Err(fmt.Sprintf(format, a...))
}
func Notice(out string) {
Write(out, LOG_NOTICE)
}
func Noticef(format string, a ...interface{}) {
Notice(fmt.Sprintf(format, a...))
}

41
pkg/build/proxy/proxy.go Normal file
View File

@@ -0,0 +1,41 @@
package proxy
import (
"bytes"
"fmt"
)
// bash header
const header = "#!/bin/bash\n"
// this command string will check if the socat utility
// exists, and if it does, will proxy connections to
// the external IP address.
const command = "[ -x /usr/bin/socat ] && socat TCP-LISTEN:%s,fork TCP:%s:%s &\n"
// Proxy stores proxy configuration details mapping
// a local port to an external IP address with the
// same port number.
type Proxy map[string]string
func (p Proxy) Set(port, ip string) {
p[port] = ip
}
// String converts the proxy configuration details
// to a bash script.
func (p Proxy) String() string {
var buf bytes.Buffer
buf.WriteString(header)
for port, ip := range p {
buf.WriteString(fmt.Sprintf(command, port, ip, port))
}
return buf.String()
}
// Bytes converts the proxy configuration details
// to a bash script in byte array format.
func (p Proxy) Bytes() []byte {
return []byte(p.String())
}

View File

@@ -0,0 +1,32 @@
package proxy
import (
"testing"
)
func TestProxy(t *testing.T) {
// test creating a proxy with a few different
// addresses, and our ability to create the
// proxy shell script.
p := Proxy{}
p.Set("8080", "172.1.4.5")
p.Set("8000", "172.1.3.1")
b := p.Bytes()
expected := `#!/bin/bash
[ -x /usr/bin/socat ] && socat TCP-LISTEN:8080,fork TCP:172.1.4.5:8080 &
[ -x /usr/bin/socat ] && socat TCP-LISTEN:8000,fork TCP:172.1.3.1:8000 &
`
if string(b) != expected {
t.Errorf("Invalid proxy \n%s", expected)
}
// test creating a proxy script when there
// are no proxy addresses added to the map
p = Proxy{}
b = p.Bytes()
expected = "#!/bin/bash\n"
if string(b) != expected {
t.Errorf("Invalid proxy \n%s", expected)
}
}

118
pkg/build/repo/repo.go Normal file
View File

@@ -0,0 +1,118 @@
package repo
import (
"fmt"
"strings"
)
type Repo struct {
// The path of the Repoisotry. This could be
// the remote path of a Git repository or the path of
// of the repository on the local file system.
//
// A remote path must start with http://, https://,
// git://, ssh:// or git@. Otherwise we'll assume
// the repository is located on the local filesystem.
Path string
// (optional) Specific Branch that we should checkout
// when the Repository is cloned. If no value is
// provided we'll assume the default, master branch.
Branch string
// (optional) Specific Commit Hash that we should
// checkout when the Repository is cloned. If no
// value is provided we'll assume HEAD.
Commit string
// (optional) Pull Request number that we should
// checkout when the Repository is cloned.
PR string
// (optional) The filesystem path that the repository
// will be cloned into (or copied to) inside the
// host system (Docker Container).
Dir string
}
// IsRemote returns true if the Repository is located
// on a remote server (ie Github, Bitbucket)
func (r *Repo) IsRemote() bool {
switch {
case strings.HasPrefix(r.Path, "git://"):
return true
case strings.HasPrefix(r.Path, "git@"):
return true
case strings.HasPrefix(r.Path, "http://"):
return true
case strings.HasPrefix(r.Path, "https://"):
return true
case strings.HasPrefix(r.Path, "ssh://"):
return true
}
return false
}
// IsLocal returns true if the Repository is located
// on the local filesystem.
func (r *Repo) IsLocal() bool {
return !r.IsRemote()
}
// IsGit returns true if the Repository is
// a Git repoisitory.
func (r *Repo) IsGit() bool {
switch {
case strings.HasPrefix(r.Path, "git://"):
return true
case strings.HasPrefix(r.Path, "git@"):
return true
case strings.HasPrefix(r.Path, "ssh://git@"):
return true
case strings.HasPrefix(r.Path, "https://github.com/"):
return true
case strings.HasPrefix(r.Path, "http://github.com"):
return true
case strings.HasSuffix(r.Path, ".git"):
return true
}
// we could also ping the repository to check
return false
}
// returns commands that can be used in a Dockerfile
// to clone the repository.
//
// TODO we should also enable Mercurial projects and SVN projects
func (r *Repo) Commands() []string {
// get the branch. default to master
// if no branch exists.
branch := r.Branch
if len(branch) == 0 {
branch = "master"
}
cmds := []string{}
cmds = append(cmds, fmt.Sprintf("git clone --branch=%s %s %s", branch, r.Path, r.Dir))
switch {
// if a specific commit is provided then we'll
// need to clone it.
case len(r.PR) > 0:
cmds = append(cmds, fmt.Sprintf("git fetch origin +refs/pull/%s/head:refs/remotes/origin/pr/%s", r.PR, r.PR))
cmds = append(cmds, fmt.Sprintf("git checkout -qf pr/%s", r.PR))
//cmds = append(cmds, fmt.Sprintf("git fetch origin +refs/pull/%s/merge:", r.PR))
//cmds = append(cmds, fmt.Sprintf("git checkout -qf %s", "FETCH_HEAD"))
// if a specific commit is provided then we'll
// need to clone it.
case len(r.Commit) > 0:
cmds = append(cmds, fmt.Sprintf("git checkout -qf %s", r.Commit))
}
return cmds
}

View File

@@ -0,0 +1,54 @@
package repo
import (
"testing"
)
func TestIsRemote(t *testing.T) {
repos := []struct {
path string
remote bool
}{
{"git://github.com/foo/far", true},
{"git://github.com/foo/far.git", true},
{"git@github.com:foo/far", true},
{"git@github.com:foo/far.git", true},
{"http://github.com/foo/far.git", true},
{"https://github.com/foo/far.git", true},
{"ssh://baz.com/foo/far.git", true},
{"/var/lib/src", false},
{"/home/ubuntu/src", false},
{"src", false},
}
for _, r := range repos {
repo := Repo{Path: r.path}
if remote := repo.IsRemote(); remote != r.remote {
t.Errorf("IsRemote %s was %v, expected %v", r.path, remote, r.remote)
}
}
}
func TestIsGit(t *testing.T) {
repos := []struct {
path string
remote bool
}{
{"git://github.com/foo/far", true},
{"git://github.com/foo/far.git", true},
{"git@github.com:foo/far", true},
{"git@github.com:foo/far.git", true},
{"http://github.com/foo/far.git", true},
{"https://github.com/foo/far.git", true},
{"ssh://baz.com/foo/far.git", true},
{"svn://gcc.gnu.org/svn/gcc/branches/gccgo", false},
{"https://code.google.com/p/go", false},
}
for _, r := range repos {
repo := Repo{Path: r.path}
if remote := repo.IsGit(); remote != r.remote {
t.Errorf("IsGit %s was %v, expected %v", r.path, remote, r.remote)
}
}
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type AppFog struct {
}
func (a *AppFog) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudControl struct {
}
func (c *CloudControl) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudFoundry struct {
}
func (c *CloudFoundry) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1,42 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Deploy stores the configuration details
// for deploying build artifacts when
// a Build has succeeded
type Deploy struct {
AppFog *AppFog `yaml:"appfog,omitempty"`
CloudControl *CloudControl `yaml:"cloudcontrol,omitempty"`
CloudFoundry *CloudFoundry `yaml:"cloudfoundry,omitempty"`
EngineYard *EngineYard `yaml:"engineyard,omitempty"`
Heroku *Heroku `yaml:"heroku,omitempty"`
Nodejitsu *Nodejitsu `yaml:"nodejitsu,omitempty"`
Openshift *Openshift `yaml:"openshift,omitempty"`
}
func (d *Deploy) Write(f *buildfile.Buildfile) {
if d.AppFog != nil {
d.AppFog.Write(f)
}
if d.CloudControl != nil {
d.CloudControl.Write(f)
}
if d.CloudFoundry != nil {
d.CloudFoundry.Write(f)
}
if d.EngineYard != nil {
d.EngineYard.Write(f)
}
if d.Heroku != nil {
d.Heroku.Write(f)
}
if d.Nodejitsu != nil {
d.Nodejitsu.Write(f)
}
if d.Openshift != nil {
d.Openshift.Write(f)
}
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type EngineYard struct {
}
func (e *EngineYard) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1 @@
package deployment

View File

@@ -0,0 +1,38 @@
package deployment
import (
"fmt"
"github.com/drone/drone/pkg/build/buildfile"
)
type Heroku struct {
App string `yaml:"app,omitempty"`
Force bool `yaml:"force,omitempty"`
Branch string `yaml:"branch,omitempty"`
}
func (h *Heroku) Write(f *buildfile.Buildfile) {
// get the current commit hash
f.WriteCmdSilent("COMMIT=$(git rev-parse HEAD)")
// set the git user and email based on the individual
// that made the commit.
f.WriteCmdSilent("git config --global user.name $(git --no-pager log -1 --pretty=format:'%an')")
f.WriteCmdSilent("git config --global user.email $(git --no-pager log -1 --pretty=format:'%ae')")
// add heroku as a git remote
f.WriteCmd(fmt.Sprintf("git remote add heroku git@heroku.com:%s.git", h.App))
switch h.Force {
case true:
// this is useful when the there are artifacts generated
// by the build script, such as less files converted to css,
// that need to be deployed to Heroku.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'adding build artifacts'"))
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master"))
}
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Nodejitsu struct {
}
func (n *Nodejitsu) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Openshift struct {
}
func (o *Openshift) Write(f *buildfile.Buildfile) {
}

View File

@@ -0,0 +1 @@
package deployment

View File

@@ -0,0 +1,85 @@
package notification
import (
"fmt"
"net/smtp"
)
type Email struct {
Recipients []string `yaml:"recipients,omitempty"`
Success string `yaml:"on_success"`
Failure string `yaml:"on_failure"`
host string // smtp host address
port string // smtp host port
user string // smtp username for authentication
pass string // smtp password for authentication
from string // smtp email address. send from this address
}
// SetServer is a function that will set the SMTP
// server location and credentials
func (e *Email) SetServer(host, port, user, pass, from string) {
e.host = host
e.port = port
e.user = user
e.pass = pass
e.from = from
}
// Send will send an email, either success or failure,
// based on the Commit Status.
func (e *Email) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && e.Success != "never":
return e.sendSuccess(context)
case context.Commit.Status == "Failure" && e.Failure != "never":
return e.sendFailure(context)
}
return nil
}
// sendFailure sends email notifications to the list of
// recipients indicating the build failed.
func (e *Email) sendFailure(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendFailure(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// sendSuccess sends email notifications to the list of
// recipients indicating the build was a success.
func (e *Email) sendSuccess(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendSuccess(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// send is a simple helper function to format and
// send an email message.
func (e *Email) send(to, subject, body string) error {
// Format the raw email message body
raw := fmt.Sprintf(emailTemplate, e.from, to, subject, body)
auth := smtp.PlainAuth("", e.user, e.pass, e.host)
addr := fmt.Sprintf("%s:%s", e.host, e.port)
return smtp.SendMail(addr, auth, e.from, []string{to}, []byte(raw))
}
// text-template used to generate a raw Email message
var emailTemplate = `From: %s
To: %s
Subject: %s
MIME-version: 1.0
Content-Type: text/html; charset="UTF-8"
%s`

View File

@@ -0,0 +1,64 @@
package notification
import (
"fmt"
"github.com/andybons/hipchat"
)
const (
startedMessage = "Building %s, commit %s, author %s"
successMessage = "<b>Success</b> %s, commit %s, author %s"
failureMessage = "<b>Failed</b> %s, commit %s, author %s"
)
type Hipchat struct {
Room string `yaml:"room,omitempty"`
Token string `yaml:"token,omitempty"`
Started bool `yaml:"on_started,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (h *Hipchat) Send(context *Context) error {
switch {
case context.Commit.Status == "Started" && h.Started:
return h.sendStarted(context)
case context.Commit.Status == "Success" && h.Success:
return h.sendSuccess(context)
case context.Commit.Status == "Failure" && h.Failure:
return h.sendFailure(context)
}
return nil
}
func (h *Hipchat) sendStarted(context *Context) error {
msg := fmt.Sprintf(startedMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorYellow, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendFailure(context *Context) error {
msg := fmt.Sprintf(failureMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorRed, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendSuccess(context *Context) error {
msg := fmt.Sprintf(successMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorGreen, hipchat.FormatHTML, msg)
}
// helper function to send Hipchat requests
func (h *Hipchat) send(color, format, message string) error {
c := hipchat.Client{AuthToken: h.Token}
req := hipchat.MessageRequest{
RoomId: h.Room,
From: "Drone",
Message: message,
Color: color,
MessageFormat: format,
Notify: true,
}
return c.PostMessage(req)
}

View File

@@ -0,0 +1 @@
package notification

View File

@@ -0,0 +1,53 @@
package notification
import (
"github.com/drone/drone/pkg/model"
)
// Context represents the context of an
// in-progress build request.
type Context struct {
// Global settings
Host string
// User that owns the repository
User *model.User
// Repository being built.
Repo *model.Repo
// Commit being built
Commit *model.Commit
}
type Sender interface {
Send(context *Context) error
}
// Notification stores the configuration details
// for notifying a user, or group of users,
// when their Build has completed.
type Notification struct {
Email *Email `yaml:"email,omitempty"`
Webhook *Webhook `yaml:"webhook,omitempty"`
Hipchat *Hipchat `yaml:"hipchat,omitempty"`
}
func (n *Notification) Send(context *Context) error {
// send email notifications
//if n.Email != nil && n.Email.Enabled {
// n.Email.Send(context)
//}
// send email notifications
if n.Webhook != nil {
n.Webhook.Send(context)
}
// send email notifications
if n.Hipchat != nil {
n.Hipchat.Send(context)
}
return nil
}

View File

@@ -0,0 +1,59 @@
package notification
import (
"bytes"
"encoding/json"
"net/http"
"github.com/drone/drone/pkg/model"
)
type Webhook struct {
URL []string `yaml:"urls,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (w *Webhook) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && w.Success:
return w.send(context)
case context.Commit.Status == "Failure" && w.Failure:
return w.send(context)
}
return nil
}
// helper function to send HTTP requests
func (w *Webhook) send(context *Context) error {
// data will get posted in this format
data := struct {
Owner *model.User `json:"owner"`
Repo *model.Repo `json:"repository"`
Commit *model.Commit `json:"commit"`
}{context.User, context.Repo, context.Commit}
// data json encoded
payload, err := json.Marshal(data)
if err != nil {
return err
}
// loop through and email recipients
for _, url := range w.URL {
go sendJson(url, payload)
}
return nil
}
// helper fuction to sent HTTP Post requests
// with JSON data as the payload.
func sendJson(url string, payload []byte) {
buf := bytes.NewBuffer(payload)
resp, err := http.Post(url, "application/json", buf)
if err != nil {
return
}
resp.Body.Close()
}

View File

@@ -0,0 +1 @@
package notification

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1 @@
package publish

View File

@@ -0,0 +1,18 @@
package publish
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Publish stores the configuration details
// for publishing build artifacts when
// a Build has succeeded
type Publish struct {
S3 *S3 `yaml:"s3,omitempty"`
}
func (p *Publish) Write(f *buildfile.Buildfile) {
if p.S3 != nil {
p.S3.Write(f)
}
}

View File

@@ -0,0 +1,2 @@
package publish

View File

@@ -0,0 +1,85 @@
package publish
import (
"fmt"
"strings"
"github.com/drone/drone/pkg/build/buildfile"
)
type S3 struct {
Key string `yaml:"access_key,omitempty"`
Secret string `yaml:"secret_key,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
// us-east-1
// us-west-1
// us-west-2
// eu-west-1
// ap-southeast-1
// ap-southeast-2
// ap-northeast-1
// sa-east-1
Region string `yaml:"region,omitempty"`
// Indicates the files ACL, which should be one
// of the following:
// private
// public-read
// public-read-write
// authenticated-read
// bucket-owner-read
// bucket-owner-full-control
Access string `yaml:"acl,omitempty"`
// Copies the files from the specified directory.
// Regexp matching will apply to match multiple
// files
//
// Examples:
// /path/to/file
// /path/to/*.txt
// /path/to/*/*.txt
// /path/to/**
Source string `yaml:"source,omitempty"`
Target string `yaml:"target,omitempty"`
// Recursive uploads
Recursive bool `yaml:"recursive"`
Branch string `yaml:"branch,omitempty"`
}
func (s *S3) Write(f *buildfile.Buildfile) {
// install the AWS cli using PIP
f.WriteCmdSilent("[ -f /usr/bin/sudo ] || pip install awscli 1> /dev/null 2> /dev/null")
f.WriteCmdSilent("[ -f /usr/bin/sudo ] && sudo pip install awscli 1> /dev/null 2> /dev/null")
f.WriteEnv("AWS_ACCESS_KEY_ID", s.Key)
f.WriteEnv("AWS_SECRET_ACCESS_KEY", s.Secret)
// make sure a default region is set
if len(s.Region) == 0 {
s.Region = "us-east-1"
}
// make sure a default access is set
// let's be conservative and assume private
if len(s.Region) == 0 {
s.Region = "private"
}
// if the target starts with a "/" we need
// to remove it, otherwise we might adding
// a 3rd slash to s3://
if strings.HasPrefix(s.Target, "/") {
s.Target = s.Target[1:]
}
switch s.Recursive {
case true:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --recursive --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
case false:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
}
}

View File

@@ -0,0 +1,5 @@
cobertura.go
coveralls.go
gocov.go
junit.go
phpunit.go

123
pkg/build/script/script.go Normal file
View File

@@ -0,0 +1,123 @@
package script
import (
"io/ioutil"
"strings"
"launchpad.net/goyaml"
"github.com/drone/drone/pkg/build/buildfile"
"github.com/drone/drone/pkg/build/script/deployment"
"github.com/drone/drone/pkg/build/script/notification"
"github.com/drone/drone/pkg/build/script/publish"
)
func ParseBuild(data []byte) (*Build, error) {
build := Build{}
// parse the build configuration file
err := goyaml.Unmarshal(data, &build)
return &build, err
}
func ParseBuildFile(filename string) (*Build, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return ParseBuild(data)
}
// Build stores the configuration details for
// building, testing and deploying code.
type Build struct {
// Image specifies the Docker Image that will be
// used to virtualize the Build process.
Image string
// Name specifies a user-defined label used
// to identify the build.
Name string
// Script specifies the build and test commands.
Script []string
// Env specifies the environment of the build.
Env []string
// Services specifies external services, such as
// database or messaging queues, that should be
// linked to the build environment.
Services []string
Deploy *deployment.Deploy `yaml:"deploy,omitempty"`
Publish *publish.Publish `yaml:"publish,omitempty"`
Notifications *notification.Notification `yaml:"notify,omitempty"`
}
// Write adds all the steps to the build script, including
// build commands, deploy and publish commands.
func (b *Build) Write(f *buildfile.Buildfile) {
// append build commands
b.WriteBuild(f)
// write publish commands
if b.Publish != nil {
b.Publish.Write(f)
}
// write deployment commands
if b.Deploy != nil {
b.Deploy.Write(f)
}
}
// WriteBuild adds only the build steps to the build script,
// omitting publish and deploy steps. This is important for
// pull requests, where deployment would be undesirable.
func (b *Build) WriteBuild(f *buildfile.Buildfile) {
// append environment variables
for _, env := range b.Env {
parts := strings.Split(env, "=")
if len(parts) != 2 {
continue
}
f.WriteEnv(parts[0], parts[1])
}
// append build commands
for _, cmd := range b.Script {
f.WriteCmd(cmd)
}
}
type Publish interface {
Write(f *buildfile.Buildfile)
}
type Deployment interface {
Write(f *buildfile.Buildfile)
}
type Notification interface {
Set(c Context)
}
type Context interface {
Host() string
Owner() string
Name() string
Branch() string
Hash() string
Status() string
Message() string
Author() string
Gravatar() string
Duration() int64
HumanDuration() string
//Settings
}

28
pkg/build/util.go Normal file
View File

@@ -0,0 +1,28 @@
package build
import (
"crypto/rand"
"crypto/sha1"
"fmt"
"io"
)
// createUID is a helper function that will
// create a random, unique identifier.
func createUID() string {
c := sha1.New()
r := createRandom()
io.WriteString(c, string(r))
s := fmt.Sprintf("%x", c.Sum(nil))
return "drone-" + s[0:10]
}
// createRandom creates a random block of bytes
// that we can use to generate unique identifiers.
func createRandom() []byte {
k := make([]byte, sha1.BlockSize)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

57
pkg/build/writer.go Normal file
View File

@@ -0,0 +1,57 @@
package build
import (
//"bytes"
"fmt"
"io"
"strings"
)
var (
// the prefix used to determine if this is
// data that should be stripped from the output
prefix = []byte("#DRONE:")
)
// custom writer to intercept the build
// output
type writer struct {
io.Writer
}
// Write appends the contents of p to the buffer. It will
// scan for DRONE special formatting codes embedded in the
// output, and will alter the output accordingly.
func (w *writer) Write(p []byte) (n int, err error) {
lines := strings.Split(string(p), "\n")
for i, line := range lines {
if strings.HasPrefix(line, "#DRONE:") {
var cmd string
// extract the command (base16 encoded)
// from the output
fmt.Sscanf(line[7:], "%x", &cmd)
// echo the decoded command
cmd = fmt.Sprintf("$ %s", cmd)
w.Writer.Write([]byte(cmd))
} else {
w.Writer.Write([]byte(line))
}
if i < len(lines)-1 {
w.Writer.Write([]byte("\n"))
}
}
return len(p), nil
}
// WriteString appends the contents of s to the buffer.
func (w *writer) WriteString(s string) (n int, err error) {
return w.Write([]byte(s))
}

27
pkg/build/writer_test.go Normal file
View File

@@ -0,0 +1,27 @@
package build
import (
"bytes"
"testing"
)
func TestSetupDockerfile(t *testing.T) {
var buf bytes.Buffer
// wrap the buffer so we can analyze output
w := writer{&buf}
w.WriteString("#DRONE:676f206275696c64\n")
w.WriteString("#DRONE:676f2074657374202d76\n")
w.WriteString("PASS\n")
w.WriteString("ok github.com/garyburd/redigo/redis 0.113s\n")
expected := `$ go build
$ go test -v
PASS
ok github.com/garyburd/redigo/redis 0.113s
`
if expected != buf.String() {
t.Errorf("Expected commands decoded and echoed correctly. got \n%s", buf.String())
}
}