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:
parent
671b89d391
commit
da27348a1d
|
@ -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(),
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue