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"`
|
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(),
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 len(setupErrors) == 0 {
|
||||||
if err := runconfig.GenTasksLevels(rcts); err != nil {
|
if err := runconfig.GenTasksLevels(rcts); err != nil {
|
||||||
return nil, util.NewErrBadRequest(err)
|
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
|
||||||
|
|
|
@ -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,6 +47,7 @@ type RunCounter struct {
|
||||||
type RunPhase string
|
type RunPhase string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
RunPhaseSetupError RunPhase = "setuperror"
|
||||||
RunPhaseQueued RunPhase = "queued"
|
RunPhaseQueued RunPhase = "queued"
|
||||||
RunPhaseCancelled RunPhase = "cancelled"
|
RunPhaseCancelled RunPhase = "cancelled"
|
||||||
RunPhaseRunning RunPhase = "running"
|
RunPhaseRunning RunPhase = "running"
|
||||||
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue