270 lines
6.7 KiB
Go
270 lines
6.7 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"
|
|
"encoding/json"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"github.com/drone/drone/core"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var noContext = context.Background()
|
|
|
|
type service struct {
|
|
builds core.BuildStore
|
|
events core.Pubsub
|
|
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,
|
|
events core.Pubsub,
|
|
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,
|
|
events: events,
|
|
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 {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
debug.PrintStack()
|
|
}
|
|
}()
|
|
|
|
// switch {
|
|
// case repo.CancelPulls && build.Event == core.EventPullRequest:
|
|
// case repo.CancelPush && build.Event == core.EventPush:
|
|
// default:
|
|
// return nil
|
|
// }
|
|
|
|
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, item.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("canceler: 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("canceler: 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("canceler: cannot set status")
|
|
}
|
|
}
|
|
|
|
stages, err := s.stages.ListSteps(ctx, build.ID)
|
|
if err != nil {
|
|
logger.WithError(err).
|
|
Debugln("canceler: 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("canceler: 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("canceler: cannot update step status")
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.WithError(err).
|
|
Debugln("canceler: successfully cancelled build")
|
|
|
|
build.Stages = stages
|
|
|
|
// trigger a pubsub event to notify subscribers that
|
|
// the build was cancelled. Specifically, this should
|
|
// live update the user interface.
|
|
repoCopy := new(core.Repository)
|
|
*repoCopy = *repo
|
|
repoCopy.Build = build
|
|
repoCopy.Build.Stages = stages
|
|
data, _ := json.Marshal(repoCopy)
|
|
err = s.events.Publish(noContext, &core.Message{
|
|
Repository: repo.Slug,
|
|
Visibility: repo.Visibility,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
logger.WithError(err).
|
|
Warnln("canceler: cannot publish cancel event")
|
|
}
|
|
|
|
// 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
|
|
}
|