// PostHook accepts a post-commit hook and parses the payload // in order to trigger a build. The payload is specified to the // remote system (ie GitHub) and will therefore get parsed by // the appropriate remote plugin. // // GET /api/hook/:host // func PostHook(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var host = c.URLParams["host"] var token = c.URLParams["token"] var remote = remote.Lookup(host) if remote == nil { w.WriteHeader(http.StatusNotFound) return } // parse the hook payload hook, err := remote.ParseHook(r) if err != nil { log.Printf("Unable to parse hook. %s\n", err) w.WriteHeader(http.StatusBadRequest) return } // in some cases we have neither a hook nor error. An example // would be GitHub sending a ping request to the URL, in which // case we'll just exit quiely with an 'OK' shouldSkip, _ := regexp.MatchString(`\[(?i:ci *skip|skip *ci)\]`, hook.Message) if hook == nil || shouldSkip { w.WriteHeader(http.StatusOK) return } // fetch the repository from the database repo, err := datastore.GetRepoName(ctx, remote.GetHost(), hook.Owner, hook.Repo) if err != nil { w.WriteHeader(http.StatusNotFound) return } // each hook contains a token to verify the sender. If the token // is not provided or does not match, exit if len(repo.Token) == 0 || repo.Token != token { log.Printf("Rejected post commit hook for %s. Token mismatch\n", repo.Name) w.WriteHeader(http.StatusUnauthorized) return } if repo.Active == false || (repo.PostCommit == false && len(hook.PullRequest) == 0) || (repo.PullRequest == false && len(hook.PullRequest) != 0) { w.WriteHeader(http.StatusNotFound) return } // fetch the user from the database that owns this repo user, err := datastore.GetUser(ctx, repo.UserID) if err != nil { w.WriteHeader(http.StatusNotFound) return } // Request a new token and update user_token, err := remote.GetToken(user) if user_token != nil { user.Access = user_token.AccessToken user.Secret = user_token.RefreshToken user.TokenExpiry = user_token.Expiry datastore.PutUser(ctx, user) } else if err != nil { log.Printf("Unable to refresh token. %s\n", err) w.WriteHeader(http.StatusBadRequest) return } // fetch the .drone.yml file from the database yml, err := remote.GetScript(user, repo, hook) if err != nil { log.Printf("Unable to fetch .drone.yml file. %s\n", err) w.WriteHeader(http.StatusBadRequest) return } // verify the commit hooks branch matches the list of approved // branches (unless it is a pull request). Note that we don't really // care if parsing the yaml fails here. s, _ := script.ParseBuild(string(yml)) if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) { w.WriteHeader(http.StatusOK) return } commit := model.Commit{ RepoID: repo.ID, Status: model.StatusEnqueue, Sha: hook.Sha, Branch: hook.Branch, PullRequest: hook.PullRequest, Timestamp: hook.Timestamp, Message: hook.Message, Config: string(yml), } commit.SetAuthor(hook.Author) // inserts the commit into the database if err := datastore.PostCommit(ctx, &commit); err != nil { log.Printf("Unable to persist commit %s@%s. %s\n", commit.Sha, commit.Branch, err) w.WriteHeader(http.StatusBadRequest) return } owner, err := datastore.GetUser(ctx, repo.UserID) if err != nil { log.Printf("Unable to retrieve repository owner. %s.\n", err) w.WriteHeader(http.StatusBadRequest) return } // drop the items on the queue go worker.Do(ctx, &worker.Work{ User: owner, Repo: repo, Commit: &commit, Host: httputil.GetURL(r), }) w.WriteHeader(http.StatusOK) }
func (d *Docker) Do(c context.Context, r *worker.Work) { // ensure that we can recover from any panics to // avoid bringing down the entire application. defer func() { if e := recover(); e != nil { log.Printf("%s: %s", e, debug.Stack()) } }() // mark the build as Started and update the database r.Commit.Status = model.StatusStarted r.Commit.Started = time.Now().UTC().Unix() datastore.PutCommit(c, r.Commit) // notify all listeners that the build is started commitc := pubsub.Register(c, "_global") commitc.Publish(r) stdoutc := pubsub.RegisterOpts(c, r.Commit.ID, pubsub.ConsoleOpts) defer pubsub.Unregister(c, r.Commit.ID) // create a special buffer that will also // write to a websocket channel buf := pubsub.NewBuffer(stdoutc) // parse the parameters and build script. The script has already // been parsed in the hook, so we can be confident it will succeed. // that being said, we should clean this up params, err := r.Repo.ParamMap() if err != nil { log.Printf("Error parsing PARAMS for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error()) } script, err := script.ParseBuild(script.Inject(r.Commit.Config, params)) if err != nil { log.Printf("Error parsing YAML for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error()) } // TODO: handle error better? buildNumber, err := datastore.GetBuildNumber(c, r.Commit) if err != nil { log.Printf("Unable to fetch build number, Err: %s", err.Error()) } script.Env = append(script.Env, fmt.Sprintf("DRONE_BUILD_NUMBER=%d", buildNumber)) script.Env = append(script.Env, fmt.Sprintf("CI_BUILD_NUMBER=%d", buildNumber)) path := r.Repo.Host + "/" + r.Repo.Owner + "/" + r.Repo.Name repo := &repo.Repo{ Name: path, Path: r.Repo.CloneURL, Branch: r.Commit.Branch, Commit: r.Commit.Sha, PR: r.Commit.PullRequest, Private: r.Repo.Private, Dir: filepath.ToSlash(filepath.Join("/var/cache/drone/src", git.GitPath(script.Git, path))), Depth: git.GitDepth(script.Git), } // append private parameters to the environment // variable section of the .drone.yml file, if // this is trusted if params != nil && repo.IsTrusted() { for k, v := range params { script.Env = append(script.Env, k+"="+v) } } priorCommit, _ := datastore.GetCommitPrior(c, r.Commit) // send all "started" notifications if script.Notifications == nil { script.Notifications = ¬ify.Notification{} } script.Notifications.Send(&model.Request{ User: r.User, Repo: r.Repo, Commit: r.Commit, Host: r.Host, Prior: priorCommit, }) // create an instance of the Docker builder builder := build.New(d.docker) builder.Build = script builder.Repo = repo builder.Stdout = buf builder.Timeout = time.Duration(r.Repo.Timeout) * time.Second builder.Privileged = r.Repo.Privileged if repo.IsTrusted() { builder.Key = []byte(r.Repo.PrivateKey) } // run the build err = builder.Run() // update the build status based on the results // from the build runner. switch { case err != nil: r.Commit.Status = model.StatusError log.Printf("Error building %s, Err: %s", r.Commit.Sha, err) buf.WriteString(err.Error()) case builder.BuildState == nil: r.Commit.Status = model.StatusFailure case builder.BuildState.ExitCode != 0: r.Commit.Status = model.StatusFailure default: r.Commit.Status = model.StatusSuccess } // calcualte the build finished and duration details and // update the commit r.Commit.Finished = time.Now().UTC().Unix() r.Commit.Duration = (r.Commit.Finished - r.Commit.Started) datastore.PutCommit(c, r.Commit) blobstore.Put(c, filepath.Join(r.Repo.Host, r.Repo.Owner, r.Repo.Name, r.Commit.Branch, r.Commit.Sha), buf.Bytes()) // notify all listeners that the build is finished commitc.Publish(r) priorCommit, _ = datastore.GetCommitPrior(c, r.Commit) // send all "finished" notifications script.Notifications.Send(&model.Request{ User: r.User, Repo: r.Repo, Commit: r.Commit, Host: r.Host, Prior: priorCommit, }) }