mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add auto-locking/deleting workspace based on template config (#8240)
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
||||
"github.com/coder/coder/coderd/autobuild"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/schedule"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/enterprise/coderd"
|
||||
@ -93,7 +94,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
// builds.
|
||||
IgnoreErrors: true,
|
||||
})
|
||||
failureTTL = time.Millisecond
|
||||
failureTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
@ -106,11 +107,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
})
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
@ -124,12 +121,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||||
require.Eventually(t,
|
||||
func() bool {
|
||||
return database.Now().Sub(*build.Job.CompletedAt) > failureTTL
|
||||
},
|
||||
testutil.IntervalMedium, testutil.IntervalFast)
|
||||
ticker <- time.Now()
|
||||
ticker <- build.Job.CompletedAt.Add(failureTTL * 2)
|
||||
stats := <-statCh
|
||||
// Expect workspace to transition to stopped state for breaching
|
||||
// failure TTL.
|
||||
@ -161,11 +153,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
})
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
@ -178,13 +166,16 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||||
ticker <- time.Now()
|
||||
// Make it impossible to trigger the failure TTL.
|
||||
ticker <- build.Job.CompletedAt.Add(-failureTTL * 2)
|
||||
stats := <-statCh
|
||||
// Expect no transitions since not enough time has elapsed.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
})
|
||||
|
||||
t.Run("FailureTTLUnset", func(t *testing.T) {
|
||||
// This just provides a baseline that no actions are being taken
|
||||
// against a workspace when none of the TTL fields are set.
|
||||
t.Run("TemplateTTLsUnset", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
@ -207,25 +198,419 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
})
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionFailed,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
// Create a template without setting a failure_ttl.
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Zero(t, template.InactivityTTLMillis)
|
||||
require.Zero(t, template.FailureTTLMillis)
|
||||
require.Zero(t, template.LockedTTLMillis)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
ticker <- time.Now()
|
||||
stats := <-statCh
|
||||
// Expect no transitions since the field is unset on the template.
|
||||
// Expect no transitions since the fields are unset on the template.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
})
|
||||
|
||||
t.Run("InactiveTTLOK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = testutil.Context(t, testutil.WaitMedium)
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
inactiveTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
// Simulate being inactive.
|
||||
ticker <- ws.LastUsedAt.Add(inactiveTTL * 2)
|
||||
stats := <-statCh
|
||||
|
||||
// Expect workspace to transition to stopped state for breaching
|
||||
// failure TTL.
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop)
|
||||
|
||||
// The workspace should be locked.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.NotNil(t, ws.LockedAt)
|
||||
lastUsedAt := ws.LastUsedAt
|
||||
|
||||
err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{Lock: false})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that we updated our last_used_at so that we don't immediately
|
||||
// retrigger another lock action.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.True(t, ws.LastUsedAt.After(lastUsedAt))
|
||||
})
|
||||
|
||||
t.Run("InactiveTTLTooEarly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
inactiveTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
// Make it impossible to trigger the inactive ttl.
|
||||
ticker <- ws.LastUsedAt.Add(-inactiveTTL)
|
||||
stats := <-statCh
|
||||
// Expect no transitions since not enough time has elapsed.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
})
|
||||
|
||||
// This is kind of a dumb test but it exists to offer some marginal
|
||||
// confidence that a bug in the auto-deletion logic doesn't delete running
|
||||
// workspaces.
|
||||
t.Run("UnlockedWorkspacesNotDeleted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
lockedTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Nil(t, ws.LockedAt)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
ticker <- ws.LastUsedAt.Add(lockedTTL * 2)
|
||||
stats := <-statCh
|
||||
// Expect no transitions since workspace is unlocked.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
})
|
||||
|
||||
// Assert that a stopped workspace that breaches the inactivity threshold
|
||||
// does not trigger a build transition but is still placed in the
|
||||
// lock state.
|
||||
t.Run("InactiveStoppedWorkspaceNoTransition", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
inactiveTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
|
||||
// Stop the workspace so we can assert autobuild does nothing
|
||||
// if we breach our inactivity threshold.
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Simulate not having accessed the workspace in a while.
|
||||
ticker <- ws.LastUsedAt.Add(2 * inactiveTTL)
|
||||
stats := <-statCh
|
||||
// Expect no transitions since workspace is stopped.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
// The workspace should still be locked even though we didn't
|
||||
// transition the workspace.
|
||||
require.NotNil(t, ws.LockedAt)
|
||||
})
|
||||
|
||||
// Test the flow of a workspace transitioning from
|
||||
// inactive -> locked -> deleted.
|
||||
t.Run("WorkspaceInactiveDeleteTransition", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
transitionTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.InactivityTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
|
||||
ctr.LockedTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
|
||||
// Simulate not having accessed the workspace in a while.
|
||||
ticker <- ws.LastUsedAt.Add(2 * transitionTTL)
|
||||
stats := <-statCh
|
||||
// Expect workspace to transition to stopped state for breaching
|
||||
// inactive TTL.
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
// The workspace should be locked.
|
||||
require.NotNil(t, ws.LockedAt)
|
||||
|
||||
// Wait for the autobuilder to stop the workspace.
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Simulate the workspace being locked beyond the threshold.
|
||||
ticker <- ws.LockedAt.Add(2 * transitionTTL)
|
||||
stats = <-statCh
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
// The workspace should be scheduled for deletion.
|
||||
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionDelete)
|
||||
|
||||
// Wait for the workspace to be deleted.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Assert that the workspace is actually deleted.
|
||||
_, err := client.Workspace(testutil.Context(t, testutil.WaitShort), ws.ID)
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusGone, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("LockedTTTooEarly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
lockedTTL = time.Minute
|
||||
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{
|
||||
Lock: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.NotNil(t, ws.LockedAt)
|
||||
|
||||
// Ensure we haven't breached our threshold.
|
||||
ticker <- ws.LockedAt.Add(-lockedTTL * 2)
|
||||
stats := <-statCh
|
||||
// Expect no transitions since not enough time has elapsed.
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
|
||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
LockedTTLMillis: lockedTTL.Milliseconds(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simlute the workspace breaching the threshold.
|
||||
ticker <- ws.LockedAt.Add(lockedTTL * 2)
|
||||
stats = <-statCh
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Equal(t, database.WorkspaceTransitionDelete, stats.Transitions[ws.ID])
|
||||
})
|
||||
|
||||
// Assert that a locked workspace does not autostart.
|
||||
t.Run("LockedNoAutostart", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = testutil.Context(t, testutil.WaitMedium)
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
client = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: &coderd.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
})
|
||||
inactiveTTL = time.Minute
|
||||
)
|
||||
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddFullLicense(t, client)
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
sched, err := schedule.Weekly("CRON_TZ=UTC 0 * * * *")
|
||||
require.NoError(t, err)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Assert that autostart works when the workspace isn't locked..
|
||||
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
stats := <-statsCh
|
||||
require.NoError(t, stats.Error)
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Contains(t, stats.Transitions, ws.ID)
|
||||
require.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID])
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Now that we've validated that the workspace is eligible for autostart
|
||||
// lets cause it to become locked.
|
||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
InactivityTTLMillis: inactiveTTL.Milliseconds(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should see the workspace get stopped now.
|
||||
tickCh <- ws.LastUsedAt.Add(inactiveTTL * 2)
|
||||
stats = <-statsCh
|
||||
require.NoError(t, stats.Error)
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Contains(t, stats.Transitions, ws.ID)
|
||||
require.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[ws.ID])
|
||||
|
||||
// The workspace should be locked now.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
require.NotNil(t, ws.LockedAt)
|
||||
|
||||
// Assert that autostart is no longer triggered since workspace is locked.
|
||||
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
stats = <-statsCh
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user