From da27348a1db97bc12bff01075026bf60b0e9431c Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Tue, 9 Apr 2019 16:51:37 +0200 Subject: [PATCH] runservice: implement run setup errors Add the ability to define a run with a setuperror phase. When the run setup has errors client could submit a run with a list of setup errors. In such case the run will be created in the setuperror phase. Setup errors are currently generated by the webhook receiver and the run service when it checks the run config for possible issues. --- internal/services/gateway/api/run.go | 2 ++ internal/services/gateway/webhook.go | 25 ++++++++++++++++++- .../services/runservice/scheduler/api/api.go | 2 ++ .../runservice/scheduler/command/command.go | 21 +++++++++++++--- internal/services/runservice/types/types.go | 24 ++++++++++++++---- 5 files changed, 65 insertions(+), 9 deletions(-) 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