harness-drone/service/canceler/canceler.go
2019-10-01 22:51:23 -07:00

232 lines
5.8 KiB
Go

// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package canceler
import (
"context"
"time"
"github.com/drone/drone/core"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
)
type service struct {
builds core.BuildStore
repos core.RepositoryStore
scheduler core.Scheduler
stages core.StageStore
status core.StatusService
steps core.StepStore
users core.UserStore
webhooks core.WebhookSender
}
// New returns a new cancellation service that encapsulates
// all cancellation operations.
func New(
builds core.BuildStore,
repos core.RepositoryStore,
scheduler core.Scheduler,
stages core.StageStore,
status core.StatusService,
steps core.StepStore,
users core.UserStore,
webhooks core.WebhookSender,
) core.Canceler {
return &service{
builds: builds,
repos: repos,
scheduler: scheduler,
stages: stages,
status: status,
steps: steps,
users: users,
webhooks: webhooks,
}
}
// Cancel cancels a build.
func (s *service) Cancel(ctx context.Context, repo *core.Repository, build *core.Build) error {
return s.cancel(ctx, repo, build, core.StatusKilled)
}
// CancelPending cancels all pending builds of the same event
// and reference with lower build numbers.
func (s *service) CancelPending(ctx context.Context, repo *core.Repository, build *core.Build) error {
switch build.Event {
// on the push and pull request builds can be automatically
// cancelled by the system.
case core.EventPush, core.EventPullRequest:
default:
return nil
}
// get a list of all incomplete builds from the database
// for all repositories. this will need to be filtered.
incomplete, err := s.repos.ListIncomplete(ctx)
if err != nil {
return err
}
var result error
for _, item := range incomplete {
// ignore incomplete items in the list that do
// not match the repository or build, are already
// running, or are newer than the current build.
if !match(build, item) {
continue
}
err := s.cancel(ctx, repo, build, core.StatusSkipped)
if err != nil {
result = multierror.Append(result, err)
}
}
return result
}
func (s *service) cancel(ctx context.Context, repo *core.Repository, build *core.Build, status string) error {
logger := logrus.WithFields(
logrus.Fields{
"repo": repo.Slug,
"ref": build.Ref,
"build": build.Number,
"event": build.Event,
"status": build.Status,
},
)
// do not cancel the build if the build status is
// complete. only cancel the build if the status is
// running or pending.
switch build.Status {
case core.StatusPending, core.StatusRunning:
default:
return nil
}
// update the build status to killed. if the update fails
// due to an optimistic lock error it means the build has
// already started, and should now be ignored.
build.Status = status
build.Finished = time.Now().Unix()
if build.Started == 0 {
build.Started = time.Now().Unix()
}
err := s.builds.Update(ctx, build)
if err != nil {
logger.WithError(err).
Warnln("api: cannot update build status to cancelled")
return err
}
// notify the scheduler to cancel the build. this will
// instruct runners subscribing to the scheduler to
// cancel execution.
err = s.scheduler.Cancel(ctx, build.ID)
if err != nil {
logger.WithError(err).
Warnln("api: cannot signal cancelled build is complete")
}
// update the commit status in the remote source
// control management system.
user, err := s.users.Find(ctx, repo.UserID)
if err == nil {
err := s.status.Send(ctx, user, &core.StatusInput{
Repo: repo,
Build: build,
})
if err != nil {
logger.WithError(err).
Debugln("api: cannot set status")
}
}
stages, err := s.stages.ListSteps(ctx, build.ID)
if err != nil {
logger.WithError(err).
Debugln("api: cannot list build stages")
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, stage := range stages {
if stage.IsDone() {
continue
}
if stage.Started != 0 {
stage.Status = core.StatusKilled
} else {
stage.Status = core.StatusSkipped
stage.Started = time.Now().Unix()
}
stage.Stopped = time.Now().Unix()
err := s.stages.Update(ctx, stage)
if err != nil {
logger.WithError(err).
WithField("stage", stage.Number).
Debugln("api: cannot update stage status")
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, step := range stage.Steps {
if step.IsDone() {
continue
}
if step.Started != 0 {
step.Status = core.StatusKilled
} else {
step.Status = core.StatusSkipped
step.Started = time.Now().Unix()
}
step.Stopped = time.Now().Unix()
step.ExitCode = 130
err := s.steps.Update(ctx, step)
if err != nil {
logger.WithError(err).
WithField("stage", stage.Number).
WithField("step", step.Number).
Debugln("api: cannot update step status")
}
}
}
logger.WithError(err).
Debugln("api: successfully cancelled build")
build.Stages = stages
// trigger a webhook to notify subscribing systems that
// the build was cancelled.
payload := &core.WebhookData{
Event: core.WebhookEventBuild,
Action: core.WebhookActionUpdated,
Repo: repo,
Build: build,
}
err = s.webhooks.Send(ctx, payload)
if err != nil {
logger.WithError(err).
Warnln("manager: cannot send global webhook")
}
return nil
}