diff --git a/internal/config/config.go b/internal/config/config.go index a815db979..9e99da0ae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,6 +55,7 @@ type Config struct { Runs []*Run `json:"runs"` DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"` + TaskTimeoutInterval *types.Duration `json:"task_timeout_interval"` } type RuntimeType string @@ -113,6 +114,7 @@ type Run struct { Tasks []*Task `json:"tasks"` When *When `json:"when"` DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"` + TaskTimeoutInterval *types.Duration `json:"task_timeout_interval"` } type Task struct { @@ -128,6 +130,7 @@ type Task struct { Approval bool `json:"approval"` When *When `json:"when"` DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"` + TaskTimeoutInterval *types.Duration `json:"task_timeout_interval"` } type DependCondition string diff --git a/internal/migration/runservice/types/types.go b/internal/migration/runservice/types/types.go index 2b7ebd2fb..0ee06f7e5 100644 --- a/internal/migration/runservice/types/types.go +++ b/internal/migration/runservice/types/types.go @@ -206,10 +206,11 @@ const ( RunTaskStatusStopped RunTaskStatus = "stopped" RunTaskStatusSuccess RunTaskStatus = "success" RunTaskStatusFailed RunTaskStatus = "failed" + RunTaskStatusTimedout RunTaskStatus = "timedout" ) func (s RunTaskStatus) IsFinished() bool { - return s == RunTaskStatusCancelled || s == RunTaskStatusSkipped || s == RunTaskStatusStopped || s == RunTaskStatusSuccess || s == RunTaskStatusFailed + return s == RunTaskStatusCancelled || s == RunTaskStatusSkipped || s == RunTaskStatusStopped || s == RunTaskStatusSuccess || s == RunTaskStatusFailed || s == RunTaskStatusTimedout } type RunTaskFetchPhase string diff --git a/internal/runconfig/runconfig.go b/internal/runconfig/runconfig.go index 9ff2512c3..faf2aa41e 100644 --- a/internal/runconfig/runconfig.go +++ b/internal/runconfig/runconfig.go @@ -270,6 +270,20 @@ func GenRunConfigTasks(uuid util.UUIDGenerator, c *config.Config, runName string } } + if c.TaskTimeoutInterval != nil { + t.TaskTimeoutInterval = c.TaskTimeoutInterval.Duration + } + + // override with per run task timeout + if cr.TaskTimeoutInterval != nil { + t.TaskTimeoutInterval = cr.TaskTimeoutInterval.Duration + } + + // override with per task timeout + if ct.TaskTimeoutInterval != nil { + t.TaskTimeoutInterval = ct.TaskTimeoutInterval.Duration + } + rcts[t.ID] = t } diff --git a/internal/runconfig/runconfig_test.go b/internal/runconfig/runconfig_test.go index 4cde8ce62..9d51ffc36 100644 --- a/internal/runconfig/runconfig_test.go +++ b/internal/runconfig/runconfig_test.go @@ -18,6 +18,7 @@ import ( "fmt" "reflect" "testing" + "time" "agola.io/agola/internal/config" "agola.io/agola/internal/errors" @@ -1358,6 +1359,162 @@ func TestGenRunConfig(t *testing.T) { }, }, }, + { + name: "test runconfig generation task timeout global", + in: &config.Config{ + Runs: []*config.Run{ + &config.Run{ + Name: "run01", + Tasks: []*config.Task{ + &config.Task{ + Name: "task01", + Runtime: &config.Runtime{ + Type: "pod", + Arch: "", + Containers: []*config.Container{ + &config.Container{ + Image: "image01", + }, + }, + }, + + Depends: []*config.Depend{}, + WorkingDir: "", + Shell: "", + User: "", + }, + }, + }, + }, + TaskTimeoutInterval: &types.Duration{Duration: 10 * time.Second}, + }, + out: map[string]*rstypes.RunConfigTask{ + uuid.New("task01").String(): &rstypes.RunConfigTask{ + ID: uuid.New("task01").String(), + Name: "task01", Depends: map[string]*rstypes.RunConfigTaskDepend{}, + DockerRegistriesAuth: map[string]rstypes.DockerRegistryAuth{}, + TaskTimeoutInterval: 10 * time.Second, + Runtime: &rstypes.Runtime{Type: rstypes.RuntimeType("pod"), + Containers: []*rstypes.Container{ + { + Image: "image01", + Volumes: []rstypes.Volume{}, + Environment: map[string]string{}, + }, + }, + }, + Environment: map[string]string{}, + Skip: false, + Steps: rstypes.Steps{}, + Shell: "/bin/sh -e", + }, + }, + }, + { + name: "test global task timeout override by run", + in: &config.Config{ + Runs: []*config.Run{ + &config.Run{ + Name: "run01", + Tasks: []*config.Task{ + &config.Task{ + Name: "task01", + Runtime: &config.Runtime{ + Type: "pod", + Arch: "", + Containers: []*config.Container{ + &config.Container{ + Image: "image01", + }, + }, + }, + + Depends: []*config.Depend{}, + WorkingDir: "", + Shell: "", + User: "", + }, + }, + TaskTimeoutInterval: &types.Duration{Duration: 15 * time.Second}, + }, + }, + TaskTimeoutInterval: &types.Duration{Duration: 10 * time.Second}, + }, + out: map[string]*rstypes.RunConfigTask{ + uuid.New("task01").String(): &rstypes.RunConfigTask{ + ID: uuid.New("task01").String(), + Name: "task01", Depends: map[string]*rstypes.RunConfigTaskDepend{}, + DockerRegistriesAuth: map[string]rstypes.DockerRegistryAuth{}, + TaskTimeoutInterval: 15 * time.Second, + Runtime: &rstypes.Runtime{Type: rstypes.RuntimeType("pod"), + Containers: []*rstypes.Container{ + { + Image: "image01", + Volumes: []rstypes.Volume{}, + Environment: map[string]string{}, + }, + }, + }, + Environment: map[string]string{}, + Skip: false, + Steps: rstypes.Steps{}, + Shell: "/bin/sh -e", + }, + }, + }, + { + name: "test global task timeout override by task", + in: &config.Config{ + Runs: []*config.Run{ + &config.Run{ + Name: "run01", + Tasks: []*config.Task{ + &config.Task{ + Name: "task01", + TaskTimeoutInterval: &types.Duration{Duration: 20 * time.Second}, + Runtime: &config.Runtime{ + Type: "pod", + Arch: "", + Containers: []*config.Container{ + &config.Container{ + Image: "image01", + }, + }, + }, + + Depends: []*config.Depend{}, + WorkingDir: "", + Shell: "", + User: "", + }, + }, + TaskTimeoutInterval: &types.Duration{Duration: 15 * time.Second}, + }, + }, + TaskTimeoutInterval: &types.Duration{Duration: 10 * time.Second}, + }, + out: map[string]*rstypes.RunConfigTask{ + uuid.New("task01").String(): &rstypes.RunConfigTask{ + ID: uuid.New("task01").String(), + Name: "task01", Depends: map[string]*rstypes.RunConfigTaskDepend{}, + DockerRegistriesAuth: map[string]rstypes.DockerRegistryAuth{}, + TaskTimeoutInterval: 20 * time.Second, + Runtime: &rstypes.Runtime{Type: rstypes.RuntimeType("pod"), + Containers: []*rstypes.Container{ + { + Image: "image01", + Volumes: []rstypes.Volume{}, + Environment: map[string]string{}, + }, + }, + }, + Environment: map[string]string{}, + Skip: false, + Steps: rstypes.Steps{}, + Shell: "/bin/sh -e", + }, + }, + }, } for _, tt := range tests { diff --git a/internal/services/executor/executor.go b/internal/services/executor/executor.go index 2e873a646..e3b3025c8 100644 --- a/internal/services/executor/executor.go +++ b/internal/services/executor/executor.go @@ -785,7 +785,11 @@ func (e *Executor) executeTask(rt *runningTask) { if err != nil { e.log.Err(err).Send() if rt.et.Spec.Stop { - et.Status.Phase = types.ExecutorTaskPhaseStopped + if et.Spec.StopReason == types.RunStopped { + et.Status.Phase = types.ExecutorTaskPhaseStopped + } else { + et.Status.Phase = types.ExecutorTaskPhaseTimedout + } } else { et.Status.Phase = types.ExecutorTaskPhaseFailed } @@ -1159,6 +1163,7 @@ func (e *Executor) taskUpdater(ctx context.Context, et *types.ExecutorTask) { if rt != nil { rt.Lock() // update running task Spec.Stop value only when there's a transitions from false to true, + // update running task Spec.Timeout value only when there's a transitions from false to true, // other spec values cannot change once the task has been scheduled if !rt.et.Spec.Stop && et.Spec.Stop { rt.et.Spec.Stop = et.Spec.Stop @@ -1488,6 +1493,7 @@ func (e *Executor) Run(ctx context.Context) error { go e.podsCleanerLoop(ctx) go e.tasksUpdaterLoop(ctx) go e.tasksDataCleanerLoop(ctx) + go e.tasksTimeoutCleanerLoop(ctx) go e.handleTasks(ctx, ch) @@ -1517,3 +1523,51 @@ func (e *Executor) Run(ctx context.Context) error { return nil } + +func (e *Executor) tasksTimeoutCleanerLoop(ctx context.Context) { + for { + e.log.Debug().Msgf("tasksTimeoutCleaner") + + e.tasksTimeoutCleaner(ctx) + + sleepCh := time.NewTimer(2 * time.Second).C + select { + case <-ctx.Done(): + return + case <-sleepCh: + } + } +} + +func (e *Executor) tasksTimeoutCleaner(ctx context.Context) { + for _, rtID := range e.runningTasks.ids() { + rt, ok := e.runningTasks.get(rtID) + if !ok { + continue + } + + rt.Lock() + if rt.et.Status.Phase == types.ExecutorTaskPhaseRunning && rt.et.Spec.TaskTimeoutInterval != 0 && time.Since(*rt.et.Status.StartTime) > rt.et.Spec.TaskTimeoutInterval { + rt.et.Status.Phase = types.ExecutorTaskPhaseTimedout + rt.et.Status.EndTime = util.TimeP(time.Now()) + + if rt.et.Status.SetupStep.Phase == types.ExecutorTaskPhaseRunning { + rt.et.Status.SetupStep.Phase = types.ExecutorTaskPhaseFailed + rt.et.Status.SetupStep.EndTime = util.TimeP(time.Now()) + } + for _, s := range rt.et.Status.Steps { + if s.Phase == types.ExecutorTaskPhaseRunning { + s.Phase = types.ExecutorTaskPhaseFailed + s.EndTime = util.TimeP(time.Now()) + } + } + + if err := e.sendExecutorTaskStatus(ctx, rt.et); err != nil { + e.log.Err(err).Send() + } + + rt.cancel() + } + rt.Unlock() + } +} diff --git a/internal/services/gateway/api/run.go b/internal/services/gateway/api/run.go index a16e946e7..f8344eb94 100644 --- a/internal/services/gateway/api/run.go +++ b/internal/services/gateway/api/run.go @@ -76,6 +76,8 @@ func createRunResponseTask(r *rstypes.Run, rt *rstypes.RunTask, rct *rstypes.Run Level: rct.Level, Depends: rct.Depends, + + TaskTimeoutInterval: rct.TaskTimeoutInterval, } return t @@ -96,6 +98,8 @@ func createRunTaskResponse(rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *gwa StartTime: rt.StartTime, EndTime: rt.EndTime, + + TaskTimeoutInterval: rct.TaskTimeoutInterval, } t.SetupStep = &gwapitypes.RunTaskResponseSetupStep{ diff --git a/internal/services/runservice/common/common.go b/internal/services/runservice/common/common.go index 2bfc24df8..cabed73d2 100644 --- a/internal/services/runservice/common/common.go +++ b/internal/services/runservice/common/common.go @@ -143,6 +143,7 @@ func GenExecutorTaskSpecData(r *types.Run, rt *types.RunTask, rc *types.RunConfi Steps: rct.Steps, CachePrefix: cachePrefix, DockerRegistriesAuth: rct.DockerRegistriesAuth, + TaskTimeoutInterval: rct.TaskTimeoutInterval, } // calculate workspace operations diff --git a/internal/services/runservice/scheduler.go b/internal/services/runservice/scheduler.go index 65ace5b20..6a5080f0a 100644 --- a/internal/services/runservice/scheduler.go +++ b/internal/services/runservice/scheduler.go @@ -554,6 +554,7 @@ func (s *Runservice) scheduleRun(ctx context.Context, runID string) error { if r.Stop { for _, et := range scheduledExecutorTasks { et.Spec.Stop = true + et.Spec.StopReason = types.RunStopped if err := s.d.UpdateExecutorTask(tx, et); err != nil { return errors.WithStack(err) } @@ -624,7 +625,7 @@ func advanceRun(log zerolog.Logger, r *types.Run, rc *types.RunConfig, scheduled if !ok { return errors.Errorf("no such run config task with id %s for run config %s", rt.ID, rc.ID) } - if rt.Status == types.RunTaskStatusFailed { + if rt.Status == types.RunTaskStatusFailed || rt.Status == types.RunTaskStatusTimedout { if !rct.IgnoreFailure { log.Debug().Msgf("marking run %q as failed is task %q is failed", r.ID, rt.ID) r.Result = types.RunResultFailed @@ -774,6 +775,12 @@ func (s *Runservice) updateRunTaskStatus(et *types.ExecutorTask, r *types.Run) e rt.Status != types.RunTaskStatusRunning { wrongstatus = true } + case types.ExecutorTaskPhaseTimedout: + if rt.Status != types.RunTaskStatusTimedout && + rt.Status != types.RunTaskStatusNotStarted && + rt.Status != types.RunTaskStatusRunning { + wrongstatus = true + } } if wrongstatus { s.log.Warn().Msgf("ignoring wrong executor task %q status: %q, rt status: %q", et.ID, et.Status.Phase, rt.Status) @@ -793,6 +800,8 @@ func (s *Runservice) updateRunTaskStatus(et *types.ExecutorTask, r *types.Run) e rt.Status = types.RunTaskStatusSuccess case types.ExecutorTaskPhaseFailed: rt.Status = types.RunTaskStatusFailed + case types.ExecutorTaskPhaseTimedout: + rt.Status = types.RunTaskStatusTimedout } rt.SetupStep.Phase = et.Status.SetupStep.Phase @@ -902,6 +911,7 @@ func (s *Runservice) executorTaskCleaner(ctx context.Context, executorTaskID str // if the run is finished mark the executor tasks to stop if !et.Spec.Stop { et.Spec.Stop = true + et.Spec.StopReason = types.RunStopped if err := s.d.UpdateExecutorTask(tx, et); err != nil { return errors.WithStack(err) } @@ -911,6 +921,34 @@ func (s *Runservice) executorTaskCleaner(ctx context.Context, executorTaskID str } if !et.Status.Phase.IsFinished() { + if et.Status.Phase == types.ExecutorTaskPhaseRunning { + // generate ExecutorTaskSpecData + r, err := s.d.GetRun(tx, et.Spec.RunID) + if err != nil { + return errors.WithStack(err) + } + rt, ok := r.Tasks[et.Spec.RunTaskID] + if !ok { + return errors.Errorf("no such run task with id %s for run %s", et.Spec.RunTaskID, r.ID) + } + rc, err := s.d.GetRunConfig(tx, r.RunConfigID) + if err != nil { + return errors.WithStack(err) + } + et.Spec.ExecutorTaskSpecData = common.GenExecutorTaskSpecData(r, rt, rc) + + if et.Spec.TaskTimeoutInterval != 0 && time.Since(*et.Status.StartTime) > et.Spec.TaskTimeoutInterval { + s.log.Warn().Msgf("task with id %q timeout", et.ID) + + et.Spec.Stop = true + et.Spec.StopReason = types.TaskTimedout + if err := s.d.UpdateExecutorTask(tx, et); err != nil { + return errors.WithStack(err) + } + shouldSend = true + } + } + // if the executor doesn't exists anymore mark the not finished executor tasks as failed executor, err := s.d.GetExecutorByExecutorID(tx, et.Spec.ExecutorID) if err != nil { diff --git a/services/gateway/api/types/run.go b/services/gateway/api/types/run.go index 0cedf640c..b5e6c68e9 100644 --- a/services/gateway/api/types/run.go +++ b/services/gateway/api/types/run.go @@ -73,6 +73,8 @@ type RunResponseTask struct { StartTime *time.Time `json:"start_time"` EndTime *time.Time `json:"end_time"` + + TaskTimeoutInterval time.Duration `json:"task_timeout_interval"` } type RunTaskResponse struct { @@ -90,6 +92,8 @@ type RunTaskResponse struct { StartTime *time.Time `json:"start_time"` EndTime *time.Time `json:"end_time"` + + TaskTimeoutInterval time.Duration `json:"task_timeout_interval"` } type RunTaskResponseContainer struct { diff --git a/services/runservice/types/executortask.go b/services/runservice/types/executortask.go index 87abc3e8c..0e072b51a 100644 --- a/services/runservice/types/executortask.go +++ b/services/runservice/types/executortask.go @@ -23,10 +23,11 @@ const ( ExecutorTaskPhaseStopped ExecutorTaskPhase = "stopped" ExecutorTaskPhaseSuccess ExecutorTaskPhase = "success" ExecutorTaskPhaseFailed ExecutorTaskPhase = "failed" + ExecutorTaskPhaseTimedout ExecutorTaskPhase = "timedout" ) func (s ExecutorTaskPhase) IsFinished() bool { - return s == ExecutorTaskPhaseCancelled || s == ExecutorTaskPhaseStopped || s == ExecutorTaskPhaseSuccess || s == ExecutorTaskPhaseFailed + return s == ExecutorTaskPhaseCancelled || s == ExecutorTaskPhaseStopped || s == ExecutorTaskPhaseSuccess || s == ExecutorTaskPhaseFailed || s == ExecutorTaskPhaseTimedout } type ExecutorTask struct { @@ -53,10 +54,19 @@ type ExecutorTaskSpec struct { // Stop is used to signal from the scheduler when the task must be stopped Stop bool `json:"stop,omitempty"` + // StopReason is used to know why the Executor should stop it + StopReason `json:"stop_reason,omitempty"` *ExecutorTaskSpecData } +type StopReason string + +const ( + RunStopped StopReason = "runstopped" + TaskTimedout StopReason = "tasktimedout" +) + // ExecutorTaskSpecData defines the task data required to execute the tasks. // These values are not saved in the db to avoid using too much space but are // generated everytime they are sent to the executor @@ -79,6 +89,8 @@ type ExecutorTaskSpecData struct { CachePrefix string `json:"cache_prefix,omitempty"` Steps Steps `json:"steps,omitempty"` + + TaskTimeoutInterval time.Duration `json:"task_timeout_interval"` } type ExecutorTaskStatus struct { diff --git a/services/runservice/types/run.go b/services/runservice/types/run.go index 2e6802341..a580fab93 100644 --- a/services/runservice/types/run.go +++ b/services/runservice/types/run.go @@ -178,10 +178,11 @@ const ( RunTaskStatusStopped RunTaskStatus = "stopped" RunTaskStatusSuccess RunTaskStatus = "success" RunTaskStatusFailed RunTaskStatus = "failed" + RunTaskStatusTimedout RunTaskStatus = "timedout" ) func (s RunTaskStatus) IsFinished() bool { - return s == RunTaskStatusCancelled || s == RunTaskStatusSkipped || s == RunTaskStatusStopped || s == RunTaskStatusSuccess || s == RunTaskStatusFailed + return s == RunTaskStatusCancelled || s == RunTaskStatusSkipped || s == RunTaskStatusStopped || s == RunTaskStatusSuccess || s == RunTaskStatusFailed || s == RunTaskStatusTimedout } type RunTaskFetchPhase string @@ -220,6 +221,8 @@ type RunTask struct { StartTime *time.Time `json:"start_time,omitempty"` EndTime *time.Time `json:"end_time,omitempty"` + + TaskTimeoutInterval *time.Duration `json:"task_timeout_interval"` } func (rt *RunTask) LogsFetchFinished() bool { diff --git a/services/runservice/types/runconfig.go b/services/runservice/types/runconfig.go index 33e2db83b..93291204b 100644 --- a/services/runservice/types/runconfig.go +++ b/services/runservice/types/runconfig.go @@ -2,6 +2,7 @@ package types import ( "encoding/json" + "time" "agola.io/agola/internal/errors" stypes "agola.io/agola/services/types" @@ -80,6 +81,7 @@ type RunConfigTask struct { NeedsApproval bool `json:"needs_approval,omitempty"` Skip bool `json:"skip,omitempty"` DockerRegistriesAuth map[string]DockerRegistryAuth `json:"docker_registries_auth"` + TaskTimeoutInterval time.Duration `json:"task_timeout_interval"` } func (rct *RunConfigTask) DeepCopy() *RunConfigTask { diff --git a/tests/setup_test.go b/tests/setup_test.go index 947b7c314..4c02f5b3a 100644 --- a/tests/setup_test.go +++ b/tests/setup_test.go @@ -41,6 +41,7 @@ import ( "agola.io/agola/internal/util" gwapitypes "agola.io/agola/services/gateway/api/types" gwclient "agola.io/agola/services/gateway/client" + "agola.io/agola/services/runservice/types" rstypes "agola.io/agola/services/runservice/types" "code.gitea.io/sdk/gitea" @@ -1928,3 +1929,269 @@ func TestUserOrgs(t *testing.T) { t.Fatalf("user orgs mismatch (-want +got):\n%s", diff) } } + +func TestTaskTimeout(t *testing.T) { + tests := []struct { + name string + config string + tasksResultExpected map[string]rstypes.RunTaskStatus + }{ + { + name: "test timeout string value", + tasksResultExpected: map[string]rstypes.RunTaskStatus{"task01": types.RunTaskStatusTimedout}, + config: ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: "15s", + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + ], + }, + ], + } + `, + }, + { + name: "test timeout int value", + tasksResultExpected: map[string]rstypes.RunTaskStatus{"task01": types.RunTaskStatusTimedout}, + config: ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: 15000000000, + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + ], + }, + ], + } + `, + }, + { + name: "test timeout child timeout", + tasksResultExpected: map[string]rstypes.RunTaskStatus{"task01": types.RunTaskStatusSuccess, "task02": types.RunTaskStatusTimedout}, + config: ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + { + name: 'task02', + depends: ['task01'], + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: "15s", + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + ], + }, + ], + } + `, + }, + { + name: "test timeout parent timeout", + tasksResultExpected: map[string]rstypes.RunTaskStatus{"task01": types.RunTaskStatusTimedout, "task02": types.RunTaskStatusSkipped}, + config: ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: "15s", + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + { + name: 'task02', + depends: ['task01'], + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + ], + }, + ], + } + `, + }, + { + name: "test timeout parent and child timeout", + tasksResultExpected: map[string]rstypes.RunTaskStatus{"task01": types.RunTaskStatusTimedout, "task02": types.RunTaskStatusSkipped}, + config: ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: "15s", + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + { + name: 'task02', + depends: ['task01'], + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + task_timeout_interval: "15s", + steps: [ + { type: 'run', command: 'sleep 30' }, + ], + }, + ], + }, + ], + } + `, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + _, c := setup(ctx, t, dir, false) + + gwClient := gwclient.NewClient(c.Gateway.APIExposedURL, "admintoken") + user, _, err := gwClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser01}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + t.Logf("created agola user: %s", user.UserName) + + token := createAgolaUserToken(ctx, t, c) + + // From now use the user token + gwClient = gwclient.NewClient(c.Gateway.APIExposedURL, token) + + directRun(t, dir, tt.config, ConfigFormatJsonnet, c.Gateway.APIExposedURL, token) + + time.Sleep(30 * time.Second) + + _ = testutil.Wait(120*time.Second, func() (bool, error) { + run, _, err := gwClient.GetUserRun(ctx, user.ID, 1) + if err != nil { + return false, nil + } + + if run == nil { + return false, nil + } + + if run.Phase != rstypes.RunPhaseFinished { + return false, nil + } + + return true, nil + }) + + run, _, err := gwClient.GetUserRun(ctx, user.ID, 1) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + t.Logf("runs: %s", util.Dump(run)) + + if run == nil { + t.Fatalf("user run not found") + } + if run.Phase != rstypes.RunPhaseFinished { + t.Fatalf("expected run finished got: %s", run.Phase) + } + if run.Result != rstypes.RunResultFailed { + t.Fatalf("expected run failed") + } + if len(run.Tasks) != len(tt.tasksResultExpected) { + t.Fatalf("expected 1 task got: %d", len(run.Tasks)) + } + for _, task := range run.Tasks { + if task.Status != tt.tasksResultExpected[task.Name] { + t.Fatalf("expected task status timeout got: %s", task.Status) + } + } + }) + } +}