mirror of https://github.com/harness/drone.git
526 lines
15 KiB
Go
526 lines
15 KiB
Go
// 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 manager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/bootstrap"
|
|
events "github.com/harness/gitness/app/events/pipeline"
|
|
"github.com/harness/gitness/app/jwt"
|
|
"github.com/harness/gitness/app/pipeline/converter"
|
|
"github.com/harness/gitness/app/pipeline/file"
|
|
"github.com/harness/gitness/app/pipeline/scheduler"
|
|
"github.com/harness/gitness/app/services/publicaccess"
|
|
"github.com/harness/gitness/app/sse"
|
|
"github.com/harness/gitness/app/store"
|
|
urlprovider "github.com/harness/gitness/app/url"
|
|
"github.com/harness/gitness/livelog"
|
|
gitness_store "github.com/harness/gitness/store"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
// pipelineJWTLifetime specifies the max lifetime of an ephemeral pipeline jwt token.
|
|
pipelineJWTLifetime = 72 * time.Hour
|
|
// pipelineJWTRole specifies the role of an ephemeral pipeline jwt token.
|
|
pipelineJWTRole = enum.MembershipRoleContributor
|
|
)
|
|
|
|
var noContext = context.Background()
|
|
|
|
var _ ExecutionManager = (*Manager)(nil)
|
|
|
|
type (
|
|
// Request provides filters when requesting a pending
|
|
// build from the queue. This allows an agent, for example,
|
|
// to request a build that matches its architecture and kernel.
|
|
Request struct {
|
|
Kind string `json:"kind"`
|
|
Type string `json:"type"`
|
|
OS string `json:"os"`
|
|
Arch string `json:"arch"`
|
|
Variant string `json:"variant"`
|
|
Kernel string `json:"kernel"`
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
}
|
|
|
|
// Config represents a pipeline config file.
|
|
Config struct {
|
|
Data string `json:"data"`
|
|
Kind string `json:"kind"`
|
|
}
|
|
|
|
// Netrc contains login and initialization information used
|
|
// by an automated login process.
|
|
Netrc struct {
|
|
Machine string `json:"machine"`
|
|
Login string `json:"login"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// ExecutionContext represents the minimum amount of information
|
|
// required by the runner to execute a build.
|
|
ExecutionContext struct {
|
|
Repo *types.Repository `json:"repository"`
|
|
RepoIsPublic bool `json:"repository_is_public,omitempty"`
|
|
Execution *types.Execution `json:"build"`
|
|
Stage *types.Stage `json:"stage"`
|
|
Secrets []*types.Secret `json:"secrets"`
|
|
Config *file.File `json:"config"`
|
|
Netrc *Netrc `json:"netrc"`
|
|
}
|
|
|
|
// ExecutionManager encapsulates complex build operations and provides
|
|
// a simplified interface for build runners.
|
|
ExecutionManager interface {
|
|
// Request requests the next available build stage for execution.
|
|
Request(ctx context.Context, args *Request) (*types.Stage, error)
|
|
|
|
// Watch watches for build cancellation requests.
|
|
Watch(ctx context.Context, executionID int64) (bool, error)
|
|
|
|
// Accept accepts the build stage for execution.
|
|
Accept(ctx context.Context, stage int64, machine string) (*types.Stage, error)
|
|
|
|
// Write writes a line to the build logs.
|
|
Write(ctx context.Context, step int64, line *livelog.Line) error
|
|
|
|
// Details returns details about stage.
|
|
Details(ctx context.Context, stageID int64) (*ExecutionContext, error)
|
|
|
|
// UploadLogs uploads the full logs.
|
|
UploadLogs(ctx context.Context, step int64, r io.Reader) error
|
|
|
|
// BeforeStep signals the build step is about to start.
|
|
BeforeStep(ctx context.Context, step *types.Step) error
|
|
|
|
// AfterStep signals the build step is complete.
|
|
AfterStep(ctx context.Context, step *types.Step) error
|
|
|
|
// BeforeStage signals the build stage is about to start.
|
|
BeforeStage(ctx context.Context, stage *types.Stage) error
|
|
|
|
// AfterStage signals the build stage is complete.
|
|
AfterStage(ctx context.Context, stage *types.Stage) error
|
|
}
|
|
)
|
|
|
|
// Manager provides a simplified interface to the build runner so that it
|
|
// can more easily interact with the server.
|
|
type Manager struct {
|
|
Executions store.ExecutionStore
|
|
Config *types.Config
|
|
FileService file.Service
|
|
ConverterService converter.Service
|
|
Pipelines store.PipelineStore
|
|
urlProvider urlprovider.Provider
|
|
Checks store.CheckStore
|
|
// Converter store.ConvertService
|
|
SSEStreamer sse.Streamer
|
|
// Globals store.GlobalSecretStore
|
|
Logs store.LogStore
|
|
Logz livelog.LogStream
|
|
// Netrcs store.NetrcService
|
|
Repos store.RepoStore
|
|
Scheduler scheduler.Scheduler
|
|
Secrets store.SecretStore
|
|
// Status store.StatusService
|
|
Stages store.StageStore
|
|
Steps store.StepStore
|
|
// System *store.System
|
|
Users store.PrincipalStore
|
|
// Webhook store.WebhookSender
|
|
|
|
publicAccess publicaccess.Service
|
|
// events reporter
|
|
reporter events.Reporter
|
|
}
|
|
|
|
func New(
|
|
config *types.Config,
|
|
executionStore store.ExecutionStore,
|
|
pipelineStore store.PipelineStore,
|
|
urlProvider urlprovider.Provider,
|
|
sseStreamer sse.Streamer,
|
|
fileService file.Service,
|
|
converterService converter.Service,
|
|
logStore store.LogStore,
|
|
logStream livelog.LogStream,
|
|
checkStore store.CheckStore,
|
|
repoStore store.RepoStore,
|
|
scheduler scheduler.Scheduler,
|
|
secretStore store.SecretStore,
|
|
stageStore store.StageStore,
|
|
stepStore store.StepStore,
|
|
userStore store.PrincipalStore,
|
|
publicAccess publicaccess.Service,
|
|
reporter events.Reporter,
|
|
) *Manager {
|
|
return &Manager{
|
|
Config: config,
|
|
Executions: executionStore,
|
|
Pipelines: pipelineStore,
|
|
urlProvider: urlProvider,
|
|
SSEStreamer: sseStreamer,
|
|
FileService: fileService,
|
|
ConverterService: converterService,
|
|
Logs: logStore,
|
|
Logz: logStream,
|
|
Checks: checkStore,
|
|
Repos: repoStore,
|
|
Scheduler: scheduler,
|
|
Secrets: secretStore,
|
|
Stages: stageStore,
|
|
Steps: stepStore,
|
|
Users: userStore,
|
|
publicAccess: publicAccess,
|
|
reporter: reporter,
|
|
}
|
|
}
|
|
|
|
// Request requests the next available build stage for execution.
|
|
func (m *Manager) Request(ctx context.Context, args *Request) (*types.Stage, error) {
|
|
log := log.With().
|
|
Str("kind", args.Kind).
|
|
Str("type", args.Type).
|
|
Str("os", args.OS).
|
|
Str("arch", args.Arch).
|
|
Str("kernel", args.Kernel).
|
|
Str("variant", args.Variant).
|
|
Logger()
|
|
log.Debug().Msg("manager: request queue item")
|
|
|
|
stage, err := m.Scheduler.Request(ctx, scheduler.Filter{
|
|
Kind: args.Kind,
|
|
Type: args.Type,
|
|
OS: args.OS,
|
|
Arch: args.Arch,
|
|
Kernel: args.Kernel,
|
|
Variant: args.Variant,
|
|
Labels: args.Labels,
|
|
})
|
|
if err != nil && ctx.Err() != nil {
|
|
log.Debug().Err(err).Msg("manager: context canceled")
|
|
return nil, err
|
|
}
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: request queue item error")
|
|
return nil, err
|
|
}
|
|
return stage, nil
|
|
}
|
|
|
|
// Accept accepts the build stage for execution. It is possible for multiple
|
|
// agents to pull the same stage from the queue.
|
|
func (m *Manager) Accept(_ context.Context, id int64, machine string) (*types.Stage, error) {
|
|
log := log.With().
|
|
Int64("stage-id", id).
|
|
Str("machine", machine).
|
|
Logger()
|
|
log.Debug().Msg("manager: accept stage")
|
|
|
|
stage, err := m.Stages.Find(noContext, id)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot find stage")
|
|
return nil, err
|
|
}
|
|
if stage.Machine != "" {
|
|
log.Debug().Msg("manager: stage already assigned. abort.")
|
|
return nil, fmt.Errorf("stage already assigned, abort")
|
|
}
|
|
|
|
stage.Machine = machine
|
|
stage.Status = enum.CIStatusPending
|
|
err = m.Stages.Update(noContext, stage)
|
|
switch {
|
|
case errors.Is(err, gitness_store.ErrVersionConflict):
|
|
log.Debug().Err(err).Msg("manager: stage processed by another agent")
|
|
case err != nil:
|
|
log.Debug().Err(err).Msg("manager: cannot update stage")
|
|
default:
|
|
log.Info().Msg("manager: stage accepted")
|
|
}
|
|
return stage, err
|
|
}
|
|
|
|
// Write writes a line to the build logs.
|
|
func (m *Manager) Write(ctx context.Context, step int64, line *livelog.Line) error {
|
|
err := m.Logz.Write(ctx, step, line)
|
|
if err != nil {
|
|
log.Warn().Int64("step-id", step).Err(err).Msg("manager: cannot write to log stream")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UploadLogs uploads the full logs.
|
|
func (m *Manager) UploadLogs(ctx context.Context, step int64, r io.Reader) error {
|
|
err := m.Logs.Create(ctx, step, r)
|
|
if err != nil {
|
|
log.Error().Err(err).Int64("step-id", step).Msg("manager: cannot upload complete logs")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Details provides details about the stage.
|
|
func (m *Manager) Details(ctx context.Context, stageID int64) (*ExecutionContext, error) {
|
|
log := log.With().
|
|
Int64("stage-id", stageID).
|
|
Logger()
|
|
log.Debug().Msg("manager: fetching stage details")
|
|
|
|
stage, err := m.Stages.Find(noContext, stageID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot find stage")
|
|
return nil, err
|
|
}
|
|
execution, err := m.Executions.Find(noContext, stage.ExecutionID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot find build")
|
|
return nil, err
|
|
}
|
|
pipeline, err := m.Pipelines.Find(noContext, execution.PipelineID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot find pipeline")
|
|
return nil, err
|
|
}
|
|
repo, err := m.Repos.Find(noContext, execution.RepoID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot find repo")
|
|
return nil, err
|
|
}
|
|
|
|
// Backfill clone URL
|
|
repo.GitURL = m.urlProvider.GenerateContainerGITCloneURL(ctx, repo.Path)
|
|
|
|
stages, err := m.Stages.List(noContext, stage.ExecutionID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot list stages")
|
|
return nil, err
|
|
}
|
|
execution.Stages = stages
|
|
log = log.With().
|
|
Int64("build", execution.Number).
|
|
Str("repo", repo.GetGitUID()).
|
|
Logger()
|
|
|
|
// TODO: Currently we fetch all the secrets from the same space.
|
|
// This logic can be updated when needed.
|
|
secrets, err := m.Secrets.ListAll(noContext, repo.ParentID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot list secrets")
|
|
return nil, err
|
|
}
|
|
|
|
// Fetch contents of YAML from the execution ref at the pipeline config path.
|
|
file, err := m.FileService.Get(noContext, repo, pipeline.ConfigPath, execution.After)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot fetch file")
|
|
return nil, err
|
|
}
|
|
|
|
// Get public access settings of the repo
|
|
repoIsPublic, err := m.publicAccess.Get(noContext, enum.PublicResourceTypeRepo, repo.Path)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot check if repo is public")
|
|
return nil, err
|
|
}
|
|
|
|
// Convert file contents in case templates are being used.
|
|
args := &converter.ConvertArgs{
|
|
Repo: repo,
|
|
Pipeline: pipeline,
|
|
Execution: execution,
|
|
File: file,
|
|
RepoIsPublic: repoIsPublic,
|
|
}
|
|
file, err = m.ConverterService.Convert(noContext, args)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot convert template contents")
|
|
return nil, err
|
|
}
|
|
|
|
netrc, err := m.createNetrc(repo)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: failed to create netrc")
|
|
return nil, err
|
|
}
|
|
|
|
return &ExecutionContext{
|
|
Repo: repo,
|
|
RepoIsPublic: repoIsPublic,
|
|
Execution: execution,
|
|
Stage: stage,
|
|
Secrets: secrets,
|
|
Config: file,
|
|
Netrc: netrc,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) createNetrc(repo *types.Repository) (*Netrc, error) {
|
|
pipelinePrincipal := bootstrap.NewPipelineServiceSession().Principal
|
|
jwt, err := jwt.GenerateWithMembership(
|
|
pipelinePrincipal.ID,
|
|
repo.ParentID,
|
|
pipelineJWTRole,
|
|
pipelineJWTLifetime,
|
|
pipelinePrincipal.Salt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create jwt: %w", err)
|
|
}
|
|
|
|
cloneURL, err := url.Parse(repo.GitURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse clone url '%s': %w", cloneURL, err)
|
|
}
|
|
|
|
return &Netrc{
|
|
Machine: cloneURL.Hostname(),
|
|
Login: pipelinePrincipal.UID,
|
|
Password: jwt,
|
|
}, nil
|
|
}
|
|
|
|
// Before signals the build step is about to start.
|
|
func (m *Manager) BeforeStep(_ context.Context, step *types.Step) error {
|
|
log := log.With().
|
|
Str("step.status", string(step.Status)).
|
|
Str("step.name", step.Name).
|
|
Int64("step.id", step.ID).
|
|
Logger()
|
|
|
|
log.Debug().Msg("manager: updating step status")
|
|
|
|
err := m.Logz.Create(noContext, step.ID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("manager: cannot create log stream")
|
|
return err
|
|
}
|
|
updater := &updater{
|
|
Executions: m.Executions,
|
|
SSEStreamer: m.SSEStreamer,
|
|
Repos: m.Repos,
|
|
Steps: m.Steps,
|
|
Stages: m.Stages,
|
|
}
|
|
return updater.do(noContext, step)
|
|
}
|
|
|
|
// After signals the build step is complete.
|
|
func (m *Manager) AfterStep(_ context.Context, step *types.Step) error {
|
|
log := log.With().
|
|
Str("step.status", string(step.Status)).
|
|
Str("step.name", step.Name).
|
|
Int64("step.id", step.ID).
|
|
Logger()
|
|
log.Debug().Msg("manager: updating step status")
|
|
|
|
var retErr error
|
|
updater := &updater{
|
|
Executions: m.Executions,
|
|
SSEStreamer: m.SSEStreamer,
|
|
Repos: m.Repos,
|
|
Steps: m.Steps,
|
|
Stages: m.Stages,
|
|
}
|
|
|
|
if err := updater.do(noContext, step); err != nil {
|
|
retErr = err
|
|
log.Warn().Err(err).Msg("manager: cannot update step")
|
|
}
|
|
|
|
if err := m.Logz.Delete(noContext, step.ID); err != nil && !errors.Is(err, livelog.ErrStreamNotFound) {
|
|
log.Warn().Err(err).Msg("manager: cannot teardown log stream")
|
|
}
|
|
return retErr
|
|
}
|
|
|
|
// BeforeAll signals the build stage is about to start.
|
|
func (m *Manager) BeforeStage(_ context.Context, stage *types.Stage) error {
|
|
s := &setup{
|
|
Executions: m.Executions,
|
|
Checks: m.Checks,
|
|
Pipelines: m.Pipelines,
|
|
SSEStreamer: m.SSEStreamer,
|
|
Repos: m.Repos,
|
|
Steps: m.Steps,
|
|
Stages: m.Stages,
|
|
Users: m.Users,
|
|
}
|
|
|
|
return s.do(noContext, stage)
|
|
}
|
|
|
|
// AfterAll signals the build stage is complete.
|
|
func (m *Manager) AfterStage(_ context.Context, stage *types.Stage) error {
|
|
t := &teardown{
|
|
Executions: m.Executions,
|
|
Pipelines: m.Pipelines,
|
|
Checks: m.Checks,
|
|
SSEStreamer: m.SSEStreamer,
|
|
Logs: m.Logz,
|
|
Repos: m.Repos,
|
|
Scheduler: m.Scheduler,
|
|
Steps: m.Steps,
|
|
Stages: m.Stages,
|
|
Reporter: m.reporter,
|
|
}
|
|
return t.do(noContext, stage)
|
|
}
|
|
|
|
// Watch watches for build cancellation requests.
|
|
func (m *Manager) Watch(ctx context.Context, executionID int64) (bool, error) {
|
|
ok, err := m.Scheduler.Cancelled(ctx, executionID)
|
|
// we expect a context cancel error here which
|
|
// indicates a polling timeout. The subscribing
|
|
// client should look for the context cancel error
|
|
// and resume polling.
|
|
if err != nil {
|
|
return ok, err
|
|
}
|
|
|
|
// // TODO: we should be able to return
|
|
// // immediately if Cancelled returns true. This requires
|
|
// // some more testing but would avoid the extra database
|
|
// // call.
|
|
// if ok {
|
|
// return ok, err
|
|
// }
|
|
|
|
// if no error is returned we should check
|
|
// the database to see if the build is complete. If
|
|
// complete, return true.
|
|
execution, err := m.Executions.Find(ctx, executionID)
|
|
if err != nil {
|
|
log := log.With().
|
|
Int64("execution.id", executionID).
|
|
Logger()
|
|
log.Warn().Msg("manager: cannot find build")
|
|
return ok, fmt.Errorf("could not find build for cancellation: %w", err)
|
|
}
|
|
return execution.Status.IsDone(), nil
|
|
}
|