diff --git a/internal/services/gateway/api/run.go b/internal/services/gateway/api/run.go index f9a67f7..eed5b23 100644 --- a/internal/services/gateway/api/run.go +++ b/internal/services/gateway/api/run.go @@ -53,6 +53,7 @@ type RunResponse struct { Annotations map[string]string `json:"annotations"` Phase rstypes.RunPhase `json:"phase"` Result rstypes.RunResult `json:"result"` + SetupErrors []string `json:"setup_errors"` Tasks map[string]*RunResponseTask `json:"tasks"` TasksWaitingApproval []string `json:"tasks_waiting_approval"` @@ -121,6 +122,7 @@ func createRunResponse(r *rstypes.Run, rc *rstypes.RunConfig) *RunResponse { Annotations: r.Annotations, Phase: r.Phase, Result: r.Result, + SetupErrors: rc.SetupErrors, Tasks: make(map[string]*RunResponseTask), TasksWaitingApproval: r.TasksWaitingApproval(), diff --git a/internal/services/gateway/webhook.go b/internal/services/gateway/webhook.go index 4c20590..dedd2e5 100644 --- a/internal/services/gateway/webhook.go +++ b/internal/services/gateway/webhook.go @@ -28,6 +28,7 @@ import ( csapi "github.com/sorintlab/agola/internal/services/configstore/api" "github.com/sorintlab/agola/internal/services/gateway/common" rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api" + rstypes "github.com/sorintlab/agola/internal/services/runservice/types" "github.com/sorintlab/agola/internal/services/types" "github.com/sorintlab/agola/internal/util" @@ -339,9 +340,29 @@ func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) { } func (h *webhooksHandler) createRuns(ctx context.Context, configData []byte, group string, annotations, staticEnv, variables map[string]string, webhookData *types.WebhookData) error { + setupErrors := []string{} + config, err := config.ParseConfig([]byte(configData)) if err != nil { - return errors.Wrapf(err, "failed to parse config") + log.Errorf("failed to parse config: %+v", err) + + // create a run (per config file) with a generic error since we cannot parse + // it and know how many pipelines are defined + setupErrors = append(setupErrors, err.Error()) + createRunReq := &rsapi.RunCreateRequest{ + RunConfigTasks: nil, + Group: group, + SetupErrors: setupErrors, + Name: rstypes.RunGenericSetupErrorName, + StaticEnvironment: staticEnv, + Annotations: annotations, + } + + if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil { + log.Errorf("failed to create run: %+v", err) + return err + } + return nil } //h.log.Debugf("config: %v", util.Dump(config)) @@ -354,12 +375,14 @@ func (h *webhooksHandler) createRuns(ctx context.Context, configData []byte, gro createRunReq := &rsapi.RunCreateRequest{ RunConfigTasks: rcts, Group: group, + SetupErrors: setupErrors, Name: pipeline.Name, StaticEnvironment: staticEnv, Annotations: annotations, } if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil { + log.Errorf("failed to create run: %+v", err) return err } } diff --git a/internal/services/runservice/scheduler/api/api.go b/internal/services/runservice/scheduler/api/api.go index faed497..1766ed7 100644 --- a/internal/services/runservice/scheduler/api/api.go +++ b/internal/services/runservice/scheduler/api/api.go @@ -484,6 +484,7 @@ type RunCreateRequest struct { RunConfigTasks map[string]*types.RunConfigTask `json:"run_config_tasks"` Name string `json:"name"` Group string `json:"group"` + SetupErrors []string `json:"setup_errors"` StaticEnvironment map[string]string `json:"static_environment"` // existing run fields @@ -524,6 +525,7 @@ func (h *RunCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { RunConfigTasks: req.RunConfigTasks, Name: req.Name, Group: req.Group, + SetupErrors: req.SetupErrors, StaticEnvironment: req.StaticEnvironment, RunID: req.RunID, diff --git a/internal/services/runservice/scheduler/command/command.go b/internal/services/runservice/scheduler/command/command.go index d3402ea..e9d054d 100644 --- a/internal/services/runservice/scheduler/command/command.go +++ b/internal/services/runservice/scheduler/command/command.go @@ -120,6 +120,7 @@ type RunCreateRequest struct { RunConfigTasks map[string]*types.RunConfigTask Name string Group string + SetupErrors []string StaticEnvironment map[string]string // existing run fields @@ -155,6 +156,7 @@ func (s *CommandHandler) CreateRun(ctx context.Context, req *RunCreateRequest) ( func (s *CommandHandler) newRun(ctx context.Context, req *RunCreateRequest) (*types.RunBundle, error) { rcts := req.RunConfigTasks + setupErrors := req.SetupErrors if req.Group == "" { return nil, util.NewErrBadRequest(errors.Errorf("run group is empty")) @@ -162,6 +164,9 @@ func (s *CommandHandler) newRun(ctx context.Context, req *RunCreateRequest) (*ty if !path.IsAbs(req.Group) { return nil, util.NewErrBadRequest(errors.Errorf("run group %q must be an absolute path", req.Group)) } + if req.RunConfigTasks == nil && len(setupErrors) == 0 { + return nil, util.NewErrBadRequest(errors.Errorf("empty run config tasks and setup errors")) + } // generate a new run sequence that will be the same for the run and runconfig seq, err := sequence.IncSequence(ctx, s.e, common.EtcdRunSequenceKey) @@ -171,18 +176,23 @@ func (s *CommandHandler) newRun(ctx context.Context, req *RunCreateRequest) (*ty id := seq.String() if err := runconfig.CheckRunConfigTasks(rcts); err != nil { - return nil, util.NewErrBadRequest(err) + s.log.Errorf("check run config tasks failed: %+v", err) + setupErrors = append(setupErrors, err.Error()) } // generate tasks levels - if err := runconfig.GenTasksLevels(rcts); err != nil { - return nil, util.NewErrBadRequest(err) + if len(setupErrors) == 0 { + if err := runconfig.GenTasksLevels(rcts); err != nil { + s.log.Errorf("gen tasks leveles failed: %+v", err) + setupErrors = append(setupErrors, err.Error()) + } } rc := &types.RunConfig{ ID: id, Name: req.Name, Group: req.Group, + SetupErrors: setupErrors, Tasks: rcts, StaticEnvironment: req.StaticEnvironment, Environment: req.Environment, @@ -374,6 +384,11 @@ func (s *CommandHandler) genRun(ctx context.Context, rc *types.RunConfig) *types EnqueueTime: util.TimePtr(time.Now()), } + if len(rc.SetupErrors) > 0 { + r.Phase = types.RunPhaseSetupError + return r + } + for _, rct := range rc.Tasks { rt := s.genRunTask(ctx, rct) r.RunTasks[rt.ID] = rt diff --git a/internal/services/runservice/types/types.go b/internal/services/runservice/types/types.go index 1be0578..705ec04 100644 --- a/internal/services/runservice/types/types.go +++ b/internal/services/runservice/types/types.go @@ -23,6 +23,10 @@ import ( "github.com/sorintlab/agola/internal/util" ) +const ( + RunGenericSetupErrorName = "Setup Error" +) + type SortOrder int const ( @@ -43,10 +47,11 @@ type RunCounter struct { type RunPhase string const ( - RunPhaseQueued RunPhase = "queued" - RunPhaseCancelled RunPhase = "cancelled" - RunPhaseRunning RunPhase = "running" - RunPhaseFinished RunPhase = "finished" + RunPhaseSetupError RunPhase = "setuperror" + RunPhaseQueued RunPhase = "queued" + RunPhaseCancelled RunPhase = "cancelled" + RunPhaseRunning RunPhase = "running" + RunPhaseFinished RunPhase = "finished" ) type RunResult string @@ -59,7 +64,7 @@ const ( ) func (s RunPhase) IsFinished() bool { - return s == RunPhaseCancelled || s == RunPhaseFinished + return s == RunPhaseSetupError || s == RunPhaseCancelled || s == RunPhaseFinished } func (s RunResult) IsSet() bool { @@ -137,6 +142,9 @@ func (r *Run) TasksWaitingApproval() []string { // CanRestartFromScratch reports if the run can be restarted from scratch func (r *Run) CanRestartFromScratch() (bool, string) { + if r.Phase == RunPhaseSetupError { + return false, fmt.Sprintf("run has setup errors") + } // can restart only if the run phase is finished or cancelled if !r.Phase.IsFinished() { return false, fmt.Sprintf("run is not finished, phase: %q", r.Phase) @@ -146,6 +154,9 @@ func (r *Run) CanRestartFromScratch() (bool, string) { // CanRestartFromFailedTasks reports if the run can be restarted from failed tasks func (r *Run) CanRestartFromFailedTasks() (bool, string) { + if r.Phase == RunPhaseSetupError { + return false, fmt.Sprintf("run has setup errors") + } // can restart only if the run phase is finished or cancelled if !r.Phase.IsFinished() { return false, fmt.Sprintf("run is not finished, phase: %q", r.Phase) @@ -267,6 +278,9 @@ type RunConfig struct { // also needed to fetch them when they aren't indexed in the readdb. Group string `json:"group,omitempty"` + // A list of setup errors when the run is in phase setuperror + SetupErrors []string `json:"setup_errors,omitempty"` + // Annotations contain custom run properties // Note: Annotations are currently both saved in a Run and in RunConfig to // easily return them without loading RunConfig from the lts