Skip to content

feat: add customizable ttl_bump interval #10566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ test-postgres-docker:
--name test-postgres-docker \
--restart no \
--detach \
-v ~/.coder/data:/var/lib/postgresql/data \
gcr.io/coder-dev-1/postgres:13 \
-c shared_buffers=1GB \
-c work_mem=1GB \
Expand Down
20 changes: 16 additions & 4 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import (

func (r *RootCmd) create() *clibase.Cmd {
var (
templateName string
startAt string
stopAfter time.Duration
workspaceName string
templateName string
startAt string
stopAfter time.Duration
defaultTTLBump time.Duration
workspaceName string

parameterFlags workspaceParameterFlags
autoUpdates string
Expand Down Expand Up @@ -199,12 +200,17 @@ func (r *RootCmd) create() *clibase.Cmd {
if stopAfter > 0 {
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
}
var ttlBumpMillis *int64
if defaultTTLBump.Milliseconds() > 0 {
ttlBumpMillis = ptr.Ref(defaultTTLBump.Milliseconds())
}

workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
TemplateVersionID: templateVersionID,
Name: workspaceName,
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
TTLBumpMillis: ttlBumpMillis,
RichParameterValues: richParameters,
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
})
Expand Down Expand Up @@ -246,6 +252,12 @@ func (r *RootCmd) create() *clibase.Cmd {
Description: "Specify a duration after which the workspace should shut down (e.g. 8h).",
Value: clibase.DurationOf(&stopAfter),
},
clibase.Option{
Flag: "activity-bump",
Description: "Specify a default amount of time to bump the deadline for the workspaces based on workspace activity. By default, activity will extend the deadline for a workspace by the 'stop-after' amount.",
Default: "0",
Value: clibase.DurationOf(&defaultTTLBump),
},
clibase.Option{
Flag: "automatic-updates",
Env: "CODER_WORKSPACE_AUTOMATIC_UPDATES",
Expand Down
2 changes: 2 additions & 0 deletions cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestCreate(t *testing.T) {
"--template", template.Name,
"--start-at", "9:30AM Mon-Fri US/Central",
"--stop-after", "8h",
"--activity-bump", "1h",
"--automatic-updates", "always",
}
inv, root := clitest.New(t, args...)
Expand Down Expand Up @@ -75,6 +76,7 @@ func TestCreate(t *testing.T) {
if assert.NotNil(t, ws.TTLMillis) {
assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
}
assert.Equal(t, ws.TTLBumpMillis, 1*time.Hour.Milliseconds())
assert.Equal(t, codersdk.AutomaticUpdatesAlways, ws.AutomaticUpdates)
}
})
Expand Down
16 changes: 12 additions & 4 deletions cli/templatecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
disableEveryone bool
requireActiveVersion bool

defaultTTL time.Duration
failureTTL time.Duration
inactivityTTL time.Duration
maxTTL time.Duration
defaultTTL time.Duration
defaultTTLBump time.Duration
failureTTL time.Duration
inactivityTTL time.Duration
maxTTL time.Duration

uploadFlags templateUploadFlags
)
Expand Down Expand Up @@ -156,6 +157,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
Name: templateName,
VersionID: job.ID,
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
DefaultTTLBumpMillis: ptr.Ref(defaultTTLBump.Milliseconds()),
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
Expand Down Expand Up @@ -213,6 +215,12 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
Default: "24h",
Value: clibase.DurationOf(&defaultTTL),
},
{
Flag: "default-activity-bump",
Description: "Specify a default amount of time to bump the deadline for the workspaces based on workspace activity. By default, activity will extend the deadline for a workspace by the 'default-ttl' amount.",
Default: "0",
Value: clibase.DurationOf(&defaultTTLBump),
},
{
Flag: "failure-ttl",
Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\"in the UI.",
Expand Down
18 changes: 15 additions & 3 deletions cli/templatecreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
Expand Down Expand Up @@ -72,8 +74,9 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
coderdtest.CreateFirstUser(t, client)
rootClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, rootClient)
client, _ := coderdtest.CreateAnotherUser(t, rootClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
args := []string{
"templates",
Expand All @@ -82,12 +85,13 @@ func TestTemplateCreate(t *testing.T) {
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--default-ttl", "24h",
"--default-activity-bump", "1h",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)

clitest.Start(t, inv)
waitor := clitest.StartWithWaiter(t, inv)

matches := []struct {
match string
Expand All @@ -104,6 +108,14 @@ func TestTemplateCreate(t *testing.T) {
pty.WriteLine(m.write)
}
}
waitor.RequireSuccess()

ctx := testutil.Context(t, testutil.WaitShort)
tpl, err := client.TemplateByName(ctx, owner.OrganizationID, "my-template")
require.NoError(t, err)

require.EqualValues(t, (24 * time.Hour).Milliseconds(), tpl.DefaultTTLMillis)
require.EqualValues(t, time.Hour.Milliseconds(), tpl.DefaultTTLBumpMillis)
})
t.Run("CreateNoLockfile", func(t *testing.T) {
t.Parallel()
Expand Down
20 changes: 14 additions & 6 deletions cli/templateedit.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
description string
icon string
defaultTTL time.Duration
defaultTTLBump time.Duration
maxTTL time.Duration
autostopRequirementDaysOfWeek []string
autostopRequirementWeeks int64
Expand Down Expand Up @@ -120,12 +121,13 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {

// NOTE: coderd will ignore empty fields.
req := codersdk.UpdateTemplateMeta{
Name: name,
DisplayName: displayName,
Description: description,
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
MaxTTLMillis: maxTTL.Milliseconds(),
Name: name,
DisplayName: displayName,
Description: description,
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
DefaultTTLBumpMillis: defaultTTLBump.Milliseconds(),
MaxTTLMillis: maxTTL.Milliseconds(),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: autostopRequirementDaysOfWeek,
Weeks: autostopRequirementWeeks,
Expand Down Expand Up @@ -176,6 +178,12 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Description: "Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Value: clibase.DurationOf(&defaultTTL),
},
{
Flag: "default-activity-bump",
Description: "Specify a default amount of time to bump the deadline for the workspaces based on workspace activity. By default, activity will extend the deadline for a workspace by the 'default-ttl' amount.",
Default: "0",
Value: clibase.DurationOf(&defaultTTLBump),
},
{
Flag: "max-ttl",
Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to \"Max lifetime\" in the UI.",
Expand Down
3 changes: 3 additions & 0 deletions cli/templateedit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestTemplateEdit(t *testing.T) {
desc := "lorem ipsum dolor sit amet et cetera"
icon := "/icon/new-icon.png"
defaultTTL := 12 * time.Hour
defaultTTLBump := time.Hour
allowUserCancelWorkspaceJobs := false

cmdArgs := []string{
Expand All @@ -55,6 +56,7 @@ func TestTemplateEdit(t *testing.T) {
"--description", desc,
"--icon", icon,
"--default-ttl", defaultTTL.String(),
"--default-activity-bump", defaultTTLBump.String(),
"--allow-user-cancel-workspace-jobs=" + strconv.FormatBool(allowUserCancelWorkspaceJobs),
}
inv, root := clitest.New(t, cmdArgs...)
Expand All @@ -73,6 +75,7 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, desc, updated.Description)
assert.Equal(t, icon, updated.Icon)
assert.Equal(t, defaultTTL.Milliseconds(), updated.DefaultTTLMillis)
assert.Equal(t, defaultTTLBump.Milliseconds(), updated.DefaultTTLBumpMillis)
assert.Equal(t, allowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs)
})
t.Run("FirstEmptyThenNotModified", func(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion cli/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type templateTableRow struct {
ActiveVersionID uuid.UUID `json:"-" table:"active version id"`
UsedBy string `json:"-" table:"used by"`
DefaultTTL time.Duration `json:"-" table:"default ttl"`
DefaultTTLBump time.Duration `json:"-" table:"default ttl bump"`
}

// templateToRows converts a list of templates to a list of templateTableRow for
Expand All @@ -111,7 +112,8 @@ func templatesToRows(templates ...codersdk.Template) []templateTableRow {
Provisioner: template.Provisioner,
ActiveVersionID: template.ActiveVersionID,
UsedBy: pretty.Sprint(cliui.DefaultStyles.Fuchsia, formatActiveDevelopers(template.ActiveUserCount)),
DefaultTTL: (time.Duration(template.DefaultTTLMillis) * time.Millisecond),
DefaultTTL: time.Duration(template.DefaultTTLMillis) * time.Millisecond,
DefaultTTLBump: time.Duration(template.DefaultTTLBumpMillis) * time.Millisecond,
}
}

Expand Down
5 changes: 5 additions & 0 deletions cli/testdata/coder_create_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ USAGE:
$ coder create <username>/<workspace_name>

OPTIONS:
--activity-bump duration (default: 0)
Specify a default amount of time to bump the deadline for the
workspaces based on workspace activity. By default, activity will
extend the deadline for a workspace by the 'stop-after' amount.

--automatic-updates string, $CODER_WORKSPACE_AUTOMATIC_UPDATES (default: never)
Specify automatic updates setting for the workspace (accepts 'always'
or 'never').
Expand Down
5 changes: 5 additions & 0 deletions cli/testdata/coder_templates_create_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ USAGE:
Create a template from the current directory or as specified by flag

OPTIONS:
--default-activity-bump duration (default: 0)
Specify a default amount of time to bump the deadline for the
workspaces based on workspace activity. By default, activity will
extend the deadline for a workspace by the 'default-ttl' amount.

--default-ttl duration (default: 24h)
Specify a default TTL for workspaces created from this template. It is
the default time before shutdown - workspaces created from this
Expand Down
5 changes: 5 additions & 0 deletions cli/testdata/coder_templates_edit_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ OPTIONS:
this value for the template (and allow autostart on all days), pass
'all'.

--default-activity-bump duration (default: 0)
Specify a default amount of time to bump the deadline for the
workspaces based on workspace activity. By default, activity will
extend the deadline for a workspace by the 'default-ttl' amount.

--default-ttl duration
Edit the template default time before shutdown - workspaces created
from this template default to this value. Maps to "Default autostop"
Expand Down
2 changes: 1 addition & 1 deletion cli/testdata/coder_templates_list_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ OPTIONS:
-c, --column string-array (default: name,last updated,used by)
Columns to display in table output. Available columns: name, created
at, last updated, organization id, provisioner, active version id,
used by, default ttl.
used by, default ttl, default ttl bump.

-o, --output string (default: table)
Output format. Available formats: table, json.
Expand Down
42 changes: 40 additions & 2 deletions coderd/activitybump_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
buildDeadlineOffset *time.Duration
maxDeadlineOffset *time.Duration
workspaceTTL time.Duration
workspaceBumpTTL time.Duration
templateTTL time.Duration
templateBumpTTL time.Duration
templateDisallowsUserAutostop bool
expectedBump time.Duration
}{
Expand Down Expand Up @@ -83,6 +85,16 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
workspaceTTL: 8 * time.Hour,
expectedBump: 1 * time.Hour,
},
{
name: "MaxDeadlineCustomBump",
transition: database.WorkspaceTransitionStart,
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)},
buildDeadlineOffset: ptr.Ref(time.Minute), // last chance to bump!
maxDeadlineOffset: ptr.Ref(4 * time.Hour),
workspaceTTL: 8 * time.Hour,
workspaceBumpTTL: 5 * time.Hour,
expectedBump: 5 * time.Hour,
},
{
// A workspace that is still running, has passed its deadline, but has not
// yet been auto-stopped should still bump the deadline.
Expand All @@ -101,6 +113,17 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
buildDeadlineOffset: ptr.Ref(-time.Minute),
workspaceTTL: 8 * time.Hour,
},
{
// Deadline bump TTL can be less than the time left in the
// deadline. It should be a no-op.
name: "DeadlineNotDecreased",
transition: database.WorkspaceTransitionStart,
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Hour)},
buildDeadlineOffset: ptr.Ref(time.Hour * 5),
workspaceTTL: 8 * time.Hour,
workspaceBumpTTL: time.Hour,
expectedBump: 0,
},
{
// A workspace built from a template that disallows user autostop should bump
// by the template TTL instead.
Expand All @@ -113,6 +136,18 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
templateDisallowsUserAutostop: true,
expectedBump: 8 * time.Hour,
},
{
name: "TemplateDisallowsUserAutostopCustomBump",
transition: database.WorkspaceTransitionStart,
templateDisallowsUserAutostop: true,
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-7 * time.Hour)},
buildDeadlineOffset: ptr.Ref(time.Minute),
templateTTL: time.Hour * 8,
templateBumpTTL: time.Hour,
workspaceTTL: 24 * time.Hour,
workspaceBumpTTL: 10 * time.Hour,
expectedBump: time.Hour, // Expect smaller bump
},
} {
tt := tt
for _, tz := range timezones {
Expand Down Expand Up @@ -147,6 +182,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
OrganizationID: org.ID,
TemplateID: template.ID,
Ttl: sql.NullInt64{Valid: true, Int64: int64(tt.workspaceTTL)},
TtlBump: int64(tt.workspaceBumpTTL),
})
job = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Expand All @@ -163,6 +199,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
UpdatedAt: dbtime.Now(),
AllowUserAutostop: !tt.templateDisallowsUserAutostop,
DefaultTTL: int64(tt.templateTTL),
DefaultTTLBump: int64(tt.templateBumpTTL),
}), "unexpected error updating template schedule")

