Files
coder/coderd/prebuilds/preset_snapshot_test.go
Yevhenii Shcherbina 183146e2c9 fix: add minor fix to reconciliation loop (#17454)
Follow-up PR to https://github.com/coder/coder/pull/17261
I noticed that 1 metrics-related test fails in `dk/prebuilds` after
merging my PR into `dk/prebuilds`.
2025-04-17 13:43:24 -04:00

764 lines
25 KiB
Go

package prebuilds_test
import (
"database/sql"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/quartz"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/prebuilds"
)
type options struct {
templateID uuid.UUID
templateVersionID uuid.UUID
presetID uuid.UUID
presetName string
prebuiltWorkspaceID uuid.UUID
workspaceName string
}
// templateID is common across all option sets.
var templateID = uuid.UUID{1}
const (
backoffInterval = time.Second * 5
optionSet0 = iota
optionSet1
optionSet2
)
var opts = map[uint]options{
optionSet0: {
templateID: templateID,
templateVersionID: uuid.UUID{11},
presetID: uuid.UUID{12},
presetName: "my-preset",
prebuiltWorkspaceID: uuid.UUID{13},
workspaceName: "prebuilds0",
},
optionSet1: {
templateID: templateID,
templateVersionID: uuid.UUID{21},
presetID: uuid.UUID{22},
presetName: "my-preset",
prebuiltWorkspaceID: uuid.UUID{23},
workspaceName: "prebuilds1",
},
optionSet2: {
templateID: templateID,
templateVersionID: uuid.UUID{31},
presetID: uuid.UUID{32},
presetName: "my-preset",
prebuiltWorkspaceID: uuid.UUID{33},
workspaceName: "prebuilds2",
},
}
// A new template version with a preset without prebuilds configured should result in no prebuilds being created.
func TestNoPrebuilds(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
clock := quartz.NewMock(t)
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 0, current),
}
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil)
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{ /*all zero values*/ }, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 0,
}, *actions)
}
// A new template version with a preset with prebuilds configured should result in a new prebuild being created.
func TestNetNew(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
clock := quartz.NewMock(t)
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 1, current),
}
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil)
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Desired: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, *actions)
}
// A new template version is created with a preset with prebuilds configured; this outdates the older version and
// requires the old prebuilds to be destroyed and new prebuilds to be created.
func TestOutdatedPrebuilds(t *testing.T) {
t.Parallel()
outdated := opts[optionSet0]
current := opts[optionSet1]
clock := quartz.NewMock(t)
// GIVEN: 2 presets, one outdated and one new.
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(false, 1, outdated),
preset(true, 1, current),
}
// GIVEN: a running prebuild for the outdated preset.
running := []database.GetRunningPrebuiltWorkspacesRow{
prebuiltWorkspace(outdated, clock),
}
// GIVEN: no in-progress builds.
var inProgress []database.CountInProgressPrebuildsRow
// WHEN: calculating the outdated preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil)
ps, err := snapshot.FilterByPreset(outdated.presetID)
require.NoError(t, err)
// THEN: we should identify that this prebuild is outdated and needs to be deleted.
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
}, *actions)
// WHEN: calculating the current preset's state.
ps, err = snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
// THEN: we should not be blocked from creating a new prebuild while the outdate one deletes.
state = ps.CalculateState()
actions, err = ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{Desired: 1}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, *actions)
}
// Make sure that outdated prebuild will be deleted, even if deletion of another outdated prebuild is already in progress.
func TestDeleteOutdatedPrebuilds(t *testing.T) {
t.Parallel()
outdated := opts[optionSet0]
clock := quartz.NewMock(t)
// GIVEN: 1 outdated preset.
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(false, 1, outdated),
}
// GIVEN: one running prebuild for the outdated preset.
running := []database.GetRunningPrebuiltWorkspacesRow{
prebuiltWorkspace(outdated, clock),
}
// GIVEN: one deleting prebuild for the outdated preset.
inProgress := []database.CountInProgressPrebuildsRow{
{
TemplateID: outdated.templateID,
TemplateVersionID: outdated.templateVersionID,
Transition: database.WorkspaceTransitionDelete,
Count: 1,
PresetID: uuid.NullUUID{
UUID: outdated.presetID,
Valid: true,
},
},
}
// WHEN: calculating the outdated preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil)
ps, err := snapshot.FilterByPreset(outdated.presetID)
require.NoError(t, err)
// THEN: we should identify that this prebuild is outdated and needs to be deleted.
// Despite the fact that deletion of another outdated prebuild is already in progress.
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 1,
Deleting: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
}, *actions)
}
// A new template version is created with a preset with prebuilds configured; while a prebuild is provisioning up or down,
// the calculated actions should indicate the state correctly.
func TestInProgressActions(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
clock := quartz.NewMock(t)
cases := []struct {
name string
transition database.WorkspaceTransition
desired int32
running int32
inProgress int32
checkFn func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions)
}{
// With no running prebuilds and one starting, no creations/deletions should take place.
{
name: fmt.Sprintf("%s-short", database.WorkspaceTransitionStart),
transition: database.WorkspaceTransitionStart,
desired: 1,
running: 0,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Desired: 1, Starting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
}, actions)
},
},
// With one running prebuild and one starting, no creations/deletions should occur since we're approaching the correct state.
{
name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionStart),
transition: database.WorkspaceTransitionStart,
desired: 2,
running: 1,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Starting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
}, actions)
},
},
// With one running prebuild and one starting, no creations/deletions should occur
// SIDE-NOTE: once the starting prebuild completes, the older of the two will be considered extraneous since we only desire 2.
{
name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionStart),
transition: database.WorkspaceTransitionStart,
desired: 2,
running: 2,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Starting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
}, actions)
},
},
// With one prebuild desired and one stopping, a new prebuild will be created.
{
name: fmt.Sprintf("%s-short", database.WorkspaceTransitionStop),
transition: database.WorkspaceTransitionStop,
desired: 1,
running: 0,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Desired: 1, Stopping: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, actions)
},
},
// With 3 prebuilds desired, 2 running, and 1 stopping, a new prebuild will be created.
{
name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionStop),
transition: database.WorkspaceTransitionStop,
desired: 3,
running: 2,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 3, Stopping: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, actions)
},
},
// With 3 prebuilds desired, 3 running, and 1 stopping, no creations/deletions should occur since the desired state is already achieved.
{
name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionStop),
transition: database.WorkspaceTransitionStop,
desired: 3,
running: 3,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 3, Desired: 3, Stopping: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
}, actions)
},
},
// With one prebuild desired and one deleting, a new prebuild will be created.
{
name: fmt.Sprintf("%s-short", database.WorkspaceTransitionDelete),
transition: database.WorkspaceTransitionDelete,
desired: 1,
running: 0,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Desired: 1, Deleting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, actions)
},
},
// With 2 prebuilds desired, 1 running, and 1 deleting, a new prebuild will be created.
{
name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionDelete),
transition: database.WorkspaceTransitionDelete,
desired: 2,
running: 1,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Deleting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, actions)
},
},
// With 2 prebuilds desired, 2 running, and 1 deleting, no creations/deletions should occur since the desired state is already achieved.
{
name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionDelete),
transition: database.WorkspaceTransitionDelete,
desired: 2,
running: 2,
inProgress: 1,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Deleting: 1}, state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
}, actions)
},
},
// With 3 prebuilds desired, 1 running, and 2 starting, no creations should occur since the builds are in progress.
{
name: fmt.Sprintf("%s-inhibit", database.WorkspaceTransitionStart),
transition: database.WorkspaceTransitionStart,
desired: 3,
running: 1,
inProgress: 2,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 3, Starting: 2}, state)
validateActions(t, prebuilds.ReconciliationActions{ActionType: prebuilds.ActionTypeCreate, Create: 0}, actions)
},
},
// With 3 prebuilds desired, 5 running, and 2 deleting, no deletions should occur since the builds are in progress.
{
name: fmt.Sprintf("%s-inhibit", database.WorkspaceTransitionDelete),
transition: database.WorkspaceTransitionDelete,
desired: 3,
running: 5,
inProgress: 2,
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
expectedState := prebuilds.ReconciliationState{Actual: 5, Desired: 3, Deleting: 2, Extraneous: 2}
expectedActions := prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeDelete,
}
validateState(t, expectedState, state)
assert.EqualValuesf(t, expectedActions.ActionType, actions.ActionType, "'ActionType' did not match expectation")
assert.Len(t, actions.DeleteIDs, 2, "'deleteIDs' did not match expectation")
assert.EqualValuesf(t, expectedActions.Create, actions.Create, "'create' did not match expectation")
assert.EqualValuesf(t, expectedActions.BackoffUntil, actions.BackoffUntil, "'BackoffUntil' did not match expectation")
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// GIVEN: a preset.
defaultPreset := preset(true, tc.desired, current)
presets := []database.GetTemplatePresetsWithPrebuildsRow{
defaultPreset,
}
// GIVEN: running prebuilt workspaces for the preset.
running := make([]database.GetRunningPrebuiltWorkspacesRow, 0, tc.running)
for range tc.running {
name, err := prebuilds.GenerateName()
require.NoError(t, err)
running = append(running, database.GetRunningPrebuiltWorkspacesRow{
ID: uuid.New(),
Name: name,
TemplateID: current.templateID,
TemplateVersionID: current.templateVersionID,
CurrentPresetID: uuid.NullUUID{UUID: current.presetID, Valid: true},
Ready: false,
CreatedAt: clock.Now(),
})
}
// GIVEN: some prebuilds for the preset which are currently transitioning.
inProgress := []database.CountInProgressPrebuildsRow{
{
TemplateID: current.templateID,
TemplateVersionID: current.templateVersionID,
Transition: tc.transition,
Count: tc.inProgress,
PresetID: uuid.NullUUID{
UUID: defaultPreset.ID,
Valid: true,
},
},
}
// WHEN: calculating the current preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil)
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
// THEN: we should identify that this prebuild is in progress.
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
tc.checkFn(*state, *actions)
})
}
}
// Additional prebuilds exist for a given preset configuration; these must be deleted.
func TestExtraneous(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
clock := quartz.NewMock(t)
// GIVEN: a preset with 1 desired prebuild.
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 1, current),
}
var older uuid.UUID
// GIVEN: 2 running prebuilds for the preset.
running := []database.GetRunningPrebuiltWorkspacesRow{
prebuiltWorkspace(current, clock, func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow {
// The older of the running prebuilds will be deleted in order to maintain freshness.
row.CreatedAt = clock.Now().Add(-time.Hour)
older = row.ID
return row
}),
prebuiltWorkspace(current, clock, func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow {
row.CreatedAt = clock.Now()
return row
}),
}
// GIVEN: NO prebuilds in progress.
var inProgress []database.CountInProgressPrebuildsRow
// WHEN: calculating the current preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil)
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
// THEN: an extraneous prebuild is detected and marked for deletion.
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 2, Desired: 1, Extraneous: 1, Eligible: 2,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{older},
}, *actions)
}
// A template marked as deprecated will not have prebuilds running.
func TestDeprecated(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
clock := quartz.NewMock(t)
// GIVEN: a preset with 1 desired prebuild.
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 1, current, func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
row.Deprecated = true
return row
}),
}
// GIVEN: 1 running prebuilds for the preset.
running := []database.GetRunningPrebuiltWorkspacesRow{
prebuiltWorkspace(current, clock),
}
// GIVEN: NO prebuilds in progress.
var inProgress []database.CountInProgressPrebuildsRow
// WHEN: calculating the current preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil)
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
// THEN: all running prebuilds should be deleted because the template is deprecated.
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeDelete,
DeleteIDs: []uuid.UUID{current.prebuiltWorkspaceID},
}, *actions)
}
// If the latest build failed, backoff exponentially with the given interval.
func TestLatestBuildFailed(t *testing.T) {
t.Parallel()
current := opts[optionSet0]
other := opts[optionSet1]
clock := quartz.NewMock(t)
// GIVEN: two presets.
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 1, current),
preset(true, 1, other),
}
// GIVEN: running prebuilds only for one preset (the other will be failing, as evidenced by the backoffs below).
running := []database.GetRunningPrebuiltWorkspacesRow{
prebuiltWorkspace(other, clock),
}
// GIVEN: NO prebuilds in progress.
var inProgress []database.CountInProgressPrebuildsRow
// GIVEN: a backoff entry.
lastBuildTime := clock.Now()
numFailed := 1
backoffs := []database.GetPresetsBackoffRow{
{
TemplateVersionID: current.templateVersionID,
PresetID: current.presetID,
NumFailed: int32(numFailed),
LastBuildAt: lastBuildTime,
},
}
// WHEN: calculating the current preset's state.
snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, backoffs)
psCurrent, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
// THEN: reconciliation should backoff.
state := psCurrent.CalculateState()
actions, err := psCurrent.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 0, Desired: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeBackoff,
BackoffUntil: lastBuildTime.Add(time.Duration(numFailed) * backoffInterval),
}, *actions)
// WHEN: calculating the other preset's state.
psOther, err := snapshot.FilterByPreset(other.presetID)
require.NoError(t, err)
// THEN: it should NOT be in backoff because all is OK.
state = psOther.CalculateState()
actions, err = psOther.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 1, Desired: 1, Eligible: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
BackoffUntil: time.Time{},
}, *actions)
// WHEN: the clock is advanced a backoff interval.
clock.Advance(backoffInterval + time.Microsecond)
// THEN: a new prebuild should be created.
psCurrent, err = snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
state = psCurrent.CalculateState()
actions, err = psCurrent.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Actual: 0, Desired: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1, // <--- NOTE: we're now able to create a new prebuild because the interval has elapsed.
}, *actions)
}
func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
t.Parallel()
templateID := uuid.New()
templateVersionID := uuid.New()
presetOpts1 := options{
templateID: templateID,
templateVersionID: templateVersionID,
presetID: uuid.New(),
presetName: "my-preset-1",
prebuiltWorkspaceID: uuid.New(),
workspaceName: "prebuilds1",
}
presetOpts2 := options{
templateID: templateID,
templateVersionID: templateVersionID,
presetID: uuid.New(),
presetName: "my-preset-2",
prebuiltWorkspaceID: uuid.New(),
workspaceName: "prebuilds2",
}
clock := quartz.NewMock(t)
presets := []database.GetTemplatePresetsWithPrebuildsRow{
preset(true, 1, presetOpts1),
preset(true, 1, presetOpts2),
}
inProgress := []database.CountInProgressPrebuildsRow{
{
TemplateID: templateID,
TemplateVersionID: templateVersionID,
Transition: database.WorkspaceTransitionStart,
Count: 1,
PresetID: uuid.NullUUID{
UUID: presetOpts1.presetID,
Valid: true,
},
},
}
snapshot := prebuilds.NewGlobalSnapshot(presets, nil, inProgress, nil)
// Nothing has to be created for preset 1.
{
ps, err := snapshot.FilterByPreset(presetOpts1.presetID)
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Starting: 1,
Desired: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 0,
}, *actions)
}
// One prebuild has to be created for preset 2. Make sure preset 1 doesn't block preset 2.
{
ps, err := snapshot.FilterByPreset(presetOpts2.presetID)
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Starting: 0,
Desired: 1,
}, *state)
validateActions(t, prebuilds.ReconciliationActions{
ActionType: prebuilds.ActionTypeCreate,
Create: 1,
}, *actions)
}
}
func preset(active bool, instances int32, opts options, muts ...func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
entry := database.GetTemplatePresetsWithPrebuildsRow{
TemplateID: opts.templateID,
TemplateVersionID: opts.templateVersionID,
ID: opts.presetID,
UsingActiveVersion: active,
Name: opts.presetName,
DesiredInstances: sql.NullInt32{
Valid: true,
Int32: instances,
},
Deleted: false,
Deprecated: false,
}
for _, mut := range muts {
entry = mut(entry)
}
return entry
}
func prebuiltWorkspace(
opts options,
clock quartz.Clock,
muts ...func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow,
) database.GetRunningPrebuiltWorkspacesRow {
entry := database.GetRunningPrebuiltWorkspacesRow{
ID: opts.prebuiltWorkspaceID,
Name: opts.workspaceName,
TemplateID: opts.templateID,
TemplateVersionID: opts.templateVersionID,
CurrentPresetID: uuid.NullUUID{UUID: opts.presetID, Valid: true},
Ready: true,
CreatedAt: clock.Now(),
}
for _, mut := range muts {
entry = mut(entry)
}
return entry
}
func validateState(t *testing.T, expected, actual prebuilds.ReconciliationState) {
require.Equal(t, expected, actual)
}
// validateActions is a convenience func to make tests more readable; it exploits the fact that the default states for
// prebuilds align with zero values.
func validateActions(t *testing.T, expected, actual prebuilds.ReconciliationActions) {
require.Equal(t, expected, actual)
}