mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
This PR adds fields to templates that constrain values for workspaces derived from that template. - Autostop: Adds a field max_ttl on the template which limits the maximum value of ttl on all workspaces derived from that template. Defaulting to 168 hours, enforced on edits to workspace metadata. New workspaces will default to the templates's `max_ttl` if not specified. - Autostart: Adds a field min_autostart_duration which limits the minimum duration between successive autostarts of a template, measured from a single reference time. Defaulting to 1 hour, enforced on edits to workspace metadata.
530 lines
17 KiB
Go
530 lines
17 KiB
Go
package executor_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/goleak"
|
|
|
|
"github.com/coder/coder/coderd/autobuild/executor"
|
|
"github.com/coder/coder/coderd/autobuild/schedule"
|
|
"github.com/coder/coder/coderd/coderdtest"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/util/ptr"
|
|
"github.com/coder/coder/codersdk"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestExecutorAutostartOK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace that has autostart enabled
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
// Given: workspace is stopped
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// When: the autobuild executor ticks after the scheduled time
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should eventually be started
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 1)
|
|
assert.Contains(t, stats.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
|
}
|
|
|
|
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
ctx = context.Background()
|
|
err error
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace that has autostart enabled
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
// Given: workspace is stopped
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// Given: the workspace template has been updated
|
|
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
|
|
require.NoError(t, err)
|
|
require.Len(t, orgs, 1)
|
|
|
|
newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, nil, workspace.TemplateID)
|
|
coderdtest.AwaitTemplateVersionJob(t, client, newVersion.ID)
|
|
require.NoError(t, client.UpdateActiveTemplateVersion(ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
|
|
ID: newVersion.ID,
|
|
}))
|
|
|
|
// When: the autobuild executor ticks after the scheduled time
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should be started using the previous template version, and not the updated version.
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 1)
|
|
assert.Contains(t, stats.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
|
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
|
|
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID, "expected workspace build to be using the old template version")
|
|
}
|
|
|
|
func TestExecutorAutostartAlreadyRunning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace that has autostart enabled
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
|
|
// Given: we ensure the workspace is running
|
|
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
|
|
|
// When: the autobuild executor ticks
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should not be started.
|
|
stats := <-statsCh
|
|
require.NoError(t, stats.Error)
|
|
require.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorAutostartNotEnabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace that does not have autostart enabled
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
})
|
|
)
|
|
|
|
// Given: workspace does not have autostart enabled
|
|
require.Empty(t, workspace.AutostartSchedule)
|
|
|
|
// Given: workspace is stopped
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// When: the autobuild executor ticks way into the future
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should not be started.
|
|
stats := <-statsCh
|
|
require.NoError(t, stats.Error)
|
|
require.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorAutostopOK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace
|
|
workspace = mustProvisionWorkspace(t, client)
|
|
)
|
|
// Given: workspace is running
|
|
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
|
require.NotZero(t, workspace.LatestBuild.Deadline)
|
|
|
|
// When: the autobuild executor ticks *after* the deadline:
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should be stopped
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 1)
|
|
assert.Contains(t, stats.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
|
}
|
|
|
|
func TestExecutorAutostopExtend(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace
|
|
workspace = mustProvisionWorkspace(t, client)
|
|
originalDeadline = workspace.LatestBuild.Deadline
|
|
)
|
|
// Given: workspace is running
|
|
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
|
require.NotZero(t, originalDeadline)
|
|
|
|
// Given: we extend the workspace deadline
|
|
newDeadline := originalDeadline.Add(30 * time.Minute)
|
|
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: newDeadline,
|
|
})
|
|
require.NoError(t, err, "extend workspace deadline")
|
|
|
|
// When: the autobuild executor ticks *after* the original deadline:
|
|
go func() {
|
|
tickCh <- originalDeadline.Add(time.Minute)
|
|
}()
|
|
|
|
// Then: nothing should happen and the workspace should stay running
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
|
|
// When: the autobuild executor ticks after the *new* deadline:
|
|
go func() {
|
|
tickCh <- newDeadline.Add(time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should be stopped
|
|
stats = <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 1)
|
|
assert.Contains(t, stats.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
|
}
|
|
|
|
func TestExecutorAutostopAlreadyStopped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace (disabling autostart)
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
})
|
|
)
|
|
|
|
// Given: workspace is stopped
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// When: the autobuild executor ticks past the TTL
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should remain stopped and no build should happen.
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorAutostopNotEnabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace
|
|
workspace = mustProvisionWorkspace(t, client)
|
|
)
|
|
|
|
// Given: workspace has no TTL set
|
|
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
|
|
require.NoError(t, err)
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, workspace.TTLMillis)
|
|
|
|
// TODO(cian): need to stop and start the workspace as we do not update the deadline yet
|
|
// see: https://github.com/coder/coder/issues/1783
|
|
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
|
|
|
|
// Given: workspace is running
|
|
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
|
|
|
// When: the autobuild executor ticks past the TTL
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should not be stopped.
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorWorkspaceDeleted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace that has autostart enabled
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
|
|
// Given: workspace is deleted
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionDelete)
|
|
|
|
// When: the autobuild executor ticks
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: nothing should happen
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// futureTime = time.Now().Add(time.Hour)
|
|
// futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
|
// Given: we have a user with a workspace configured to autostart some time in the future
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
|
|
// When: the autobuild executor ticks before the next scheduled time
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(-time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: nothing should happen
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace
|
|
workspace = mustProvisionWorkspace(t, client)
|
|
)
|
|
|
|
// When: the autobuild executor ticks before the TTL
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.Deadline.Add(-1 * time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: nothing should happen
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 0)
|
|
}
|
|
|
|
func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
// Given: we have a user with a workspace
|
|
workspace = mustProvisionWorkspace(t, client)
|
|
)
|
|
|
|
// Given: the user changes their mind and decides their workspace should not auto-stop
|
|
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
|
|
require.NoError(t, err)
|
|
|
|
// When: the autobuild executor ticks after the deadline
|
|
go func() {
|
|
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Then: the workspace should still stop - sorry!
|
|
stats := <-statsCh
|
|
assert.NoError(t, stats.Error)
|
|
assert.Len(t, stats.Transitions, 1)
|
|
assert.Contains(t, stats.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
|
}
|
|
|
|
func TestExecutorAutostartMultipleOK(t *testing.T) {
|
|
if os.Getenv("DB") == "" {
|
|
t.Skip(`This test only really works when using a "real" database, similar to a HA setup`)
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
var (
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
tickCh2 = make(chan time.Time)
|
|
statsCh1 = make(chan executor.Stats)
|
|
statsCh2 = make(chan executor.Stats)
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh1,
|
|
})
|
|
_ = coderdtest.New(t, &coderdtest.Options{
|
|
AutobuildTicker: tickCh2,
|
|
IncludeProvisionerD: true,
|
|
AutobuildStats: statsCh2,
|
|
})
|
|
// Given: we have a user with a workspace that has autostart enabled (default)
|
|
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
)
|
|
// Given: workspace is stopped
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// When: the autobuild executor ticks past the scheduled time
|
|
go func() {
|
|
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
tickCh2 <- sched.Next(workspace.LatestBuild.CreatedAt)
|
|
close(tickCh)
|
|
close(tickCh2)
|
|
}()
|
|
|
|
// Then: the workspace should eventually be started
|
|
stats1 := <-statsCh1
|
|
assert.NoError(t, stats1.Error)
|
|
assert.Len(t, stats1.Transitions, 1)
|
|
assert.Contains(t, stats1.Transitions, workspace.ID)
|
|
assert.Equal(t, database.WorkspaceTransitionStart, stats1.Transitions[workspace.ID])
|
|
|
|
// Then: the other executor should not have done anything
|
|
stats2 := <-statsCh2
|
|
assert.NoError(t, stats2.Error)
|
|
assert.Len(t, stats2.Transitions, 0)
|
|
}
|
|
|
|
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
|
t.Helper()
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
|
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, mut...)
|
|
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
|
return coderdtest.MustWorkspace(t, client, ws.ID)
|
|
}
|
|
|
|
func mustSchedule(t *testing.T, s string) *schedule.Schedule {
|
|
t.Helper()
|
|
sched, err := schedule.Weekly(s)
|
|
require.NoError(t, err)
|
|
return sched
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
goleak.VerifyTestMain(m)
|
|
}
|