var buildNumber int32 = 1
Expand Down Expand Up @@ -207,6 +244,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
require.Equal(t, buildDeadline.UTC(), bld.Deadline.UTC(), "unexpected build deadline before bump")
require.Equal(t, maxDeadline.UTC(), bld.MaxDeadline.UTC(), "unexpected max deadline before bump")
require.Equal(t, tt.workspaceTTL, time.Duration(ws.Ttl.Int64), "unexpected workspace TTL before bump")
require.Equal(t, tt.workspaceBumpTTL, time.Duration(ws.TtlBump), "unexpected workspace TTL bump")

// Wait a bit before bumping as dbtime is rounded to the nearest millisecond.
// This should also hopefully be enough for Windows time resolution to register
Expand Down Expand Up @@ -236,8 +274,8 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
// Assert that the bump occurred between start and end.
expectedDeadlineStart := start.Add(tt.expectedBump)
expectedDeadlineEnd := end.Add(tt.expectedBump)
require.GreaterOrEqual(t, updatedBuild.Deadline, expectedDeadlineStart, "new deadline should be greater than or equal to start")
require.LessOrEqual(t, updatedBuild.Deadline, expectedDeadlineEnd, "new deadline should be lesser than or equal to end")
require.GreaterOrEqualf(t, updatedBuild.Deadline, expectedDeadlineStart, "new deadline should be greater than or equal to start, pre-bump: %s, diff_from_last_deadline: %s", buildDeadline.String(), updatedBuild.Deadline.Sub(buildDeadline))
require.LessOrEqualf(t, updatedBuild.Deadline, expectedDeadlineEnd, "new deadline should be lesser than or equal to end, pre-bump: %s, diff_from_last_deadline: %s", buildDeadline.String(), updatedBuild.Deadline.Sub(buildDeadline))
})
}
}
Expand Down
Loading