github.com/unclejack/drone@v0.2.1-0.20140918182345-831b034aa33b/pkg/queue/worker.go (about)

     1  package queue
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"github.com/drone/drone/pkg/build/git"
     7  	r "github.com/drone/drone/pkg/build/repo"
     8  	"github.com/drone/drone/pkg/build/script"
     9  	"github.com/drone/drone/pkg/channel"
    10  	"github.com/drone/drone/pkg/database"
    11  	. "github.com/drone/drone/pkg/model"
    12  	"github.com/drone/drone/pkg/plugin/notify"
    13  	"github.com/drone/go-github/github"
    14  	"io"
    15  	"log"
    16  	"net/url"
    17  	"path/filepath"
    18  	"time"
    19  )
    20  
    21  type worker struct {
    22  	runner BuildRunner
    23  }
    24  
    25  // work is a function that will infinitely
    26  // run in the background waiting for tasks that
    27  // it can pull off the queue and execute.
    28  func (w *worker) work(queue <-chan *BuildTask) {
    29  	var task *BuildTask
    30  	for {
    31  		// get work item (pointer) from the queue
    32  		task = <-queue
    33  		if task == nil {
    34  			continue
    35  		}
    36  
    37  		// execute the task
    38  		w.execute(task)
    39  	}
    40  }
    41  
    42  // execute will execute the build task and persist
    43  // the results to the datastore.
    44  func (w *worker) execute(task *BuildTask) error {
    45  	// we need to be sure that we can recover
    46  	// from any sort panic that could occur
    47  	// to avoid brining down the entire application
    48  	defer func() {
    49  		if e := recover(); e != nil {
    50  			task.Build.Finished = time.Now().UTC()
    51  			task.Commit.Finished = time.Now().UTC()
    52  			task.Build.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix()
    53  			task.Commit.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix()
    54  			task.Commit.Status = "Error"
    55  			task.Build.Status = "Error"
    56  			database.SaveBuild(task.Build)
    57  			database.SaveCommit(task.Commit)
    58  		}
    59  	}()
    60  
    61  	// update commit and build status
    62  	task.Commit.Status = "Started"
    63  	task.Build.Status = "Started"
    64  	task.Build.Started = time.Now().UTC()
    65  	task.Commit.Started = time.Now().UTC()
    66  
    67  	// persist the commit to the database
    68  	if err := database.SaveCommit(task.Commit); err != nil {
    69  		return err
    70  	}
    71  
    72  	// persist the build to the database
    73  	if err := database.SaveBuild(task.Build); err != nil {
    74  		return err
    75  	}
    76  
    77  	// get settings
    78  	settings, _ := database.GetSettings()
    79  
    80  	// notification context
    81  	context := &notify.Context{
    82  		Repo:   task.Repo,
    83  		Commit: task.Commit,
    84  		Host:   settings.URL().String(),
    85  	}
    86  
    87  	// parse the build script
    88  	buildscript, err := script.ParseBuild([]byte(task.Build.BuildScript), task.Repo.Params)
    89  	if err != nil {
    90  		log.Printf("Could not parse your .drone.yml file.  It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n")
    91  		return err
    92  	}
    93  
    94  	// send all "started" notifications
    95  	if buildscript.Notifications != nil {
    96  		buildscript.Notifications.Send(context)
    97  	}
    98  
    99  	// Send "started" notification to Github
   100  	if err := updateGitHubStatus(task.Repo, task.Commit); err != nil {
   101  		log.Printf("error updating github status: %s\n", err.Error())
   102  	}
   103  
   104  	// make sure a channel exists for the repository,
   105  	// the commit, and the commit output (TODO)
   106  	reposlug := fmt.Sprintf("%s/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name)
   107  	commitslug := fmt.Sprintf("%s/%s/%s/commit/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Hash)
   108  	consoleslug := fmt.Sprintf("%s/%s/%s/commit/%s/%s/builds/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Hash, task.Build.Slug)
   109  	channel.Create(reposlug)
   110  	channel.Create(commitslug)
   111  	channel.CreateStream(consoleslug)
   112  
   113  	// notify the channels that the commit and build started
   114  	channel.SendJSON(reposlug, task.Commit)
   115  	channel.SendJSON(commitslug, task.Build)
   116  
   117  	var buf = &bufferWrapper{channel: consoleslug}
   118  
   119  	// append private parameters to the environment
   120  	// variable section of the .drone.yml file, iff
   121  	// this is not a pull request (for security purposes)
   122  	if task.Repo.Params != nil && len(task.Commit.PullRequest) == 0 {
   123  		for k, v := range task.Repo.Params {
   124  			buildscript.Env = append(buildscript.Env, k+"="+v)
   125  		}
   126  	}
   127  
   128  	defer func() {
   129  		// update the status of the commit using the
   130  		// GitHub status API.
   131  		if err := updateGitHubStatus(task.Repo, task.Commit); err != nil {
   132  			log.Printf("error updating github status: %s\n", err.Error())
   133  		}
   134  	}()
   135  
   136  	// execute the build
   137  	passed, buildErr := w.runBuild(task, buildscript, buf)
   138  
   139  	task.Build.Finished = time.Now().UTC()
   140  	task.Commit.Finished = time.Now().UTC()
   141  	task.Build.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano()
   142  	task.Commit.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano()
   143  	task.Commit.Status = "Success"
   144  	task.Build.Status = "Success"
   145  	task.Build.Stdout = buf.buf.String()
   146  
   147  	// if exit code != 0 set to failure
   148  	if passed {
   149  		task.Commit.Status = "Failure"
   150  		task.Build.Status = "Failure"
   151  		if buildErr != nil && task.Build.Stdout == "" {
   152  			// TODO: If you wanted to have very friendly error messages, you could do that here
   153  			task.Build.Stdout = buildErr.Error() + "\n"
   154  		}
   155  	}
   156  
   157  	// persist the build to the database
   158  	if err := database.SaveBuild(task.Build); err != nil {
   159  		return err
   160  	}
   161  
   162  	// persist the commit to the database
   163  	if err := database.SaveCommit(task.Commit); err != nil {
   164  		return err
   165  	}
   166  
   167  	// notify the channels that the commit and build finished
   168  	channel.SendJSON(reposlug, task.Commit)
   169  	channel.SendJSON(commitslug, task.Build)
   170  	channel.Close(consoleslug)
   171  
   172  	// send all "finished" notifications
   173  	if buildscript.Notifications != nil {
   174  		buildscript.Notifications.Send(context)
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func (w *worker) runBuild(task *BuildTask, buildscript *script.Build, buf io.Writer) (bool, error) {
   181  	repo := &r.Repo{
   182  		Name:   task.Repo.Slug,
   183  		Path:   task.Repo.URL,
   184  		Branch: task.Commit.Branch,
   185  		Commit: task.Commit.Hash,
   186  		PR:     task.Commit.PullRequest,
   187  		Dir:    filepath.Join("/var/cache/drone/src", git.GitPath(buildscript.Git, task.Repo.Slug)),
   188  		Depth:  git.GitDepth(buildscript.Git),
   189  	}
   190  
   191  	return w.runner.Run(
   192  		buildscript,
   193  		repo,
   194  		[]byte(task.Repo.PrivateKey),
   195  		task.Repo.Privileged,
   196  		buf,
   197  	)
   198  }
   199  
   200  // updateGitHubStatus is a helper function that will send
   201  // the build status to GitHub using the Status API.
   202  // see https://github.com/blog/1227-commit-status-api
   203  func updateGitHubStatus(repo *Repo, commit *Commit) error {
   204  
   205  	// convert from drone status to github status
   206  	var message, status string
   207  	switch commit.Status {
   208  	case "Success":
   209  		status = "success"
   210  		message = "The build succeeded on drone.io"
   211  	case "Failure":
   212  		status = "failure"
   213  		message = "The build failed on drone.io"
   214  	case "Started":
   215  		status = "pending"
   216  		message = "The build is pending on drone.io"
   217  	default:
   218  		status = "error"
   219  		message = "The build errored on drone.io"
   220  	}
   221  
   222  	// get the system settings
   223  	settings, _ := database.GetSettings()
   224  
   225  	// get the user from the database
   226  	// since we need his / her GitHub token
   227  	user, err := database.GetUser(repo.UserID)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	client := github.New(user.GithubToken)
   233  	client.ApiUrl = settings.GitHubApiUrl
   234  	buildUrl := getBuildUrl(settings.URL().String(), repo, commit)
   235  
   236  	return client.Repos.CreateStatus(repo.Owner, repo.Name, status, buildUrl, message, commit.Hash)
   237  }
   238  
   239  func getBuildUrl(host string, repo *Repo, commit *Commit) string {
   240  	branchQuery := url.Values{}
   241  	branchQuery.Set("branch", commit.Branch)
   242  	buildUrl := fmt.Sprintf("%s/%s/commit/%s?%s", host, repo.Slug, commit.Hash, branchQuery.Encode())
   243  	return buildUrl
   244  }
   245  
   246  type bufferWrapper struct {
   247  	buf bytes.Buffer
   248  
   249  	// name of the channel
   250  	channel string
   251  }
   252  
   253  func (b *bufferWrapper) Write(p []byte) (n int, err error) {
   254  	n, err = b.buf.Write(p)
   255  	channel.SendBytes(b.channel, p)
   256  	return
   257  }