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.
This commit is contained in:
Simone Gotti 2019-04-09 16:51:37 +02:00
parent 671b89d391
commit da27348a1d
5 changed files with 65 additions and 9 deletions

View File

@ -53,6 +53,7 @@ type RunResponse struct {
Annotations map[string]string `json:"annotations"` Annotations map[string]string `json:"annotations"`
Phase rstypes.RunPhase `json:"phase"` Phase rstypes.RunPhase `json:"phase"`
Result rstypes.RunResult `json:"result"` Result rstypes.RunResult `json:"result"`
SetupErrors []string `json:"setup_errors"`
Tasks map[string]*RunResponseTask `json:"tasks"` Tasks map[string]*RunResponseTask `json:"tasks"`
TasksWaitingApproval []string `json:"tasks_waiting_approval"` TasksWaitingApproval []string `json:"tasks_waiting_approval"`
@ -121,6 +122,7 @@ func createRunResponse(r *rstypes.Run, rc *rstypes.RunConfig) *RunResponse {
Annotations: r.Annotations, Annotations: r.Annotations,
Phase: r.Phase, Phase: r.Phase,
Result: r.Result, Result: r.Result,
SetupErrors: rc.SetupErrors,
Tasks: make(map[string]*RunResponseTask), Tasks: make(map[string]*RunResponseTask),
TasksWaitingApproval: r.TasksWaitingApproval(), TasksWaitingApproval: r.TasksWaitingApproval(),

View File

@ -28,6 +28,7 @@ import (
csapi "github.com/sorintlab/agola/internal/services/configstore/api" csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common" "github.com/sorintlab/agola/internal/services/gateway/common"
rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api" 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/services/types"
"github.com/sorintlab/agola/internal/util" "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 { 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)) config, err := config.ParseConfig([]byte(configData))
if err != nil { 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)) //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{ createRunReq := &rsapi.RunCreateRequest{
RunConfigTasks: rcts, RunConfigTasks: rcts,
Group: group, Group: group,
SetupErrors: setupErrors,
Name: pipeline.Name, Name: pipeline.Name,
StaticEnvironment: staticEnv, StaticEnvironment: staticEnv,
Annotations: annotations, Annotations: annotations,
} }
if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil { if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil {
log.Errorf("failed to create run: %+v", err)
return err return err
} }
} }

View File

