func (u *updater) SetBuild(c context.Context, r *Task) error { err := store.UpdateBuild(c, r.Build) if err != nil { return err } err = remote.FromContext(c).Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number)) if err != nil { // log err } msg, err := json.Marshal(&payload{r.Build, r.Jobs}) if err != nil { return err } u.bus.send(&Event{ Name: r.Repo.FullName, Msg: msg, }) return nil }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) return } // if the remote has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the job. if refresher, ok := remote_.(remote.Refresher); ok { ok, _ := refresher.Refresh(user) if ok { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database raw, sec, err := remote_.Script(user, repo, build) if err != nil { log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } key, _ := store.GetKey(c, repo) netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.AbortWithStatus(409) return } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so taht we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) engine_ := context.Engine(c) go engine_.Schedule(c.Copy(), &engine.Task{ User: user, Repo: repo, Build: build, BuildPrev: last, Jobs: jobs, Keys: key, Netrc: netrc, Config: string(raw), Secret: string(sec), System: &model.System{ Link: httputil.GetURL(c.Request), Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), }, }) }
// Update handles build updates from the agent and persists to the database. func Update(c *gin.Context) { work := &queue.Work{} if err := c.BindJSON(work); err != nil { logrus.Errorf("Invalid input. %s", err) return } // TODO(bradrydzewski) it is really annoying that we have to do this lookup // and I'd prefer not to. The reason we do this is because the Build and Job // have fields that aren't serialized to json and would be reset to their // empty values if we just saved what was coming in the http.Request body. build, err := store.GetBuild(c, work.Build.ID) if err != nil { c.String(404, "Unable to find build. %s", err) return } job, err := store.GetJob(c, work.Job.ID) if err != nil { c.String(404, "Unable to find job. %s", err) return } build.Started = work.Build.Started build.Finished = work.Build.Finished build.Status = work.Build.Status job.Started = work.Job.Started job.Finished = work.Job.Finished job.Status = work.Job.Status job.ExitCode = work.Job.ExitCode job.Error = work.Job.Error if build.Status == model.StatusPending { build.Started = work.Job.Started build.Status = model.StatusRunning store.UpdateBuild(c, build) } // if job.Status == model.StatusRunning { // err := stream.Create(c, stream.ToKey(job.ID)) // if err != nil { // logrus.Errorf("Unable to create stream. %s", err) // } // } ok, err := store.UpdateBuildJob(c, build, job) if err != nil { c.String(500, "Unable to update job. %s", err) return } if ok && build.Status != model.StatusRunning { // get the user because we transfer the user form the server to agent // and back we lose the token which does not get serialized to json. user, err := store.GetUser(c, work.User.ID) if err != nil { c.String(500, "Unable to find user. %s", err) return } remote.Status(c, user, work.Repo, build, fmt.Sprintf("%s/%s/%d", work.System.Link, work.Repo.FullName, work.Build.Number)) } if build.Status == model.StatusRunning { bus.Publish(c, bus.NewEvent(bus.Started, work.Repo, build, job)) } else { bus.Publish(c, bus.NewEvent(bus.Finished, work.Repo, build, job)) } c.JSON(200, work) }
// HandleUpdate handles build updates from the agent and persists to the database. func HandleUpdate(c context.Context, message *stomp.Message) { defer func() { message.Release() if r := recover(); r != nil { err := r.(error) logrus.Errorf("Panic recover: broker update handler: %s", err) } }() work := new(model.Work) if err := message.Unmarshal(work); err != nil { logrus.Errorf("Invalid input. %s", err) return } // TODO(bradrydzewski) it is really annoying that we have to do this lookup // and I'd prefer not to. The reason we do this is because the Build and Job // have fields that aren't serialized to json and would be reset to their // empty values if we just saved what was coming in the http.Request body. build, err := store.GetBuild(c, work.Build.ID) if err != nil { logrus.Errorf("Unable to find build. %s", err) return } job, err := store.GetJob(c, work.Job.ID) if err != nil { logrus.Errorf("Unable to find job. %s", err) return } build.Started = work.Build.Started build.Finished = work.Build.Finished build.Status = work.Build.Status job.Started = work.Job.Started job.Finished = work.Job.Finished job.Status = work.Job.Status job.ExitCode = work.Job.ExitCode job.Error = work.Job.Error if build.Status == model.StatusPending { build.Started = work.Job.Started build.Status = model.StatusRunning store.UpdateBuild(c, build) } // if job.Status == model.StatusRunning { // err := stream.Create(c, stream.ToKey(job.ID)) // if err != nil { // logrus.Errorf("Unable to create stream. %s", err) // } // } ok, err := store.UpdateBuildJob(c, build, job) if err != nil { logrus.Errorf("Unable to update job. %s", err) return } if ok { // get the user because we transfer the user form the server to agent // and back we lose the token which does not get serialized to json. user, uerr := store.GetUser(c, work.User.ID) if uerr != nil { logrus.Errorf("Unable to find user. %s", err) return } remote.Status(c, user, work.Repo, build, fmt.Sprintf("%s/%s/%d", work.System.Link, work.Repo.FullName, work.Build.Number)) } client := stomp.MustFromContext(c) err = client.SendJSON("/topic/events", model.Event{ Type: func() model.EventType { // HACK we don't even really care about the event type. // so we should just simplify how events are triggered. if job.Status == model.StatusRunning { return model.Started } return model.Finished }(), Repo: *work.Repo, Build: *build, Job: *job, }, stomp.WithHeader("repo", work.Repo.FullName), stomp.WithHeader("private", strconv.FormatBool(work.Repo.IsPrivate)), ) if err != nil { logrus.Errorf("Unable to publish to /topic/events. %s", err) } if job.Status == model.StatusRunning { return } var buf bytes.Buffer var sub []byte done := make(chan bool) dest := fmt.Sprintf("/topic/logs.%d", job.ID) sub, err = client.Subscribe(dest, stomp.HandlerFunc(func(m *stomp.Message) { defer m.Release() if m.Header.GetBool("eof") { done <- true return } buf.Write(m.Body) buf.WriteByte('\n') })) if err != nil { logrus.Errorf("Unable to read logs from broker. %s", err) return } defer func() { client.Unsubscribe(sub) client.Send(dest, []byte{}, stomp.WithRetain("remove")) }() select { case <-done: case <-time.After(30 * time.Second): logrus.Errorf("Unable to read logs from broker. Timeout. %s", err) return } if err := store.WriteLog(c, job, &buf); err != nil { logrus.Errorf("Unable to write logs to store. %s", err) return } }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) fork := c.DefaultQuery("fork", "false") num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) return } // if the remote has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the job. if refresher, ok := remote_.(remote.Refresher); ok { ok, _ := refresher.Refresh(user) if ok { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database config := ToConfig(c) raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } // Fetch secrets file but don't exit on error as it's optional sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.String(409, "Cannot re-start a started build") return } // forking the build creates a duplicate of the build // and then executes. This retains prior build history. if forkit, _ := strconv.ParseBool(fork); forkit { build.ID = 0 build.Number = 0 for _, job := range jobs { job.ID = 0 job.NodeID = 0 } err := store.CreateBuild(c, build, jobs...) if err != nil { c.String(500, err.Error()) return } event := c.DefaultQuery("event", build.Event) if event == model.EventPush || event == model.EventPull || event == model.EventTag || event == model.EventDeploy { build.Event = event } build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.NodeID = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) secs, err := store.GetSecretList(c, repo) if err != nil { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } var signed bool var verified bool signature, err := jose.ParseSigned(string(sec)) if err != nil { log.Debugf("cannot parse .drone.yml.sig file. %s", err) } else if len(sec) == 0 { log.Debugf("cannot parse .drone.yml.sig file. empty file") } else { signed = true output, err := signature.Verify([]byte(repo.Hash)) if err != nil { log.Debugf("cannot verify .drone.yml.sig file. %s", err) } else if string(output) != string(raw) { log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) } else { verified = true } } log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) for _, job := range jobs { queue.Publish(c, &queue.Work{ Signed: signed, Verified: verified, User: user, Repo: repo, Build: build, BuildLast: last, Job: job, Netrc: netrc, Yaml: string(raw), Secrets: secs, System: &model.System{Link: httputil.GetURL(c.Request)}, }) } }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) fork := c.DefaultQuery("fork", "false") num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) return } // if the remote has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the job. if refresher, ok := remote_.(remote.Refresher); ok { ok, _ := refresher.Refresh(user) if ok { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database config := ToConfig(c) raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } // Fetch secrets file but don't exit on error as it's optional sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.String(409, "Cannot re-start a started build") return } // forking the build creates a duplicate of the build // and then executes. This retains prior build history. if forkit, _ := strconv.ParseBool(fork); forkit { build.ID = 0 build.Number = 0 build.Parent = num for _, job := range jobs { job.ID = 0 job.NodeID = 0 } err := store.CreateBuild(c, build, jobs...) if err != nil { c.String(500, err.Error()) return } event := c.DefaultQuery("event", build.Event) if event == model.EventPush || event == model.EventPull || event == model.EventTag || event == model.EventDeploy { build.Event = event } build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) } // Read query string parameters into buildParams, exclude reserved params var buildParams = map[string]string{} for key, val := range c.Request.URL.Query() { switch key { case "fork", "event", "deploy_to": default: // We only accept string literals, because build parameters will be // injected as environment variables buildParams[key] = val[0] } } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { for k, v := range buildParams { job.Environment[k] = v } job.Error = "" job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.NodeID = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) secs, err := store.GetMergedSecretList(c, repo) if err != nil { log.Debugf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } var signed bool var verified bool signature, err := jose.ParseSigned(string(sec)) if err != nil { log.Debugf("cannot parse .drone.yml.sig file. %s", err) } else if len(sec) == 0 { log.Debugf("cannot parse .drone.yml.sig file. empty file") } else { signed = true output, err := signature.Verify([]byte(repo.Hash)) if err != nil { log.Debugf("cannot verify .drone.yml.sig file. %s", err) } else if string(output) != string(raw) { log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) } else { verified = true } } log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) client := stomp.MustFromContext(c) client.SendJSON("/topic/events", model.Event{ Type: model.Enqueued, Repo: *repo, Build: *build, }, stomp.WithHeader("repo", repo.FullName), stomp.WithHeader("private", strconv.FormatBool(repo.IsPrivate)), ) for _, job := range jobs { broker, _ := stomp.FromContext(c) broker.SendJSON("/queue/pending", &model.Work{ Signed: signed, Verified: verified, User: user, Repo: repo, Build: build, BuildLast: last, Job: job, Netrc: netrc, Yaml: string(raw), Secrets: secs, System: &model.System{Link: httputil.GetURL(c.Request)}, }, stomp.WithHeader( "platform", yaml.ParsePlatformDefault(raw, "linux/amd64"), ), stomp.WithHeaders( yaml.ParseLabel(raw), ), ) } }