// Copyright 2023 Harness, 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" "fmt" "time" "github.com/harness/gitness/internal/pipeline/scheduler" "github.com/harness/gitness/internal/sse" "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/rs/zerolog/log" ) type service struct { executionStore store.ExecutionStore sseStreamer sse.Streamer repoStore store.RepoStore scheduler scheduler.Scheduler stageStore store.StageStore stepStore store.StepStore } // Canceler cancels a build. type Canceler interface { // Cancel cancels the provided execution. Cancel(ctx context.Context, repo *types.Repository, execution *types.Execution) error } // New returns a cancellation service that encapsulates // all cancellation operations. func New( executionStore store.ExecutionStore, sseStreamer sse.Streamer, repoStore store.RepoStore, scheduler scheduler.Scheduler, stageStore store.StageStore, stepStore store.StepStore, ) Canceler { return &service{ executionStore: executionStore, sseStreamer: sseStreamer, repoStore: repoStore, scheduler: scheduler, stageStore: stageStore, stepStore: stepStore, } } //nolint:gocognit // refactor if needed. func (s *service) Cancel(ctx context.Context, repo *types.Repository, execution *types.Execution) error { log := log.With(). Int64("execution.id", execution.ID). Str("execution.status", string(execution.Status)). Str("execution.Ref", execution.Ref). Logger() // do not cancel the build if the build status is // complete. only cancel the build if the status is // running or pending. if execution.Status != enum.CIStatusPending && execution.Status != enum.CIStatusRunning { 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. now := time.Now().UnixMilli() execution.Status = enum.CIStatusKilled execution.Finished = now if execution.Started == 0 { execution.Started = now } err := s.executionStore.Update(ctx, execution) if err != nil { return fmt.Errorf("could not update execution status to canceled: %w", err) } stages, err := s.stageStore.ListWithSteps(ctx, execution.ID) if err != nil { return fmt.Errorf("could not list stages with steps: %w", err) } // update the status of all steps to indicate they // were killed or skipped. for _, stage := range stages { if stage.Status.IsDone() { continue } if stage.Started != 0 { stage.Status = enum.CIStatusKilled } else { stage.Status = enum.CIStatusSkipped stage.Started = now } stage.Stopped = now err := s.stageStore.Update(ctx, stage) if err != nil { log.Debug().Err(err). Int64("stage.number", stage.Number). Msg("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.Status.IsDone() { continue } if step.Started != 0 { step.Status = enum.CIStatusKilled } else { step.Status = enum.CIStatusSkipped step.Started = now } step.Stopped = now step.ExitCode = 130 err := s.stepStore.Update(ctx, step) if err != nil { log.Debug().Err(err). Int64("stage.number", stage.Number). Int64("step.number", step.Number). Msg("canceler: cannot update step status") } } } execution.Stages = stages log.Info().Msg("canceler: successfully cancelled build") // trigger a SSE to notify subscribers that // the execution was cancelled. err = s.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeExecutionCanceled, execution) if err != nil { log.Debug().Err(err).Msg("canceler: failed to publish server-sent event") } return nil }