@ -484,6 +484,7 @@ type RunCreateRequest struct {
RunConfigTasks map[string]*types.RunConfigTask `json:"run_config_tasks"` RunConfigTasks map[string]*types.RunConfigTask `json:"run_config_tasks"`
Name string `json:"name"` Name string `json:"name"`
Group string `json:"group"` Group string `json:"group"`
SetupErrors []string `json:"setup_errors"`
StaticEnvironment map[string]string `json:"static_environment"` StaticEnvironment map[string]string `json:"static_environment"`
// existing run fields // existing run fields
@ -524,6 +525,7 @@ func (h *RunCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
RunConfigTasks: req.RunConfigTasks, RunConfigTasks: req.RunConfigTasks,
Name: req.Name, Name: req.Name,
Group: req.Group, Group: req.Group,
SetupErrors: req.SetupErrors,
StaticEnvironment: req.StaticEnvironment, StaticEnvironment: req.StaticEnvironment,
RunID: req.RunID, RunID: req.RunID,

View File

@ -120,6 +120,7 @@ type RunCreateRequest struct {
RunConfigTasks map[string]*types.RunConfigTask RunConfigTasks map[string]*types.RunConfigTask
Name string Name string
Group string Group string
SetupErrors []string
StaticEnvironment map[string]string StaticEnvironment map[string]string
// existing run fields // 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) { func (s *CommandHandler) newRun(ctx context.Context, req *RunCreateRequest) (*types.RunBundle, error) {
rcts := req.RunConfigTasks rcts := req.RunConfigTasks
setupErrors := req.SetupErrors
if req.Group == "" { if req.Group == "" {
return nil, util.NewErrBadRequest(errors.Errorf("run group is empty")) 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) { if !path.IsAbs(req.Group) {
return nil, util.NewErrBadRequest(errors.Errorf("run group %q must be an absolute path", 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 // generate a new run sequence that will be the same for the run and runconfig
seq, err := sequence.IncSequence(ctx, s.e, common.EtcdRunSequenceKey) 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() id := seq.String()
if err := runconfig.CheckRunConfigTasks(rcts); err != nil { 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 // generate tasks levels
if err := runconfig.GenTasksLevels(rcts); err != nil { if len(setupErrors) == 0 {
return nil, util.NewErrBadRequest(err) if err := runconfig.GenTasksLevels(rcts); err != nil {
s.log.Errorf("gen tasks leveles failed: %+v", err)
setupErrors = append(setupErrors, err.Error())
}
} }
rc := &types.RunConfig{ rc := &types.RunConfig{
ID: id, ID: id,
Name: req.Name, Name: req.Name,
Group: req.Group, Group: req.Group,
SetupErrors: setupErrors,
Tasks: rcts, Tasks: rcts,
StaticEnvironment: req.StaticEnvironment, StaticEnvironment: req.StaticEnvironment,
Environment: req.Environment, Environment: req.Environment,
@ -374,6 +384,11 @@ func (s *CommandHandler) genRun(ctx context.Context, rc *types.RunConfig) *types
EnqueueTime: util.TimePtr(time.Now()), EnqueueTime: util.TimePtr(time.Now()),
} }
if len(rc.SetupErrors) > 0 {
r.Phase = types.RunPhaseSetupError
return r
}
for _, rct := range rc.Tasks { for _, rct := range rc.Tasks {
rt := s.genRunTask(ctx, rct) rt := s.genRunTask(ctx, rct)
r.RunTasks[rt.ID] = rt r.RunTasks[rt.ID] = rt

View File

@ -23,6 +23,10 @@ import (
"github.com/sorintlab/agola/internal/util" "github.com/sorintlab/agola/internal/util"
) )
const (
RunGenericSetupErrorName = "Setup Error"
)
type SortOrder int type SortOrder int
const ( const (
@ -43,10 +47,11 @@ type RunCounter struct {
type RunPhase string type RunPhase string
const ( const (
RunPhaseQueued RunPhase = "queued" RunPhaseSetupError RunPhase = "setuperror"
RunPhaseCancelled RunPhase = "cancelled" RunPhaseQueued RunPhase = "queued"
RunPhaseRunning RunPhase = "running" RunPhaseCancelled RunPhase = "cancelled"
RunPhaseFinished RunPhase = "finished" RunPhaseRunning RunPhase = "running"
RunPhaseFinished RunPhase = "finished"
) )
type RunResult string type RunResult string
@ -59,7 +64,7 @@ const (
) )
func (s RunPhase) IsFinished() bool { func (s RunPhase) IsFinished() bool {
return s == RunPhaseCancelled || s == RunPhaseFinished return s == RunPhaseSetupError || s == RunPhaseCancelled || s == RunPhaseFinished
} }
func (s RunResult) IsSet() bool { func (s RunResult) IsSet() bool {
@ -137,6 +142,9 @@ func (r *Run) TasksWaitingApproval() []string {
// CanRestartFromScratch reports if the run can be restarted from scratch // CanRestartFromScratch reports if the run can be restarted from scratch
func (r *Run) CanRestartFromScratch() (bool, string) { 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 // can restart only if the run phase is finished or cancelled
if !r.Phase.IsFinished() { if !r.Phase.IsFinished() {
return false, fmt.Sprintf("run is not finished, phase: %q", r.Phase) 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 // CanRestartFromFailedTasks reports if the run can be restarted from failed tasks
func (r *Run) CanRestartFromFailedTasks() (bool, string) { 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 // can restart only if the run phase is finished or cancelled
if !r.Phase.IsFinished() { if !r.Phase.IsFinished() {
return false, fmt.Sprintf("run is not finished, phase: %q", r.Phase) 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. // also needed to fetch them when they aren't indexed in the readdb.
Group string `json:"group,omitempty"` 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 // Annotations contain custom run properties
// Note: Annotations are currently both saved in a Run and in RunConfig to // Note: Annotations are currently both saved in a Run and in RunConfig to
// easily return them without loading RunConfig from the lts // easily return them without loading RunConfig from the lts