mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: enforce template-level constraints for TTL and autostart (#2018)
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.
This commit is contained in:
@ -2,9 +2,7 @@ package executor_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -17,7 +15,6 @@ import (
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -26,8 +23,7 @@ func TestExecutorAutostartOK(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan executor.Stats)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
@ -35,22 +31,17 @@ func TestExecutorAutostartOK(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
// 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 = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks after the scheduled time
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -66,6 +57,7 @@ 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)
|
||||
@ -75,11 +67,13 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
// 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 = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
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())
|
||||
@ -92,16 +86,9 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||||
ID: newVersion.ID,
|
||||
}))
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks after the scheduled time
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -111,7 +98,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||||
assert.Len(t, stats.Transitions, 1)
|
||||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
||||
ws := mustWorkspace(t, client, 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")
|
||||
}
|
||||
|
||||
@ -119,8 +106,7 @@ func TestExecutorAutostartAlreadyRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan executor.Stats)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
@ -128,23 +114,18 @@ func TestExecutorAutostartAlreadyRunning(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
// 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: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -165,7 +146,7 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
// 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
|
||||
})
|
||||
@ -175,11 +156,11 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks way into the future
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -290,7 +271,7 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
|
||||
)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: the autobuild executor ticks past the TTL
|
||||
go func() {
|
||||
@ -308,6 +289,7 @@ 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{
|
||||
@ -315,15 +297,22 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace that has no TTL set
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.TTLMillis = nil
|
||||
})
|
||||
// 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)
|
||||
|
||||
@ -343,8 +332,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan executor.Stats)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
@ -352,23 +340,18 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
// 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())
|
||||
})
|
||||
)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
}))
|
||||
|
||||
// Given: workspace is deleted
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionDelete)
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionDelete)
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -382,8 +365,7 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan executor.Stats)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
@ -391,24 +373,17 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
|
||||
IncludeProvisionerD: true,
|
||||
AutobuildStats: statsCh,
|
||||
})
|
||||
futureTime = time.Now().Add(time.Hour)
|
||||
futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||||
// 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 = &futureTimeCron
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
)
|
||||
|
||||
// When: we enable workspace autostart with some time in the future
|
||||
sched, err := schedule.Weekly(futureTimeCron)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks before the next scheduled time
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC()
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(-time.Minute)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@ -487,6 +462,7 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
|
||||
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)
|
||||
@ -502,15 +478,17 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
|
||||
AutobuildStats: statsCh2,
|
||||
})
|
||||
// Given: we have a user with a workspace that has autostart enabled (default)
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
)
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks past the scheduled time
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh2 <- time.Now().UTC().Add(time.Minute)
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
tickCh2 <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||
close(tickCh)
|
||||
close(tickCh2)
|
||||
}()
|
||||
@ -536,41 +514,14 @@ func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, mut...)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
return mustWorkspace(t, client, ws.ID)
|
||||
return coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
}
|
||||
|
||||
func mustTransitionWorkspace(t *testing.T, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition) codersdk.Workspace {
|
||||
func mustSchedule(t *testing.T, s string) *schedule.Schedule {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
workspace, err := client.Workspace(ctx, workspaceID)
|
||||
require.NoError(t, err, "unexpected error fetching workspace")
|
||||
require.Equal(t, workspace.LatestBuild.Transition, codersdk.WorkspaceTransition(from), "expected workspace state: %s got: %s", from, workspace.LatestBuild.Transition)
|
||||
|
||||
template, err := client.Template(ctx, workspace.TemplateID)
|
||||
require.NoError(t, err, "fetch workspace template")
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Transition: codersdk.WorkspaceTransition(to),
|
||||
})
|
||||
require.NoError(t, err, "unexpected error transitioning workspace to %s", to)
|
||||
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
|
||||
updated := mustWorkspace(t, client, workspace.ID)
|
||||
require.Equal(t, codersdk.WorkspaceTransition(to), updated.LatestBuild.Transition, "expected workspace to be in state %s but got %s", to, updated.LatestBuild.Transition)
|
||||
return updated
|
||||
}
|
||||
|
||||
func mustWorkspace(t *testing.T, client *codersdk.Client, workspaceID uuid.UUID) codersdk.Workspace {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ws, err := client.Workspace(ctx, workspaceID)
|
||||
if err != nil && strings.Contains(err.Error(), "status code 410") {
|
||||
ws, err = client.DeletedWorkspace(ctx, workspaceID)
|
||||
}
|
||||
require.NoError(t, err, "no workspace found with id %s", workspaceID)
|
||||
return ws
|
||||
sched, err := schedule.Weekly(s)
|
||||
require.NoError(t, err)
|
||||
return sched
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
Reference in New Issue
Block a user