agola/internal/services/runservice/scheduler_test.go

716 lines
20 KiB
Go

// Copyright 2019 Sorint.lab
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
// See the License for the specific language governing permissions and
// limitations under the License.
package runservice
import (
"slices"
"testing"
"time"
"gotest.tools/assert"
"agola.io/agola/internal/sqlg"
"agola.io/agola/internal/testutil"
"agola.io/agola/services/runservice/types"
stypes "agola.io/agola/services/types"
)
func TestAdvanceRunTasks(t *testing.T) {
t.Parallel()
log := testutil.NewLogger(t)
// a global run config for all tests
rc := &types.RunConfig{
Tasks: map[string]*types.RunConfigTask{
"task01": {
ID: "task01",
Name: "task01",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task02": {
ID: "task02",
Name: "task02",
Depends: map[string]*types.RunConfigTaskDepend{
"task01": {TaskID: "task01", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task03": {
ID: "task03",
Name: "task03",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task04": {
ID: "task04",
Name: "task04",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task05": {
ID: "task05",
Name: "task05",
Depends: map[string]*types.RunConfigTaskDepend{
"task03": {TaskID: "task03", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
"task04": {TaskID: "task04", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
},
}
// initial run that matched the runconfig:
// * the run is in phase running with result unknown
// * all tasks are not started or skipped
// (if the runconfig task as Skip == true). This must match the status
// generated by command.genRun()
run := &types.Run{
Phase: types.RunPhaseRunning,
Result: types.RunResultUnknown,
Tasks: map[string]*types.RunTask{
"task01": {
ID: "task01",
Status: types.RunTaskStatusNotStarted,
},
"task02": {
ID: "task02",
Status: types.RunTaskStatusNotStarted,
},
"task03": {
ID: "task03",
Status: types.RunTaskStatusNotStarted,
},
"task04": {
ID: "task04",
Status: types.RunTaskStatusNotStarted,
},
"task05": {
ID: "task05",
Status: types.RunTaskStatusNotStarted,
},
},
}
tests := []struct {
name string
rc *types.RunConfig
r *types.Run
scheduledExecutorTasks []*types.ExecutorTask
out *types.Run
}{
{
name: "test top level task not started",
rc: rc,
r: run.DeepCopy(),
out: run.DeepCopy(),
},
{
name: "test task status set to skipped when parent status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
run.Tasks["task02"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task status set to skipped when all parent status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task04"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSkipped
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSkipped
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when only some parents status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when one of the parents doesn't match default conditions (on_success)",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when one of the parents doesn't match custom conditions",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnFailure}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to not skipped when one of the parent is skipped and task condition is on_skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSkipped}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
},
{
name: "test task not set to waiting approval when task is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].NeedsApproval = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to waiting approval when all the parents are finished and task is not skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].NeedsApproval = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSkipped}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].WaitingApproval = true
return run
}(),
},
{
name: "cancel all root not started tasks when run has a result set",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task01"].Status = types.RunTaskStatusCancelled
run.Tasks["task02"].Status = types.RunTaskStatusNotStarted
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusNotStarted
return run
}(),
},
{
name: "cancel all root not started tasks when run has a result set (task01 is already scheduled)",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
scheduledExecutorTasks: []*types.ExecutorTask{
{
ObjectMeta: sqlg.ObjectMeta{ID: "executortask01"},
RunTaskID: "task01",
},
},
out: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task01"].Status = types.RunTaskStatusNotStarted
run.Tasks["task02"].Status = types.RunTaskStatusNotStarted
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusNotStarted
return run
}(),
},
{
name: "skip all not started tasks when run is set to stop",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusRunning
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Stop = true
return run
}(),
scheduledExecutorTasks: []*types.ExecutorTask{
{
ObjectMeta: sqlg.ObjectMeta{ID: "executortask01"},
RunTaskID: "task01",
},
},
out: func() *types.Run {
run := run.DeepCopy()
run.Stop = true
run.Tasks["task01"].Status = types.RunTaskStatusRunning
run.Tasks["task02"].Status = types.RunTaskStatusSkipped
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r, err := advanceRunTasks(log, tt.r, tt.rc, tt.scheduledExecutorTasks)
testutil.NilError(t, err)
assert.DeepEqual(t, tt.out, r)
})
}
}
func TestGetTasksToRun(t *testing.T) {
t.Parallel()
log := testutil.NewLogger(t)
// a global run config for all tests
rc := &types.RunConfig{
Tasks: map[string]*types.RunConfigTask{
"task01": {
ID: "task01",
Name: "task01",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task02": {
ID: "task02",
Name: "task02",
Depends: map[string]*types.RunConfigTaskDepend{
"task01": {TaskID: "task01", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task03": {
ID: "task03",
Name: "task03",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task04": {
ID: "task04",
Name: "task04",
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
"task05": {
ID: "task05",
Name: "task05",
Depends: map[string]*types.RunConfigTaskDepend{
"task03": {TaskID: "task03", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
"task04": {TaskID: "task04", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: types.Steps{},
Skip: false,
},
},
}
// initial run that matched the runconfig, all tasks are not started or skipped
// (if the runconfig task as Skip == true). This must match the status
// generated by command.genRun()
run := &types.Run{
Tasks: map[string]*types.RunTask{
"task01": {
ID: "task01",
Status: types.RunTaskStatusNotStarted,
},
"task02": {
ID: "task02",
Status: types.RunTaskStatusNotStarted,
},
"task03": {
ID: "task03",
Status: types.RunTaskStatusNotStarted,
},
"task04": {
ID: "task04",
Status: types.RunTaskStatusNotStarted,
},
"task05": {
ID: "task05",
Status: types.RunTaskStatusNotStarted,
},
},
}
tests := []struct {
name string
rc *types.RunConfig
r *types.Run
out []string
}{
{
name: "test run top level tasks",
rc: rc,
r: run.DeepCopy(),
out: []string{"task01", "task03", "task04"},
},
{
name: "test don't run skipped tasks",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
run.Tasks["task02"].Status = types.RunTaskStatusSkipped
return run
}(),
out: []string{"task03", "task04"},
},
{
name: "test don't run if needs approval but not approved",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].NeedsApproval = true
return rc
}(),
r: run.DeepCopy(),
out: []string{"task03", "task04"},
},
{
name: "test run if needs approval and approved",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].NeedsApproval = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Approved = true
return run
}(),
out: []string{"task01", "task03", "task04"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tasks, err := getTasksToRun(log, tt.r, tt.rc)
testutil.NilError(t, err)
outTasks := []string{}
for _, t := range tasks {
outTasks = append(outTasks, t.ID)
}
slices.Sort(tt.out)
slices.Sort(outTasks)
assert.DeepEqual(t, tt.out, outTasks)
})
}
}
func TestChooseExecutor(t *testing.T) {
t.Parallel()
executorOK := &types.Executor{
ExecutorID: "executorOK",
Archs: []stypes.Arch{stypes.ArchAMD64},
ActiveTasksLimit: 2,
ActiveTasks: 0,
ObjectMeta: sqlg.ObjectMeta{
UpdateTime: time.Now(),
},
}
executorNoFreeTaskSlots := func() *types.Executor {
e := executorOK.DeepCopy()
e.ExecutorID = "executorNoFreeTasksSlots"
e.ActiveTasks = 2
return e
}()
executorNotAlive := func() *types.Executor {
e := executorOK.DeepCopy()
e.ExecutorID = "executorNotAlive"
e.UpdateTime = time.Now().Add(-120 * time.Second)
return e
}()
executorOKMultipleArchs := func() *types.Executor {
e := executorOK.DeepCopy()
e.ExecutorID = "executorOKMultipleArchs"
e.Archs = []stypes.Arch{stypes.ArchAMD64, stypes.ArchARM64}
return e
}()
executorOKAllowsPriviledContainers := func() *types.Executor {
e := executorOK.DeepCopy()
e.ExecutorID = "executorOKAllowsPrivilegedContainers"
e.AllowPrivilegedContainers = true
return e
}()
// Only primary and the required variables for this test are set
rct := &types.RunConfigTask{
ID: "task01",
Name: "task01",
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Arch: stypes.ArchAMD64,
},
}
rctWithPrivilegedContainers := &types.RunConfigTask{
ID: "task01",
Name: "task01",
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Arch: stypes.ArchAMD64,
Containers: []*types.Container{
{
Privileged: true,
},
},
},
}
tests := []struct {
name string
executors []*types.Executor
rct *types.RunConfigTask
out *types.Executor
}{
{
name: "test single executor ok",
executors: []*types.Executor{executorOK},
// Only primary and the required variables for this test are set
rct: rct,
out: executorOK,
},
{
name: "test single executor without free task slots",
executors: []*types.Executor{executorNoFreeTaskSlots},
// Only primary and the required variables for this test are set
rct: rct,
out: nil,
},
{
name: "test single executor not alive",
executors: []*types.Executor{executorNotAlive},
rct: rct,
out: nil,
},
{
name: "test single executor with different arch",
executors: func() []*types.Executor {
e := executorOK.DeepCopy()
e.Archs = []stypes.Arch{stypes.ArchARM64}
return []*types.Executor{e}
}(),
rct: rct,
out: nil,
},
{
name: "test single executor with multiple archs and one matches the task required arch",
executors: []*types.Executor{executorOKMultipleArchs},
rct: rct,
out: executorOKMultipleArchs,
},
{
name: "test single executor without allowed privileged container but privileged containers are required",
executors: []*types.Executor{executorOK},
rct: rctWithPrivilegedContainers,
out: nil,
},
{
name: "test single executor with allowed privileged container and privileged containers are required",
executors: []*types.Executor{executorOKAllowsPriviledContainers},
rct: rctWithPrivilegedContainers,
out: executorOKAllowsPriviledContainers,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
e := chooseExecutor(tt.executors, map[string]int{}, tt.rct)
assert.Equal(t, e, tt.out)
})
}
}