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"`
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(),

View File

@ -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
}
}

View File

@ -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,

View File

@ -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

View File

@ -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