package builtin import ( "bytes" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "io/ioutil" "os" "time" "github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient" "github.com/drone/drone/pkg/docker" "github.com/drone/drone/pkg/queue" "github.com/drone/drone/pkg/types" log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus" ) var ( // Defult docker host address DefaultHost = "unix:///var/run/docker.sock" // Docker host address from environment variable DockerHost = os.Getenv("DOCKER_HOST") // Docker TLS variables DockerHostCa = os.Getenv("DOCKER_CA") DockerHostKey = os.Getenv("DOCKER_KEY") DockerHostCert = os.Getenv("DOCKER_CERT") ) func init() { // if the environment doesn't specify a DOCKER_HOST // we should use the default Docker socket. if len(DockerHost) == 0 { DockerHost = DefaultHost } } type Runner struct { Updater } func newDockerClient() (dockerclient.Client, error) { var tlc *tls.Config // create the Docket client TLS config if len(DockerHostCert) > 0 && len(DockerHostKey) > 0 && len(DockerHostCa) > 0 { cert, err := tls.LoadX509KeyPair(DockerHostCert, DockerHostKey) if err != nil { log.Errorf("failure to load SSL cert and key. %s", err) return dockerclient.NewDockerClient(DockerHost, nil) } caCert, err := ioutil.ReadFile(DockerHostCa) if err != nil { log.Errorf("failure to load SSL CA cert. %s", err) return dockerclient.NewDockerClient(DockerHost, nil) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) tlc = &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: caCertPool, } } // create the Docker client. In this version of Drone (alpha) // we do not spread builds across clients, but this can and // (probably) will change in the future. return dockerclient.NewDockerClient(DockerHost, tlc) } func (r *Runner) Run(w *queue.Work) error { var workers []*worker var client dockerclient.Client defer func() { recover() // ensures that all containers have been removed // from the host machine. for _, worker := range workers { worker.Remove() } // if any part of the commit fails and leaves // behind orphan sub-builds we need to cleanup // after ourselves. if w.Build.Status == types.StateRunning { // if any tasks are running or pending // we should mark them as complete. for _, b := range w.Build.Jobs { if b.Status == types.StateRunning { b.Status = types.StateError b.Finished = time.Now().UTC().Unix() b.ExitCode = 255 } if b.Status == types.StatePending { b.Status = types.StateError b.Started = time.Now().UTC().Unix() b.Finished = time.Now().UTC().Unix() b.ExitCode = 255 } r.SetJob(w.Repo, w.Build, b) } // must populate build start if w.Build.Started == 0 { w.Build.Started = time.Now().UTC().Unix() } // mark the build as complete (with error) w.Build.Status = types.StateError w.Build.Finished = time.Now().UTC().Unix() r.SetBuild(w.User, w.Repo, w.Build) } }() // marks the build as running w.Build.Started = time.Now().UTC().Unix() w.Build.Status = types.StateRunning err := r.SetBuild(w.User, w.Repo, w.Build) if err != nil { log.Errorf("failure to set build. %s", err) return err } // create the Docker client. In this version of Drone (alpha) // we do not spread builds across clients, but this can and // (probably) will change in the future. client, err = newDockerClient() if err != nil { log.Errorf("failure to connect to docker. %s", err) return err } // loop through and execute the build and // clone steps for each build job. for _, job := range w.Build.Jobs { // marks the task as running job.Status = types.StateRunning job.Started = time.Now().UTC().Unix() err = r.SetJob(w.Repo, w.Build, job) if err != nil { log.Errorf("failure to set job. %s", err) return err } work := &work{ System: w.System, Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys}, Repo: w.Repo, Build: w.Build, Job: job, Secret: string(w.Secret), Config: string(w.Config), } in, err := json.Marshal(work) if err != nil { log.Errorf("failure to marshalise work. %s", err) return err } worker := newWorker(client) workers = append(workers, worker) cname := cname(job) pullrequest := (w.Build.PullRequest != nil && w.Build.PullRequest.Number != 0) state, builderr := worker.Build(cname, in, pullrequest) switch { case state == 128: job.ExitCode = state job.Status = types.StateKilled case state == 130: job.ExitCode = state job.Status = types.StateKilled case builderr != nil: job.Status = types.StateError case state != 0: job.ExitCode = state job.Status = types.StateFailure default: job.Status = types.StateSuccess } // send the logs to the datastore var buf bytes.Buffer rc, err := worker.Logs() if err != nil && builderr != nil { buf.WriteString("001 Error launching build") buf.WriteString(builderr.Error()) } else if err != nil { buf.WriteString("002 Error launching build") buf.WriteString(err.Error()) return err } else { defer rc.Close() docker.StdCopy(&buf, &buf, rc) } err = r.SetLogs(w.Repo, w.Build, job, ioutil.NopCloser(&buf)) if err != nil { return err } // update the task in the datastore job.Finished = time.Now().UTC().Unix() err = r.SetJob(w.Repo, w.Build, job) if err != nil { return err } } // update the build state if any of the sub-tasks // had a non-success status w.Build.Status = types.StateSuccess for _, job := range w.Build.Jobs { if job.Status != types.StateSuccess { w.Build.Status = job.Status break } } err = r.SetBuild(w.User, w.Repo, w.Build) if err != nil { return err } // loop through and execute the notifications and // the destroy all containers afterward. for i, job := range w.Build.Jobs { work := &work{ System: w.System, Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys}, Repo: w.Repo, Build: w.Build, Job: job, Secret: string(w.Secret), Config: string(w.Config), } in, err := json.Marshal(work) if err != nil { return err } workers[i].Notify(in) break } return nil } func (r *Runner) Cancel(job *types.Job) error { client, err := newDockerClient() if err != nil { return err } return client.StopContainer(cname(job), 30) } func (r *Runner) Logs(job *types.Job) (io.ReadCloser, error) { client, err := newDockerClient() if err != nil { return nil, err } // make sure this container actually exists info, err := client.InspectContainer(cname(job)) if err != nil { // add a small exponential backoff since there // is a small window when the container hasn't // been created yet, but the build is about to start for i := 0; ; i++ { time.Sleep(1 * time.Second) info, err = client.InspectContainer(cname(job)) if err != nil && i == 5 { return nil, err } if err == nil { break } } } // verify the container is running. if not we'll // do an exponential backoff and attempt to wait if !info.State.Running { for i := 0; ; i++ { time.Sleep(1 * time.Second) info, err = client.InspectContainer(info.Id) if err != nil { return nil, err } if info.State.Running { break } if i == 5 { return nil, dockerclient.ErrNotFound } } } return client.ContainerLogs(info.Id, logOptsTail) } func cname(job *types.Job) string { return fmt.Sprintf("drone-%d", job.ID) } func (r *Runner) Poll(q queue.Queue) { for { w := q.Pull() q.Ack(w) err := r.Run(w) if err != nil { log.Error(err) } } }