mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
439 lines
15 KiB
Go
439 lines
15 KiB
Go
package prebuilds
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
)
|
|
|
|
type options struct {
|
|
templateID uuid.UUID
|
|
templateVersionID uuid.UUID
|
|
presetID uuid.UUID
|
|
presetName string
|
|
prebuildID uuid.UUID
|
|
workspaceName string
|
|
}
|
|
|
|
// templateID is common across all option sets.
|
|
var templateID = uuid.New()
|
|
|
|
const (
|
|
optionSet0 = iota
|
|
optionSet1
|
|
optionSet2
|
|
)
|
|
|
|
var opts = map[uint]options{
|
|
optionSet0: {
|
|
templateID: templateID,
|
|
templateVersionID: uuid.New(),
|
|
presetID: uuid.New(),
|
|
presetName: "my-preset",
|
|
prebuildID: uuid.New(),
|
|
workspaceName: "prebuilds0",
|
|
},
|
|
optionSet1: {
|
|
templateID: templateID,
|
|
templateVersionID: uuid.New(),
|
|
presetID: uuid.New(),
|
|
presetName: "my-preset",
|
|
prebuildID: uuid.New(),
|
|
workspaceName: "prebuilds1",
|
|
},
|
|
optionSet2: {
|
|
templateID: templateID,
|
|
templateVersionID: uuid.New(),
|
|
presetID: uuid.New(),
|
|
presetName: "my-preset",
|
|
prebuildID: uuid.New(),
|
|
workspaceName: "prebuilds2",
|
|
},
|
|
}
|
|
|
|
// A new template version with a preset without prebuilds configured should result in no prebuilds being created.
|
|
func TestNoPrebuilds(t *testing.T) {
|
|
current := opts[optionSet0]
|
|
|
|
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
|
preset(true, 0, current),
|
|
}
|
|
|
|
state := newReconciliationState(presets, nil, nil)
|
|
ps, err := state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
|
|
validateActions(t, reconciliationActions{ /*all zero values*/ }, *actions)
|
|
}
|
|
|
|
// A new template version with a preset with prebuilds configured should result in a new prebuild being created.
|
|
func TestNetNew(t *testing.T) {
|
|
current := opts[optionSet0]
|
|
|
|
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
|
preset(true, 1, current),
|
|
}
|
|
|
|
state := newReconciliationState(presets, nil, nil)
|
|
ps, err := state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
|
|
validateActions(t, reconciliationActions{
|
|
desired: 1,
|
|
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) {
|
|
outdated := opts[optionSet0]
|
|
current := opts[optionSet1]
|
|
|
|
// 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.GetRunningPrebuildsRow{
|
|
prebuild(outdated),
|
|
}
|
|
|
|
// GIVEN: no in-progress builds.
|
|
var inProgress []database.GetPrebuildsInProgressRow
|
|
|
|
// WHEN: calculating the outdated preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(outdated.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we should identify that this prebuild is outdated and needs to be deleted.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{outdated: 1, deleteIDs: []uuid.UUID{outdated.prebuildID}}, *actions)
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
ps, err = state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we should not be blocked from creating a new prebuild while the outdate one deletes.
|
|
actions, err = ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{desired: 1, create: 1}, *actions)
|
|
}
|
|
|
|
// A new template version is created with a preset with prebuilds configured; while the outdated prebuild is deleting,
|
|
// the new preset's prebuild cannot be provisioned concurrently, to prevent clobbering.
|
|
func TestBlockedOnDeleteActions(t *testing.T) {
|
|
outdated := opts[optionSet0]
|
|
current := opts[optionSet1]
|
|
|
|
// 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.GetRunningPrebuildsRow{
|
|
prebuild(outdated),
|
|
}
|
|
|
|
// GIVEN: one prebuild for the old preset which is currently deleting.
|
|
inProgress := []database.GetPrebuildsInProgressRow{
|
|
{
|
|
TemplateID: outdated.templateID,
|
|
TemplateVersionID: outdated.templateVersionID,
|
|
Transition: database.WorkspaceTransitionDelete,
|
|
Count: 1,
|
|
},
|
|
}
|
|
|
|
// WHEN: calculating the outdated preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(outdated.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we should identify that this prebuild is in progress, and not attempt to delete this prebuild again.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{outdated: 1, deleting: 1}, *actions)
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
ps, err = state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we are blocked from creating a new prebuild while another one is busy provisioning.
|
|
actions, err = ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{desired: 1, create: 0, deleting: 1}, *actions)
|
|
}
|
|
|
|
// A new template version is created with a preset with prebuilds configured. An operator comes along and stops one of the
|
|
// running prebuilds (this shouldn't be done, but it's possible). While this prebuild is stopping, all other prebuild
|
|
// actions are blocked.
|
|
func TestBlockedOnStopActions(t *testing.T) {
|
|
outdated := opts[optionSet0]
|
|
current := opts[optionSet1]
|
|
|
|
// GIVEN: 2 presets, one outdated and one new (which now expects 2 prebuilds!).
|
|
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
|
preset(false, 1, outdated),
|
|
preset(true, 2, current),
|
|
}
|
|
|
|
// GIVEN: NO running prebuilds for either preset.
|
|
var running []database.GetRunningPrebuildsRow
|
|
|
|
// GIVEN: one prebuild for the old preset which is currently stopping.
|
|
inProgress := []database.GetPrebuildsInProgressRow{
|
|
{
|
|
TemplateID: outdated.templateID,
|
|
TemplateVersionID: outdated.templateVersionID,
|
|
Transition: database.WorkspaceTransitionStop,
|
|
Count: 1,
|
|
},
|
|
}
|
|
|
|
// WHEN: calculating the outdated preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(outdated.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: there is nothing to do.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{stopping: 1}, *actions)
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
ps, err = state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we are blocked from creating a new prebuild while another one is busy provisioning.
|
|
actions, err = ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{desired: 2, stopping: 1, create: 0}, *actions)
|
|
}
|
|
|
|
// A new template version is created with a preset with prebuilds configured; the outdated prebuilds are deleted,
|
|
// and one of the new prebuilds is already being provisioned, but we bail out early if operations are already in progress
|
|
// for this prebuild - to prevent clobbering.
|
|
func TestBlockedOnStartActions(t *testing.T) {
|
|
outdated := opts[optionSet0]
|
|
current := opts[optionSet1]
|
|
|
|
// GIVEN: 2 presets, one outdated and one new (which now expects 2 prebuilds!).
|
|
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
|
preset(false, 1, outdated),
|
|
preset(true, 2, current),
|
|
}
|
|
|
|
// GIVEN: NO running prebuilds for either preset.
|
|
var running []database.GetRunningPrebuildsRow
|
|
|
|
// GIVEN: one prebuild for the old preset which is currently provisioning.
|
|
inProgress := []database.GetPrebuildsInProgressRow{
|
|
{
|
|
TemplateID: current.templateID,
|
|
TemplateVersionID: current.templateVersionID,
|
|
Transition: database.WorkspaceTransitionStart,
|
|
Count: 1,
|
|
},
|
|
}
|
|
|
|
// WHEN: calculating the outdated preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(outdated.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: there is nothing to do.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{starting: 1}, *actions)
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
ps, err = state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: we are blocked from creating a new prebuild while another one is busy provisioning.
|
|
actions, err = ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{desired: 2, starting: 1, create: 0}, *actions)
|
|
}
|
|
|
|
// Additional prebuilds exist for a given preset configuration; these must be deleted.
|
|
func TestExtraneous(t *testing.T) {
|
|
current := opts[optionSet0]
|
|
|
|
// 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.GetRunningPrebuildsRow{
|
|
prebuild(current, func(row database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow {
|
|
// The older of the running prebuilds will be deleted in order to maintain freshness.
|
|
row.CreatedAt = time.Now().Add(-time.Hour)
|
|
older = row.WorkspaceID
|
|
return row
|
|
}),
|
|
prebuild(current, func(row database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow {
|
|
row.CreatedAt = time.Now()
|
|
return row
|
|
}),
|
|
}
|
|
|
|
// GIVEN: NO prebuilds in progress.
|
|
var inProgress []database.GetPrebuildsInProgressRow
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: an extraneous prebuild is detected and marked for deletion.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{
|
|
actual: 2, desired: 1, extraneous: 1, deleteIDs: []uuid.UUID{older}, eligible: 2,
|
|
}, *actions)
|
|
}
|
|
|
|
// As above, but no actions will be performed because
|
|
func TestExtraneousInProgress(t *testing.T) {
|
|
current := opts[optionSet0]
|
|
|
|
// 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.GetRunningPrebuildsRow{
|
|
prebuild(current, func(row database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow {
|
|
// The older of the running prebuilds will be deleted in order to maintain freshness.
|
|
row.CreatedAt = time.Now().Add(-time.Hour)
|
|
older = row.WorkspaceID
|
|
return row
|
|
}),
|
|
prebuild(current, func(row database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow {
|
|
row.CreatedAt = time.Now()
|
|
return row
|
|
}),
|
|
}
|
|
|
|
// GIVEN: NO prebuilds in progress.
|
|
var inProgress []database.GetPrebuildsInProgressRow
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: an extraneous prebuild is detected and marked for deletion.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{
|
|
actual: 2, desired: 1, extraneous: 1, deleteIDs: []uuid.UUID{older}, eligible: 2,
|
|
}, *actions)
|
|
}
|
|
|
|
// A template marked as deprecated will not have prebuilds running.
|
|
func TestDeprecated(t *testing.T) {
|
|
current := opts[optionSet0]
|
|
|
|
// 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.GetRunningPrebuildsRow{
|
|
prebuild(current),
|
|
}
|
|
|
|
// GIVEN: NO prebuilds in progress.
|
|
var inProgress []database.GetPrebuildsInProgressRow
|
|
|
|
// WHEN: calculating the current preset's state.
|
|
state := newReconciliationState(presets, running, inProgress)
|
|
ps, err := state.filterByPreset(current.presetID)
|
|
require.NoError(t, err)
|
|
|
|
// THEN: all running prebuilds should be deleted because the template is deprecated.
|
|
actions, err := ps.calculateActions()
|
|
require.NoError(t, err)
|
|
validateActions(t, reconciliationActions{
|
|
actual: 1, deleteIDs: []uuid.UUID{current.prebuildID}, eligible: 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,
|
|
PresetID: opts.presetID,
|
|
UsingActiveVersion: active,
|
|
Name: opts.presetName,
|
|
DesiredInstances: instances,
|
|
Deleted: false,
|
|
Deprecated: false,
|
|
}
|
|
|
|
for _, mut := range muts {
|
|
entry = mut(entry)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func prebuild(opts options, muts ...func(row database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow) database.GetRunningPrebuildsRow {
|
|
entry := database.GetRunningPrebuildsRow{
|
|
WorkspaceID: opts.prebuildID,
|
|
WorkspaceName: opts.workspaceName,
|
|
TemplateID: opts.templateID,
|
|
TemplateVersionID: opts.templateVersionID,
|
|
CurrentPresetID: uuid.NullUUID{UUID: opts.presetID, Valid: true},
|
|
Ready: true,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
for _, mut := range muts {
|
|
entry = mut(entry)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
// 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 reconciliationActions) bool {
|
|
return assert.EqualValuesf(t, expected.deleteIDs, actual.deleteIDs, "'deleteIDs' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.create, actual.create, "'create' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.desired, actual.desired, "'desired' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.actual, actual.actual, "'actual' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.eligible, actual.eligible, "'eligible' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.extraneous, actual.extraneous, "'extraneous' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.outdated, actual.outdated, "'outdated' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.starting, actual.starting, "'starting' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.stopping, actual.stopping, "'stopping' did not match expectation") &&
|
|
assert.EqualValuesf(t, expected.deleting, actual.deleting, "'deleting' did not match expectation")
|
|
}
|