From 3980007b48d2067cd01ba52652f63c93905689b4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 18 May 2022 20:48:38 +0000 Subject: [PATCH 01/15] refactor: workspace: autostop_schedule -> ttl --- cli/autostop.go | 167 ------------------ cli/list.go | 8 +- cli/root.go | 2 +- cli/ssh.go | 10 +- cli/ttl.go | 131 ++++++++++++++ cli/{autostop_test.go => ttl_test.go} | 66 ++----- coderd/audit/diff.go | 20 +++ coderd/audit/diff_test.go | 6 +- coderd/audit/table.go | 2 +- .../autobuild/executor/lifecycle_executor.go | 23 +-- .../executor/lifecycle_executor_test.go | 34 ++-- coderd/coderd.go | 2 +- coderd/database/databasefake/databasefake.go | 8 +- coderd/database/dump.sql | 2 +- .../999999_rename_this_autostop_ttl.down.sql | 2 + .../999999_rename_this_autostop_ttl.up.sql | 2 + coderd/database/models.go | 2 +- coderd/database/querier.go | 4 +- coderd/database/queries.sql.go | 66 +++---- coderd/database/queries/workspaces.sql | 10 +- coderd/workspaces.go | 37 ++-- coderd/workspaces_test.go | 112 +++--------- codersdk/workspaces.go | 12 +- 23 files changed, 311 insertions(+), 417 deletions(-) delete mode 100644 cli/autostop.go create mode 100644 cli/ttl.go rename cli/{autostop_test.go => ttl_test.go} (58%) create mode 100644 coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql create mode 100644 coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql diff --git a/cli/autostop.go b/cli/autostop.go deleted file mode 100644 index 5805b4516ecac..0000000000000 --- a/cli/autostop.go +++ /dev/null @@ -1,167 +0,0 @@ -package cli - -import ( - "fmt" - "os" - "time" - - "github.com/spf13/cobra" - - "github.com/coder/coder/coderd/autobuild/schedule" - "github.com/coder/coder/codersdk" -) - -const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop. -When enabling autostop, provide the minute, hour, and day(s) of week. -The default autostop schedule is at 18:00 in your local timezone (TZ env, UTC by default). -` - -func autostop() *cobra.Command { - autostopCmd := &cobra.Command{ - Annotations: workspaceCommand, - Use: "autostop enable ", - Short: "schedule a workspace to automatically stop at a regular time", - Long: autostopDescriptionLong, - Example: "coder autostop enable my-workspace --minute 0 --hour 18 --days 1-5 -tz Europe/Dublin", - } - - autostopCmd.AddCommand(autostopShow()) - autostopCmd.AddCommand(autostopEnable()) - autostopCmd.AddCommand(autostopDisable()) - - return autostopCmd -} - -func autostopShow() *cobra.Command { - cmd := &cobra.Command{ - Use: "show ", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - organization, err := currentOrganization(cmd, client) - if err != nil { - return err - } - - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) - if err != nil { - return err - } - - if workspace.AutostopSchedule == "" { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not enabled\n") - return nil - } - - validSchedule, err := schedule.Weekly(workspace.AutostopSchedule) - if err != nil { - // This should never happen. - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "invalid autostop schedule %q for workspace %s: %s\n", workspace.AutostopSchedule, workspace.Name, err.Error()) - return nil - } - - next := validSchedule.Next(time.Now()) - loc, _ := time.LoadLocation(validSchedule.Timezone()) - - _, _ = fmt.Fprintf(cmd.OutOrStdout(), - "schedule: %s\ntimezone: %s\nnext: %s\n", - validSchedule.Cron(), - validSchedule.Timezone(), - next.In(loc), - ) - - return nil - }, - } - return cmd -} - -func autostopEnable() *cobra.Command { - // yes some of these are technically numbers but the cron library will do that work - var autostopMinute string - var autostopHour string - var autostopDayOfWeek string - var autostopTimezone string - cmd := &cobra.Command{ - Use: "enable ", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - organization, err := currentOrganization(cmd, client) - if err != nil { - return err - } - - spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostopTimezone, autostopMinute, autostopHour, autostopDayOfWeek) - validSchedule, err := schedule.Weekly(spec) - if err != nil { - return err - } - - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) - if err != nil { - return err - } - - err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: validSchedule.String(), - }) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically stop at %s.\n\n", workspace.Name, validSchedule.Next(time.Now())) - - return nil - }, - } - - cmd.Flags().StringVar(&autostopMinute, "minute", "0", "autostop minute") - cmd.Flags().StringVar(&autostopHour, "hour", "18", "autostop hour") - cmd.Flags().StringVar(&autostopDayOfWeek, "days", "1-5", "autostop day(s) of week") - tzEnv := os.Getenv("TZ") - if tzEnv == "" { - tzEnv = "UTC" - } - cmd.Flags().StringVar(&autostopTimezone, "tz", tzEnv, "autostop timezone") - return cmd -} - -func autostopDisable() *cobra.Command { - return &cobra.Command{ - Use: "disable ", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - organization, err := currentOrganization(cmd, client) - if err != nil { - return err - } - - workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) - if err != nil { - return err - } - - err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: "", - }) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically stop.\n\n", workspace.Name) - - return nil - }, - } -} diff --git a/cli/list.go b/cli/list.go index 4d4b95fc6bc93..9abc68e9dff5c 100644 --- a/cli/list.go +++ b/cli/list.go @@ -49,7 +49,7 @@ func list() *cobra.Command { } tableWriter := cliui.Table() - header := table.Row{"workspace", "template", "status", "last built", "outdated", "autostart", "autostop"} + header := table.Row{"workspace", "template", "status", "last built", "outdated", "autostart", "ttl"} tableWriter.AppendHeader(header) tableWriter.SortBy([]table.SortBy{{ Name: "workspace", @@ -116,10 +116,8 @@ func list() *cobra.Command { } autostopDisplay := "-" - if workspace.AutostopSchedule != "" { - if sched, err := schedule.Weekly(workspace.AutostopSchedule); err == nil { - autostopDisplay = sched.Cron() - } + if workspace.TTL != nil { + autostopDisplay = workspace.TTL.String() } user := usersByID[workspace.OwnerID] diff --git a/cli/root.go b/cli/root.go index 424ec54155c03..c2c9a9d47c419 100644 --- a/cli/root.go +++ b/cli/root.go @@ -62,7 +62,6 @@ func Root() *cobra.Command { cmd.AddCommand( autostart(), - autostop(), configSSH(), create(), delete(), @@ -78,6 +77,7 @@ func Root() *cobra.Command { stop(), ssh(), templates(), + ttl(), update(), users(), portForward(), diff --git a/cli/ssh.go b/cli/ssh.go index 119d28446611c..ed8b7c335b0ea 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -21,7 +21,6 @@ import ( "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/autobuild/notify" - "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" ) @@ -270,16 +269,11 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u return time.Time{}, nil } - if ws.AutostopSchedule == "" { + if ws.TTL == nil { return time.Time{}, nil } - sched, err := schedule.Weekly(ws.AutostopSchedule) - if err != nil { - return time.Time{}, nil - } - - deadline = sched.Next(now) + deadline = now.Add(*ws.TTL) callback = func() { ttl := deadline.Sub(now) var title, body string diff --git a/cli/ttl.go b/cli/ttl.go new file mode 100644 index 0000000000000..a8a4ee874e8b4 --- /dev/null +++ b/cli/ttl.go @@ -0,0 +1,131 @@ +package cli + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/coder/coder/codersdk" +) + +const ttlDescriptionLong = `To have your workspace stop automatically after a configurable interval has passed.` + +func ttl() *cobra.Command { + ttlCmd := &cobra.Command{ + Annotations: workspaceCommand, + Use: "ttl enable ", + Short: "schedule a workspace to automatically stop after a configurable interval", + Long: ttlDescriptionLong, + Example: "coder ttl enable my-workspace 8h30m", + } + + ttlCmd.AddCommand(ttlShow()) + ttlCmd.AddCommand(ttlEnable()) + ttlCmd.AddCommand(ttlDisable()) + + return ttlCmd +} + +func ttlShow() *cobra.Command { + cmd := &cobra.Command{ + Use: "show ", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := createClient(cmd) + if err != nil { + return err + } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + if err != nil { + return err + } + + if workspace.TTL == nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not enabled\n") + return nil + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", workspace.TTL) + + return nil + }, + } + return cmd +} + +func ttlEnable() *cobra.Command { + cmd := &cobra.Command{ + Use: "enable ", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := createClient(cmd) + if err != nil { + return err + } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + if err != nil { + return err + } + + ttl, err := time.ParseDuration(args[1]) + if err != nil { + return err + } + + err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: &ttl, + }) + if err != nil { + return err + } + + return nil + }, + } + + return cmd +} + +func ttlDisable() *cobra.Command { + return &cobra.Command{ + Use: "disable ", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := createClient(cmd) + if err != nil { + return err + } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + + workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) + if err != nil { + return err + } + + err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: nil, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically stop.\n\n", workspace.Name) + + return nil + }, + } +} diff --git a/cli/autostop_test.go b/cli/ttl_test.go similarity index 58% rename from cli/autostop_test.go rename to cli/ttl_test.go index 14447ac037ee4..b2e4419f788a3 100644 --- a/cli/autostop_test.go +++ b/cli/ttl_test.go @@ -3,9 +3,9 @@ package cli_test import ( "bytes" "context" - "fmt" - "os" + "strings" "testing" + "time" "github.com/stretchr/testify/require" @@ -14,7 +14,7 @@ import ( "github.com/coder/coder/codersdk" ) -func TestAutostop(t *testing.T) { +func TestTTL(t *testing.T) { t.Parallel() t.Run("ShowOK", func(t *testing.T) { @@ -29,13 +29,13 @@ func TestAutostop(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) - cmdArgs = []string{"autostop", "show", workspace.Name} - sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5" + cmdArgs = []string{"ttl", "show", workspace.Name} + ttl = 8 * time.Hour stdoutBuf = &bytes.Buffer{} ) - err := client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: sched, + err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: &ttl, }) require.NoError(t, err) @@ -45,8 +45,7 @@ func TestAutostop(t *testing.T) { err = cmd.Execute() require.NoError(t, err, "unexpected error") - // CRON_TZ gets stripped - require.Contains(t, stdoutBuf.String(), "schedule: 30 17 * * 1-5") + require.Equal(t, strings.TrimSpace(stdoutBuf.String()), ttl.String()) }) t.Run("EnableDisableOK", func(t *testing.T) { @@ -61,8 +60,8 @@ func TestAutostop(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) - cmdArgs = []string{"autostop", "enable", workspace.Name, "--minute", "30", "--hour", "17", "--days", "1-5", "--tz", "Europe/Dublin"} - sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5" + ttl = 8*time.Hour + 30*time.Minute + cmdArgs = []string{"ttl", "enable", workspace.Name, ttl.String()} stdoutBuf = &bytes.Buffer{} ) @@ -72,26 +71,24 @@ func TestAutostop(t *testing.T) { err := cmd.Execute() require.NoError(t, err, "unexpected error") - require.Contains(t, stdoutBuf.String(), "will automatically stop at", "unexpected output") // Ensure autostop schedule updated updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Equal(t, sched, updated.AutostopSchedule, "expected autostop schedule to be set") + require.Equal(t, *updated.TTL, ttl) // Disable schedule - cmd, root = clitest.New(t, "autostop", "disable", workspace.Name) + cmd, root = clitest.New(t, "ttl", "disable", workspace.Name) clitest.SetupConfig(t, client, root) cmd.SetOut(stdoutBuf) err = cmd.Execute() require.NoError(t, err, "unexpected error") - require.Contains(t, stdoutBuf.String(), "will no longer automatically stop", "unexpected output") // Ensure autostop schedule updated updated, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Empty(t, updated.AutostopSchedule, "expected autostop schedule to not be set") + require.Nil(t, updated.TTL, "expected ttl to not be set") }) t.Run("Enable_NotFound", func(t *testing.T) { @@ -105,7 +102,7 @@ func TestAutostop(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ) - cmd, root := clitest.New(t, "autostop", "enable", "doesnotexist") + cmd, root := clitest.New(t, "ttl", "enable", "doesnotexist", "8h30m") clitest.SetupConfig(t, client, root) err := cmd.Execute() @@ -123,43 +120,10 @@ func TestAutostop(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ) - cmd, root := clitest.New(t, "autostop", "disable", "doesnotexist") + cmd, root := clitest.New(t, "ttl", "disable", "doesnotexist") clitest.SetupConfig(t, client, root) err := cmd.Execute() require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) - - t.Run("Enable_DefaultSchedule", func(t *testing.T) { - t.Parallel() - - var ( - ctx = context.Background() - client = coderdtest.New(t, nil) - _ = coderdtest.NewProvisionerDaemon(t, client) - user = coderdtest.CreateFirstUser(t, client) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) - ) - - // check current TZ env var - currTz := os.Getenv("TZ") - if currTz == "" { - currTz = "UTC" - } - expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 18 * * 1-5", currTz) - - cmd, root := clitest.New(t, "autostop", "enable", workspace.Name) - clitest.SetupConfig(t, client, root) - - err := cmd.Execute() - require.NoError(t, err, "unexpected error") - - // Ensure nothing happened - updated, err := client.Workspace(ctx, workspace.ID) - require.NoError(t, err, "fetch updated workspace") - require.Equal(t, expectedSchedule, updated.AutostopSchedule, "expected default autostop schedule") - }) } diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 1a844f2551b85..eb500891b99c4 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -123,6 +123,22 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { return leftStr, rightStr, true + case sql.NullInt64: + var leftInt64Ptr *int64 + var rightInt64Ptr *int64 + if !typed.Valid { + leftInt64Ptr = nil + } else { + leftInt64Ptr = ptr(typed.Int64) + } + + rightInt64Ptr = ptr(right.(sql.NullInt64).Int64) + if !right.(sql.NullInt64).Valid { + rightInt64Ptr = nil + } + + return leftInt64Ptr, rightInt64Ptr, true + default: return left, right, false } @@ -147,3 +163,7 @@ func derefPointer(ptr reflect.Value) reflect.Value { return ptr } + +func ptr[T any](x T) *T { + return &x +} diff --git a/coderd/audit/diff_test.go b/coderd/audit/diff_test.go index 21fa6499a428c..50bffa3b0d3a1 100644 --- a/coderd/audit/diff_test.go +++ b/coderd/audit/diff_test.go @@ -172,7 +172,7 @@ func TestDiff(t *testing.T) { TemplateID: uuid.UUID{3}, Name: "rust workspace", AutostartSchedule: sql.NullString{String: "0 12 * * 1-5", Valid: true}, - AutostopSchedule: sql.NullString{String: "0 2 * * 2-6", Valid: true}, + Ttl: sql.NullInt64{Int64: int64(8 * time.Hour), Valid: true}, }, exp: audit.Map{ "id": uuid.UUID{1}.String(), @@ -180,7 +180,7 @@ func TestDiff(t *testing.T) { "template_id": uuid.UUID{3}.String(), "name": "rust workspace", "autostart_schedule": "0 12 * * 1-5", - "autostop_schedule": "0 2 * * 2-6", + "ttl": int64(28800000000000), // XXX: pq still does not support time.Duration }, }, { @@ -194,7 +194,7 @@ func TestDiff(t *testing.T) { TemplateID: uuid.UUID{3}, Name: "rust workspace", AutostartSchedule: sql.NullString{}, - AutostopSchedule: sql.NullString{}, + Ttl: sql.NullInt64{}, }, exp: audit.Map{ "id": uuid.UUID{1}.String(), diff --git a/coderd/audit/table.go b/coderd/audit/table.go index f7edadbcf21f2..0a2f9c1795dda 100644 --- a/coderd/audit/table.go +++ b/coderd/audit/table.go @@ -101,7 +101,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "deleted": ActionIgnore, // Changes, but is implicit when a delete event is fired. "name": ActionTrack, "autostart_schedule": ActionTrack, - "autostop_schedule": ActionTrack, + "ttl": ActionTrack, }, }) diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go index f402e7cedcc51..69fe79826cd73 100644 --- a/coderd/autobuild/executor/lifecycle_executor.go +++ b/coderd/autobuild/executor/lifecycle_executor.go @@ -50,7 +50,7 @@ func (e *Executor) Run() { func (e *Executor) runOnce(t time.Time) error { currentTick := t.Truncate(time.Minute) return e.db.InTx(func(db database.Store) error { - eligibleWorkspaces, err := db.GetWorkspacesAutostartAutostop(e.ctx) + eligibleWorkspaces, err := db.GetWorkspacesAutostart(e.ctx) if err != nil { return xerrors.Errorf("get eligible workspaces for autostart or autostop: %w", err) } @@ -84,21 +84,24 @@ func (e *Executor) runOnce(t time.Time) error { } var validTransition database.WorkspaceTransition - var sched *schedule.Schedule + var nextTransitionAt time.Time switch priorHistory.Transition { case database.WorkspaceTransitionStart: validTransition = database.WorkspaceTransitionStop - sched, err = schedule.Weekly(ws.AutostopSchedule.String) - if err != nil { - e.log.Warn(e.ctx, "workspace has invalid autostop schedule, skipping", + if !ws.Ttl.Valid || ws.Ttl.Int64 == 0 { + e.log.Debug(e.ctx, "invalid or zero ws ttl, skipping", slog.F("workspace_id", ws.ID), - slog.F("autostart_schedule", ws.AutostopSchedule.String), + slog.F("ttl", time.Duration(ws.Ttl.Int64)), ) continue } + ttl := time.Duration(ws.Ttl.Int64) + // Measure TTL from the time the workspace finished building. + // This can be finer granularity than 1 minute. + nextTransitionAt = priorHistory.UpdatedAt.Add(ttl) case database.WorkspaceTransitionStop: validTransition = database.WorkspaceTransitionStart - sched, err = schedule.Weekly(ws.AutostartSchedule.String) + sched, err := schedule.Weekly(ws.AutostartSchedule.String) if err != nil { e.log.Warn(e.ctx, "workspace has invalid autostart schedule, skipping", slog.F("workspace_id", ws.ID), @@ -106,6 +109,9 @@ func (e *Executor) runOnce(t time.Time) error { ) continue } + // Round down to the nearest minute, as this is the finest granularity cron supports. + // Truncate is probably not necessary here, but doing it anyway to be sure. + nextTransitionAt = sched.Next(priorHistory.CreatedAt).Truncate(time.Minute) default: e.log.Debug(e.ctx, "last transition not valid for autostart or autostop", slog.F("workspace_id", ws.ID), @@ -114,9 +120,6 @@ func (e *Executor) runOnce(t time.Time) error { continue } - // Round time down to the nearest minute, as this is the finest granularity cron supports. - // Truncate is probably not necessary here, but doing it anyway to be sure. - nextTransitionAt := sched.Next(priorHistory.CreatedAt).Truncate(time.Minute) if currentTick.Before(nextTransitionAt) { e.log.Debug(e.ctx, "skipping workspace: too early", slog.F("workspace_id", ws.ID), diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 96a7660e4f8a7..38ed639ff77fa 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -194,23 +194,23 @@ func TestExecutorAutostopOK(t *testing.T) { }) // Given: we have a user with a workspace workspace = mustProvisionWorkspace(t, client) + ttl = time.Minute ) // Given: workspace is running require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) // Given: the workspace initially has autostop disabled - require.Empty(t, workspace.AutostopSchedule) + require.Nil(t, workspace.TTL) // When: we enable workspace autostop - sched, err := schedule.Weekly("* * * * *") require.NoError(t, err) - require.NoError(t, client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: sched.String(), + require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: &ttl, })) // When: the autobuild executor ticks go func() { - tickCh <- time.Now().UTC().Add(time.Minute) + tickCh <- time.Now().UTC().Add(ttl + time.Minute) close(tickCh) }() @@ -234,24 +234,24 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) { }) // Given: we have a user with a workspace workspace = mustProvisionWorkspace(t, client) + ttl = time.Minute ) // Given: workspace is stopped workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) // Given: the workspace initially has autostop disabled - require.Empty(t, workspace.AutostopSchedule) + require.Nil(t, workspace.TTL) - // When: we enable workspace autostart - sched, err := schedule.Weekly("* * * * *") + // When: we set the TTL on the workspace require.NoError(t, err) - require.NoError(t, client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: sched.String(), + require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: &ttl, })) - // When: the autobuild executor ticks + // When: the autobuild executor ticks past the TTL go func() { - tickCh <- time.Now().UTC().Add(time.Minute) + tickCh <- time.Now().UTC().Add(ttl) close(tickCh) }() @@ -278,7 +278,7 @@ func TestExecutorAutostopNotEnabled(t *testing.T) { require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) // Given: the workspace has autostop disabled - require.Empty(t, workspace.AutostopSchedule) + require.Empty(t, workspace.TTL) // When: the autobuild executor ticks go func() { @@ -308,12 +308,12 @@ func TestExecutorWorkspaceDeleted(t *testing.T) { ) // Given: the workspace initially has autostart disabled - require.Empty(t, workspace.AutostopSchedule) + require.Empty(t, workspace.AutostartSchedule) // When: we enable workspace autostart sched, err := schedule.Weekly("* * * * *") require.NoError(t, err) - require.NoError(t, client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ + require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{ Schedule: sched.String(), })) @@ -348,14 +348,14 @@ func TestExecutorWorkspaceTooEarly(t *testing.T) { ) // Given: the workspace initially has autostart disabled - require.Empty(t, workspace.AutostopSchedule) + require.Empty(t, workspace.AutostartSchedule) // When: we enable workspace autostart with some time in the future futureTime := time.Now().Add(time.Hour) futureTimeCron := fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour()) sched, err := schedule.Weekly(futureTimeCron) require.NoError(t, err) - require.NoError(t, client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ + require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{ Schedule: sched.String(), })) diff --git a/coderd/coderd.go b/coderd/coderd.go index 3680040d0578f..3b8ade94c8a94 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -306,7 +306,7 @@ func New(options *Options) (http.Handler, func()) { r.Put("/", api.putWorkspaceAutostart) }) r.Route("/autostop", func(r chi.Router) { - r.Put("/", api.putWorkspaceAutostop) + r.Put("/", api.putWorkspaceTTL) }) r.Get("/watch", api.watchWorkspace) }) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index e876a54fdad55..1a714b90b33a4 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -363,14 +363,14 @@ func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg databa return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspacesAutostartAutostop(_ context.Context) ([]database.Workspace, error) { +func (q *fakeQuerier) GetWorkspacesAutostart(_ context.Context) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() workspaces := make([]database.Workspace, 0) for _, ws := range q.workspaces { if ws.AutostartSchedule.String != "" { workspaces = append(workspaces, ws) - } else if ws.AutostopSchedule.String != "" { + } else if ws.Ttl.Valid { workspaces = append(workspaces, ws) } } @@ -1666,7 +1666,7 @@ func (q *fakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.U return sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceAutostop(_ context.Context, arg database.UpdateWorkspaceAutostopParams) error { +func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateWorkspaceTTLParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -1674,7 +1674,7 @@ func (q *fakeQuerier) UpdateWorkspaceAutostop(_ context.Context, arg database.Up if workspace.ID != arg.ID { continue } - workspace.AutostopSchedule = arg.AutostopSchedule + workspace.Ttl = arg.Ttl q.workspaces[index] = workspace return nil } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f1041e3519475..4cd90454283d7 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -314,7 +314,7 @@ CREATE TABLE workspaces ( deleted boolean DEFAULT false NOT NULL, name character varying(64) NOT NULL, autostart_schedule text, - autostop_schedule text + ttl bigint ); ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('public.licenses_id_seq'::regclass); diff --git a/coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql b/coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql new file mode 100644 index 0000000000000..c4bfbd3b493a2 --- /dev/null +++ b/coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY workspaces DROP COLUMN ttl; +ALTER TABLE ONLY workspaces ADD COLUMN autostop_schedule text DEFAULT NULL; diff --git a/coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql b/coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql new file mode 100644 index 0000000000000..d63b4431f2eb7 --- /dev/null +++ b/coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY workspaces DROP COLUMN autostop_schedule; +ALTER TABLE ONLY workspaces ADD COLUMN ttl BIGINT DEFAULT NULL; diff --git a/coderd/database/models.go b/coderd/database/models.go index a705a02410411..3fcf947bf4cae 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -471,7 +471,7 @@ type Workspace struct { Deleted bool `db:"deleted" json:"deleted"` Name string `db:"name" json:"name"` AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` - AutostopSchedule sql.NullString `db:"autostop_schedule" json:"autostop_schedule"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` } type WorkspaceAgent struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 387a2c9a06698..db061cfcb36a1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -70,7 +70,7 @@ type querier interface { GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByTemplateIDsRow, error) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) - GetWorkspacesAutostartAutostop(ctx context.Context) ([]Workspace, error) + GetWorkspacesAutostart(ctx context.Context) ([]Workspace, error) GetWorkspacesByOrganizationIDs(ctx context.Context, arg GetWorkspacesByOrganizationIDsParams) ([]Workspace, error) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorkspacesByTemplateIDParams) ([]Workspace, error) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error) @@ -109,9 +109,9 @@ type querier interface { UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error - UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error + UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error } var _ querier = (*sqlQuerier)(nil) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5985afbf33d65..607ae0af8aa78 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3193,7 +3193,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE @@ -3215,14 +3215,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ) return i, err } const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE @@ -3250,7 +3250,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ) return i, err } @@ -3295,23 +3295,23 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, i return items, nil } -const getWorkspacesAutostartAutostop = `-- name: GetWorkspacesAutostartAutostop :many +const getWorkspacesAutostart = `-- name: GetWorkspacesAutostart :many SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE deleted = false AND ( - autostart_schedule <> '' + (autostart_schedule IS NOT NULL AND autostart_schedule <> '') OR - autostop_schedule <> '' + (ttl IS NOT NULL AND ttl > 0) ) ` -func (q *sqlQuerier) GetWorkspacesAutostartAutostop(ctx context.Context) ([]Workspace, error) { - rows, err := q.db.QueryContext(ctx, getWorkspacesAutostartAutostop) +func (q *sqlQuerier) GetWorkspacesAutostart(ctx context.Context) ([]Workspace, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesAutostart) if err != nil { return nil, err } @@ -3329,7 +3329,7 @@ func (q *sqlQuerier) GetWorkspacesAutostartAutostop(ctx context.Context) ([]Work &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ); err != nil { return nil, err } @@ -3345,7 +3345,7 @@ func (q *sqlQuerier) GetWorkspacesAutostartAutostop(ctx context.Context) ([]Work } const getWorkspacesByOrganizationIDs = `-- name: GetWorkspacesByOrganizationIDs :many -SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = ANY($1 :: uuid [ ]) AND deleted = $2 +SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE organization_id = ANY($1 :: uuid [ ]) AND deleted = $2 ` type GetWorkspacesByOrganizationIDsParams struct { @@ -3372,7 +3372,7 @@ func (q *sqlQuerier) GetWorkspacesByOrganizationIDs(ctx context.Context, arg Get &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ); err != nil { return nil, err } @@ -3389,7 +3389,7 @@ func (q *sqlQuerier) GetWorkspacesByOrganizationIDs(ctx context.Context, arg Get const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE @@ -3421,7 +3421,7 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorks &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ); err != nil { return nil, err } @@ -3438,7 +3438,7 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorks const getWorkspacesWithFilter = `-- name: GetWorkspacesWithFilter :many SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl FROM workspaces WHERE @@ -3483,7 +3483,7 @@ func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspa &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ); err != nil { return nil, err } @@ -3510,7 +3510,7 @@ INSERT INTO name ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule + ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl ` type InsertWorkspaceParams struct { @@ -3544,7 +3544,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar &i.Deleted, &i.Name, &i.AutostartSchedule, - &i.AutostopSchedule, + &i.Ttl, ) return i, err } @@ -3568,40 +3568,40 @@ func (q *sqlQuerier) UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWor return err } -const updateWorkspaceAutostop = `-- name: UpdateWorkspaceAutostop :exec +const updateWorkspaceDeletedByID = `-- name: UpdateWorkspaceDeletedByID :exec UPDATE workspaces SET - autostop_schedule = $2 + deleted = $2 WHERE id = $1 ` -type UpdateWorkspaceAutostopParams struct { - ID uuid.UUID `db:"id" json:"id"` - AutostopSchedule sql.NullString `db:"autostop_schedule" json:"autostop_schedule"` +type UpdateWorkspaceDeletedByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + Deleted bool `db:"deleted" json:"deleted"` } -func (q *sqlQuerier) UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceAutostop, arg.ID, arg.AutostopSchedule) +func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceDeletedByID, arg.ID, arg.Deleted) return err } -const updateWorkspaceDeletedByID = `-- name: UpdateWorkspaceDeletedByID :exec +const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec UPDATE workspaces SET - deleted = $2 + ttl = $2 WHERE id = $1 ` -type UpdateWorkspaceDeletedByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - Deleted bool `db:"deleted" json:"deleted"` +type UpdateWorkspaceTTLParams struct { + ID uuid.UUID `db:"id" json:"id"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` } -func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceDeletedByID, arg.ID, arg.Deleted) +func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceTTL, arg.ID, arg.Ttl) return err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index eb87ad9a51d41..5e8cdaa107895 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -33,7 +33,7 @@ WHERE -- name: GetWorkspacesByOrganizationIDs :many SELECT * FROM workspaces WHERE organization_id = ANY(@ids :: uuid [ ]) AND deleted = @deleted; --- name: GetWorkspacesAutostartAutostop :many +-- name: GetWorkspacesAutostart :many SELECT * FROM @@ -42,9 +42,9 @@ WHERE deleted = false AND ( - autostart_schedule <> '' + (autostart_schedule IS NOT NULL AND autostart_schedule <> '') OR - autostop_schedule <> '' + (ttl IS NOT NULL AND ttl > 0) ); -- name: GetWorkspacesByTemplateID :many @@ -107,10 +107,10 @@ SET WHERE id = $1; --- name: UpdateWorkspaceAutostop :exec +-- name: UpdateWorkspaceTTL :exec UPDATE workspaces SET - autostop_schedule = $2 + ttl = $2 WHERE id = $1; diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 471560c4d921d..7e4902c3b45e1 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -547,38 +547,31 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { } } -func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { +func (api *api) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace. InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { return } - var req codersdk.UpdateWorkspaceAutostopRequest + var req codersdk.UpdateWorkspaceTTLRequest if !httpapi.Read(rw, r, &req) { return } - var dbSched sql.NullString - if req.Schedule != "" { - validSched, err := schedule.Weekly(req.Schedule) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("invalid autostop schedule: %s", err), - }) - return - } - dbSched.String = validSched.String() - dbSched.Valid = true + var dbTTL sql.NullInt64 + if req.TTL != nil { + dbTTL.Int64 = int64(*req.TTL) + dbTTL.Valid = true } - err := api.Database.UpdateWorkspaceAutostop(r.Context(), database.UpdateWorkspaceAutostopParams{ - ID: workspace.ID, - AutostopSchedule: dbSched, + err := api.Database.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{ + ID: workspace.ID, + Ttl: dbTTL, }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("update workspace autostop schedule: %s", err), + Message: fmt.Sprintf("update workspace ttl: %s", err), }) return } @@ -777,6 +770,14 @@ func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.Work Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(), Name: workspace.Name, AutostartSchedule: workspace.AutostartSchedule.String, - AutostopSchedule: workspace.AutostopSchedule.String, + TTL: convertSQLNullInt64(workspace.Ttl), } } + +func convertSQLNullInt64(i sql.NullInt64) *time.Duration { + if !i.Valid { + return nil + } + + return (*time.Duration)(&i.Int64) +} diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index af43c6efdfd0d..a11e6c2fc3ff7 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -551,69 +551,21 @@ func TestWorkspaceUpdateAutostart(t *testing.T) { func TestWorkspaceUpdateAutostop(t *testing.T) { t.Parallel() - var dublinLoc = mustLocation(t, "Europe/Dublin") testCases := []struct { - name string - schedule string - expectedError string - at time.Time - expectedNext time.Time - expectedInterval time.Duration + name string + ttl *time.Duration + expectedError string }{ { - name: "disable autostop", - schedule: "", + name: "disable ttl", + ttl: nil, expectedError: "", }, { - name: "friday to monday", - schedule: "CRON_TZ=Europe/Dublin 30 17 * * 1-5", - expectedError: "", - at: time.Date(2022, 5, 6, 17, 31, 0, 0, dublinLoc), - expectedNext: time.Date(2022, 5, 9, 17, 30, 0, 0, dublinLoc), - expectedInterval: 71*time.Hour + 59*time.Minute, - }, - { - name: "monday to tuesday", - schedule: "CRON_TZ=Europe/Dublin 30 17 * * 1-5", - expectedError: "", - at: time.Date(2022, 5, 9, 17, 31, 0, 0, dublinLoc), - expectedNext: time.Date(2022, 5, 10, 17, 30, 0, 0, dublinLoc), - expectedInterval: 23*time.Hour + 59*time.Minute, - }, - { - // DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour. - name: "DST start", - schedule: "CRON_TZ=Europe/Dublin 30 17 * * *", - expectedError: "", - at: time.Date(2022, 3, 26, 17, 31, 0, 0, dublinLoc), - expectedNext: time.Date(2022, 3, 27, 17, 30, 0, 0, dublinLoc), - expectedInterval: 22*time.Hour + 59*time.Minute, - }, - { - // DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour. - name: "DST end", - schedule: "CRON_TZ=Europe/Dublin 30 17 * * *", - expectedError: "", - at: time.Date(2022, 10, 29, 17, 31, 0, 0, dublinLoc), - expectedNext: time.Date(2022, 10, 30, 17, 30, 0, 0, dublinLoc), - expectedInterval: 24*time.Hour + 59*time.Minute, - }, - { - name: "invalid location", - schedule: "CRON_TZ=Imaginary/Place 30 17 * * 1-5", - expectedError: "status code 500: invalid autostop schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place", - }, - { - name: "invalid schedule", - schedule: "asdf asdf asdf ", - expectedError: `status code 500: invalid autostop schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, - }, - { - name: "only 3 values", - schedule: "CRON_TZ=Europe/Dublin 30 9 *", - expectedError: `status code 500: invalid autostop schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, + name: "enable ttl", + ttl: ptr(time.Hour), + expectedError: "", }, } @@ -633,10 +585,10 @@ func TestWorkspaceUpdateAutostop(t *testing.T) { ) // ensure test invariant: new workspaces have no autostop schedule. - require.Empty(t, workspace.AutostopSchedule, "expected newly-minted workspace to have no autstop schedule") + require.Nil(t, workspace.TTL, "expected newly-minted workspace to have no TTL") - err := client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: testCase.schedule, + err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: testCase.ttl, }) if testCase.expectedError != "" { @@ -649,18 +601,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) { updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Equal(t, testCase.schedule, updated.AutostopSchedule, "expected autostop schedule to equal requested") - - if testCase.schedule == "" { - return - } - sched, err := schedule.Weekly(updated.AutostopSchedule) - require.NoError(t, err, "parse returned schedule") - - next := sched.Next(testCase.at) - require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostop time") - interval := next.Sub(testCase.at) - require.Equal(t, testCase.expectedInterval, interval, "unexpected interval") + require.Equal(t, testCase.ttl, updated.TTL, "expected autostop ttl to equal requested") }) } @@ -670,12 +611,12 @@ func TestWorkspaceUpdateAutostop(t *testing.T) { client = coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) wsid = uuid.New() - req = codersdk.UpdateWorkspaceAutostopRequest{ - Schedule: "9 30 1-5", + req = codersdk.UpdateWorkspaceTTLRequest{ + TTL: ptr(time.Hour), } ) - err := client.UpdateWorkspaceAutostop(ctx, wsid, req) + err := client.UpdateWorkspaceTTL(ctx, wsid, req) require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error") coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404") @@ -683,15 +624,6 @@ func TestWorkspaceUpdateAutostop(t *testing.T) { }) } -func mustLocation(t *testing.T, location string) *time.Location { - loc, err := time.LoadLocation(location) - if err != nil { - t.Errorf("failed to load location %s: %s", location, err.Error()) - } - - return loc -} - func TestWorkspaceWatcher(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) @@ -715,3 +647,17 @@ func TestWorkspaceWatcher(t *testing.T) { cancel() require.EqualValues(t, codersdk.Workspace{}, <-wc) } + +func mustLocation(t *testing.T, location string) *time.Location { + t.Helper() + loc, err := time.LoadLocation(location) + if err != nil { + t.Errorf("failed to load location %s: %s", location, err.Error()) + } + + return loc +} + +func ptr[T any](x T) *T { + return &x +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index d9080d876d03d..a6f3eb09a1721 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -27,7 +27,7 @@ type Workspace struct { Outdated bool `json:"outdated"` Name string `json:"name"` AutostartSchedule string `json:"autostart_schedule"` - AutostopSchedule string `json:"autostop_schedule"` + TTL *time.Duration `json:"ttl"` } // CreateWorkspaceBuildRequest provides options to update the latest workspace build. @@ -158,13 +158,13 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req } // UpdateWorkspaceAutostopRequest is a request to update a workspace's autostop schedule. -type UpdateWorkspaceAutostopRequest struct { - Schedule string `json:"schedule"` +type UpdateWorkspaceTTLRequest struct { + TTL *time.Duration `json:"ttl"` } -// UpdateWorkspaceAutostop sets the autostop schedule for workspace by id. -// If the provided schedule is empty, autostop is disabled for the workspace. -func (c *Client) UpdateWorkspaceAutostop(ctx context.Context, id uuid.UUID, req UpdateWorkspaceAutostopRequest) error { +// UpdateWorkspaceTTL sets the ttl for workspace by id. +// If the provided duration is nil, autostop is disabled for the workspace. +func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req UpdateWorkspaceTTLRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/autostop", id.String()) res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { From 3ea3e903f0a2ce087311821048bdfbefd88c1d77 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 14:33:26 +0100 Subject: [PATCH 02/15] fix: truncate ttl to time.Minute --- agent/usershell/usershell_darwin.go | 2 +- cli/ssh.go | 4 +- cli/ttl.go | 17 +++++- cli/ttl_test.go | 54 +++++++++++++++++-- .../autobuild/executor/lifecycle_executor.go | 13 ++--- .../executor/lifecycle_executor_test.go | 41 ++++++++++++-- coderd/workspaces.go | 5 +- 7 files changed, 116 insertions(+), 20 deletions(-) diff --git a/agent/usershell/usershell_darwin.go b/agent/usershell/usershell_darwin.go index ea0fbedfdbb7a..532474f628b1e 100644 --- a/agent/usershell/usershell_darwin.go +++ b/agent/usershell/usershell_darwin.go @@ -3,6 +3,6 @@ package usershell import "os" // Get returns the $SHELL environment variable. -func Get(username string) (string, error) { +func Get(_ string) (string, error) { return os.Getenv("SHELL"), nil } diff --git a/cli/ssh.go b/cli/ssh.go index ed8b7c335b0ea..cb924ceed208c 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -269,11 +269,11 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u return time.Time{}, nil } - if ws.TTL == nil { + if ws.TTL == nil || *ws.TTL == 0 { return time.Time{}, nil } - deadline = now.Add(*ws.TTL) + deadline = ws.LatestBuild.UpdatedAt.Add(*ws.TTL) callback = func() { ttl := deadline.Sub(now) var title, body string diff --git a/cli/ttl.go b/cli/ttl.go index a8a4ee874e8b4..126725ad1a345 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -5,11 +5,14 @@ import ( "time" "github.com/spf13/cobra" + "golang.org/x/xerrors" "github.com/coder/coder/codersdk" ) -const ttlDescriptionLong = `To have your workspace stop automatically after a configurable interval has passed.` +const ttlDescriptionLong = `To have your workspace stop automatically after a configurable interval has passed. +Minimum TTL is 1 minute. +` func ttl() *cobra.Command { ttlCmd := &cobra.Command{ @@ -83,8 +86,18 @@ func ttlEnable() *cobra.Command { return err } + truncated := ttl.Truncate(time.Minute) + + if truncated == 0 { + return xerrors.Errorf("ttl must be at least 1m") + } + + if truncated != ttl { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s", truncated) + } + err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ - TTL: &ttl, + TTL: &truncated, }) if err != nil { return err diff --git a/cli/ttl_test.go b/cli/ttl_test.go index b2e4419f788a3..e85f0557ec0a9 100644 --- a/cli/ttl_test.go +++ b/cli/ttl_test.go @@ -30,7 +30,7 @@ func TestTTL(t *testing.T) { project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) cmdArgs = []string{"ttl", "show", workspace.Name} - ttl = 8 * time.Hour + ttl = 8*time.Hour + 30*time.Minute + 30*time.Second stdoutBuf = &bytes.Buffer{} ) @@ -45,7 +45,7 @@ func TestTTL(t *testing.T) { err = cmd.Execute() require.NoError(t, err, "unexpected error") - require.Equal(t, strings.TrimSpace(stdoutBuf.String()), ttl.String()) + require.Equal(t, ttl.Truncate(time.Minute).String(), strings.TrimSpace(stdoutBuf.String())) }) t.Run("EnableDisableOK", func(t *testing.T) { @@ -60,7 +60,7 @@ func TestTTL(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) - ttl = 8*time.Hour + 30*time.Minute + ttl = 8*time.Hour + 30*time.Minute + 30*time.Second cmdArgs = []string{"ttl", "enable", workspace.Name, ttl.String()} stdoutBuf = &bytes.Buffer{} ) @@ -75,7 +75,8 @@ func TestTTL(t *testing.T) { // Ensure autostop schedule updated updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Equal(t, *updated.TTL, ttl) + require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) + require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down") // Disable schedule cmd, root = clitest.New(t, "ttl", "disable", workspace.Name) @@ -91,6 +92,51 @@ func TestTTL(t *testing.T) { require.Nil(t, updated.TTL, "expected ttl to not be set") }) + t.Run("ZeroInvalid", func(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + client = coderdtest.New(t, nil) + _ = coderdtest.NewProvisionerDaemon(t, client) + user = coderdtest.CreateFirstUser(t, client) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) + ttl = 8*time.Hour + 30*time.Minute + 30*time.Second + cmdArgs = []string{"ttl", "enable", workspace.Name, ttl.String()} + stdoutBuf = &bytes.Buffer{} + ) + + cmd, root := clitest.New(t, cmdArgs...) + clitest.SetupConfig(t, client, root) + cmd.SetOut(stdoutBuf) + + err := cmd.Execute() + require.NoError(t, err, "unexpected error") + + // Ensure ttl updated + updated, err := client.Workspace(ctx, workspace.ID) + require.NoError(t, err, "fetch updated workspace") + require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) + require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down") + + // A TTL of zero is not considered valid. + stdoutBuf.Reset() + cmd, root = clitest.New(t, "ttl", "enable", workspace.Name, "0s") + clitest.SetupConfig(t, client, root) + cmd.SetOut(stdoutBuf) + + err = cmd.Execute() + require.EqualError(t, err, "ttl must be at least 1m", "unexpected error") + + // Ensure ttl remains as before + updated, err = client.Workspace(ctx, workspace.ID) + require.NoError(t, err, "fetch updated workspace") + require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) + }) + t.Run("Enable_NotFound", func(t *testing.T) { t.Parallel() diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go index 69fe79826cd73..a6c004fa2de85 100644 --- a/coderd/autobuild/executor/lifecycle_executor.go +++ b/coderd/autobuild/executor/lifecycle_executor.go @@ -84,7 +84,7 @@ func (e *Executor) runOnce(t time.Time) error { } var validTransition database.WorkspaceTransition - var nextTransitionAt time.Time + var nextTransition time.Time switch priorHistory.Transition { case database.WorkspaceTransitionStart: validTransition = database.WorkspaceTransitionStop @@ -97,8 +97,9 @@ func (e *Executor) runOnce(t time.Time) error { } ttl := time.Duration(ws.Ttl.Int64) // Measure TTL from the time the workspace finished building. - // This can be finer granularity than 1 minute. - nextTransitionAt = priorHistory.UpdatedAt.Add(ttl) + // Truncate to nearest minute for consistency with autostart + // behavior, and add one minute for padding. + nextTransition = priorHistory.UpdatedAt.Truncate(time.Minute).Add(ttl + time.Minute) case database.WorkspaceTransitionStop: validTransition = database.WorkspaceTransitionStart sched, err := schedule.Weekly(ws.AutostartSchedule.String) @@ -111,7 +112,7 @@ func (e *Executor) runOnce(t time.Time) error { } // Round down to the nearest minute, as this is the finest granularity cron supports. // Truncate is probably not necessary here, but doing it anyway to be sure. - nextTransitionAt = sched.Next(priorHistory.CreatedAt).Truncate(time.Minute) + nextTransition = sched.Next(priorHistory.CreatedAt).Truncate(time.Minute) default: e.log.Debug(e.ctx, "last transition not valid for autostart or autostop", slog.F("workspace_id", ws.ID), @@ -120,10 +121,10 @@ func (e *Executor) runOnce(t time.Time) error { continue } - if currentTick.Before(nextTransitionAt) { + if currentTick.Before(nextTransition) { e.log.Debug(e.ctx, "skipping workspace: too early", slog.F("workspace_id", ws.ID), - slog.F("next_transition_at", nextTransitionAt), + slog.F("next_transition_at", nextTransition), slog.F("transition", validTransition), slog.F("current_tick", currentTick), ) diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 38ed639ff77fa..f4af615078789 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -208,13 +208,13 @@ func TestExecutorAutostopOK(t *testing.T) { TTL: &ttl, })) - // When: the autobuild executor ticks + // When: the autobuild executor ticks *after* the TTL: go func() { tickCh <- time.Now().UTC().Add(ttl + time.Minute) close(tickCh) }() - // Then: the workspace should be started + // Then: the workspace should be stopped <-time.After(5 * time.Second) ws := mustWorkspace(t, client, workspace.ID) require.NotEqual(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected a workspace build to occur") @@ -333,7 +333,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) { require.Equal(t, codersdk.WorkspaceTransitionDelete, ws.LatestBuild.Transition, "expected workspace to be deleted") } -func TestExecutorWorkspaceTooEarly(t *testing.T) { +func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) { t.Parallel() var ( @@ -372,6 +372,41 @@ func TestExecutorWorkspaceTooEarly(t *testing.T) { require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running") } +func TestExecutorWorkspaceTTLTooEarly(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + tickCh = make(chan time.Time) + client = coderdtest.New(t, &coderdtest.Options{ + AutobuildTicker: tickCh, + }) + // Given: we have a user with a workspace + workspace = mustProvisionWorkspace(t, client) + ttl = time.Hour + ) + + // Given: the workspace initially has TTL unset + require.Nil(t, workspace.TTL) + + // When: we set the TTL to some time in the distant future + require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTL: &ttl, + })) + + // When: the autobuild executor ticks + go func() { + tickCh <- time.Now().UTC() + close(tickCh) + }() + + // Then: nothing should happen + <-time.After(5 * time.Second) + ws := mustWorkspace(t, client, workspace.ID) + require.Equal(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected no further workspace builds to occur") + require.Equal(t, database.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running") +} + 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`) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7e4902c3b45e1..b9e7be07da96e 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -560,8 +560,9 @@ func (api *api) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { } var dbTTL sql.NullInt64 - if req.TTL != nil { - dbTTL.Int64 = int64(*req.TTL) + if req.TTL != nil && *req.TTL > 0 { + truncated := req.TTL.Truncate(time.Minute) + dbTTL.Int64 = int64(truncated) dbTTL.Valid = true } From ea49127125b18382bcacc49fa3a986536bef70f1 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 14:34:20 +0100 Subject: [PATCH 03/15] noop: rename migration --- ...me_this_autostop_ttl.down.sql => 000013_autostop_ttl.down.sql} | 0 ...rename_this_autostop_ttl.up.sql => 000013_autostop_ttl.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{999999_rename_this_autostop_ttl.down.sql => 000013_autostop_ttl.down.sql} (100%) rename coderd/database/migrations/{999999_rename_this_autostop_ttl.up.sql => 000013_autostop_ttl.up.sql} (100%) diff --git a/coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql b/coderd/database/migrations/000013_autostop_ttl.down.sql similarity index 100% rename from coderd/database/migrations/999999_rename_this_autostop_ttl.down.sql rename to coderd/database/migrations/000013_autostop_ttl.down.sql diff --git a/coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql b/coderd/database/migrations/000013_autostop_ttl.up.sql similarity index 100% rename from coderd/database/migrations/999999_rename_this_autostop_ttl.up.sql rename to coderd/database/migrations/000013_autostop_ttl.up.sql From a06218f244d4e6b4c12488fde226698d7883a73a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 14:53:41 +0100 Subject: [PATCH 04/15] cli: rename ttl enable/disable -> ttl set/unset --- cli/ttl.go | 18 +++++++++--------- cli/ttl_test.go | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index 126725ad1a345..1fc119f0f3024 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -17,15 +17,15 @@ Minimum TTL is 1 minute. func ttl() *cobra.Command { ttlCmd := &cobra.Command{ Annotations: workspaceCommand, - Use: "ttl enable ", + Use: "ttl set ", Short: "schedule a workspace to automatically stop after a configurable interval", Long: ttlDescriptionLong, - Example: "coder ttl enable my-workspace 8h30m", + Example: "coder ttl set my-workspace 8h30m", } ttlCmd.AddCommand(ttlShow()) - ttlCmd.AddCommand(ttlEnable()) - ttlCmd.AddCommand(ttlDisable()) + ttlCmd.AddCommand(ttlset()) + ttlCmd.AddCommand(ttlunset()) return ttlCmd } @@ -50,7 +50,7 @@ func ttlShow() *cobra.Command { } if workspace.TTL == nil { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not enabled\n") + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not setd\n") return nil } @@ -62,9 +62,9 @@ func ttlShow() *cobra.Command { return cmd } -func ttlEnable() *cobra.Command { +func ttlset() *cobra.Command { cmd := &cobra.Command{ - Use: "enable ", + Use: "set ", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) @@ -110,9 +110,9 @@ func ttlEnable() *cobra.Command { return cmd } -func ttlDisable() *cobra.Command { +func ttlunset() *cobra.Command { return &cobra.Command{ - Use: "disable ", + Use: "unset ", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) diff --git a/cli/ttl_test.go b/cli/ttl_test.go index e85f0557ec0a9..ece7f81064dd3 100644 --- a/cli/ttl_test.go +++ b/cli/ttl_test.go @@ -48,7 +48,7 @@ func TestTTL(t *testing.T) { require.Equal(t, ttl.Truncate(time.Minute).String(), strings.TrimSpace(stdoutBuf.String())) }) - t.Run("EnableDisableOK", func(t *testing.T) { + t.Run("SetUnsetOK", func(t *testing.T) { t.Parallel() var ( @@ -61,7 +61,7 @@ func TestTTL(t *testing.T) { project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ttl = 8*time.Hour + 30*time.Minute + 30*time.Second - cmdArgs = []string{"ttl", "enable", workspace.Name, ttl.String()} + cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()} stdoutBuf = &bytes.Buffer{} ) @@ -78,8 +78,8 @@ func TestTTL(t *testing.T) { require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down") - // Disable schedule - cmd, root = clitest.New(t, "ttl", "disable", workspace.Name) + // unset schedule + cmd, root = clitest.New(t, "ttl", "unset", workspace.Name) clitest.SetupConfig(t, client, root) cmd.SetOut(stdoutBuf) @@ -105,7 +105,7 @@ func TestTTL(t *testing.T) { project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ttl = 8*time.Hour + 30*time.Minute + 30*time.Second - cmdArgs = []string{"ttl", "enable", workspace.Name, ttl.String()} + cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()} stdoutBuf = &bytes.Buffer{} ) @@ -124,7 +124,7 @@ func TestTTL(t *testing.T) { // A TTL of zero is not considered valid. stdoutBuf.Reset() - cmd, root = clitest.New(t, "ttl", "enable", workspace.Name, "0s") + cmd, root = clitest.New(t, "ttl", "set", workspace.Name, "0s") clitest.SetupConfig(t, client, root) cmd.SetOut(stdoutBuf) @@ -137,7 +137,7 @@ func TestTTL(t *testing.T) { require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) }) - t.Run("Enable_NotFound", func(t *testing.T) { + t.Run("Set_NotFound", func(t *testing.T) { t.Parallel() var ( @@ -148,14 +148,14 @@ func TestTTL(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ) - cmd, root := clitest.New(t, "ttl", "enable", "doesnotexist", "8h30m") + cmd, root := clitest.New(t, "ttl", "set", "doesnotexist", "8h30m") clitest.SetupConfig(t, client, root) err := cmd.Execute() require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) - t.Run("Disable_NotFound", func(t *testing.T) { + t.Run("Unset_NotFound", func(t *testing.T) { t.Parallel() var ( @@ -166,7 +166,7 @@ func TestTTL(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ) - cmd, root := clitest.New(t, "ttl", "disable", "doesnotexist") + cmd, root := clitest.New(t, "ttl", "unset", "doesnotexist") clitest.SetupConfig(t, client, root) err := cmd.Execute() From 58d72c3fc12c5d31bce2e6cee5ad871bc7ef806f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 17:30:54 +0100 Subject: [PATCH 05/15] address PR comments --- cli/ttl.go | 10 +++++----- coderd/coderd.go | 2 +- codersdk/workspaces.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index 1fc119f0f3024..f2b4b8959d218 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -17,8 +17,8 @@ Minimum TTL is 1 minute. func ttl() *cobra.Command { ttlCmd := &cobra.Command{ Annotations: workspaceCommand, - Use: "ttl set ", - Short: "schedule a workspace to automatically stop after a configurable interval", + Use: "ttl [command]", + Short: "Schedule a workspace to automatically stop after a configurable interval", Long: ttlDescriptionLong, Example: "coder ttl set my-workspace 8h30m", } @@ -50,7 +50,7 @@ func ttlShow() *cobra.Command { } if workspace.TTL == nil { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not setd\n") + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "not set\n") return nil } @@ -93,7 +93,7 @@ func ttlset() *cobra.Command { } if truncated != ttl { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s", truncated) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s\n", truncated) } err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ @@ -136,7 +136,7 @@ func ttlunset() *cobra.Command { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically stop.\n\n", workspace.Name) + _, _ = fmt.Fprint(cmd.OutOrStdout(), "ttl unset\n", workspace.Name) return nil }, diff --git a/coderd/coderd.go b/coderd/coderd.go index 3b8ade94c8a94..a7e1ea202167d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -305,7 +305,7 @@ func New(options *Options) (http.Handler, func()) { r.Route("/autostart", func(r chi.Router) { r.Put("/", api.putWorkspaceAutostart) }) - r.Route("/autostop", func(r chi.Router) { + r.Route("/ttl", func(r chi.Router) { r.Put("/", api.putWorkspaceTTL) }) r.Get("/watch", api.watchWorkspace) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index a6f3eb09a1721..e6ea460b73988 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -165,10 +165,10 @@ type UpdateWorkspaceTTLRequest struct { // UpdateWorkspaceTTL sets the ttl for workspace by id. // If the provided duration is nil, autostop is disabled for the workspace. func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req UpdateWorkspaceTTLRequest) error { - path := fmt.Sprintf("/api/v2/workspaces/%s/autostop", id.String()) + path := fmt.Sprintf("/api/v2/workspaces/%s/ttl", id.String()) res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { - return xerrors.Errorf("update workspace autostop: %w", err) + return xerrors.Errorf("update workspace ttl: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { From 6f67b7997de5f3a06fd719983d83371e5b473f7c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 17:35:30 +0100 Subject: [PATCH 06/15] make gen --- coderd/database/dump.sql | 1 - site/src/api/typesGenerated.ts | 24 ++++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 4cd90454283d7..40362564c158b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -483,4 +483,3 @@ ALTER TABLE ONLY workspaces ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT; - diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3a64d14942f97..052a2db62edda 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -82,7 +82,7 @@ export interface CreateUserRequest { readonly organization_id: string } -// From codersdk/workspaces.go:34:6 +// From codersdk/workspaces.go:36:6 export interface CreateWorkspaceBuildRequest { readonly template_version_id?: string readonly transition: WorkspaceTransition @@ -285,14 +285,21 @@ export interface UpdateUserProfileRequest { readonly username: string } -// From codersdk/workspaces.go:141:6 +// From codersdk/workspaces.go:134:6 export interface UpdateWorkspaceAutostartRequest { readonly schedule: string } +<<<<<<< HEAD // From codersdk/workspaces.go:161:6 export interface UpdateWorkspaceAutostopRequest { readonly schedule: string +======= +// From codersdk/workspaces.go:154:6 +export interface UpdateWorkspaceTTLRequest { + // This is likely an enum in an external package ("time.Duration") + readonly ttl?: number +>>>>>>> e46a9a0f (make gen) } // From codersdk/files.go:16:6 @@ -345,7 +352,7 @@ export interface UsersRequest extends Pagination { readonly status?: string } -// From codersdk/workspaces.go:18:6 +// From codersdk/workspaces.go:20:6 export interface Workspace { readonly id: string readonly created_at: string @@ -358,7 +365,8 @@ export interface Workspace { readonly outdated: boolean readonly name: string readonly autostart_schedule: string - readonly autostop_schedule: string + // This is likely an enum in an external package ("time.Duration") + readonly ttl?: number } // From codersdk/workspaceresources.go:31:6 @@ -421,12 +429,20 @@ export interface WorkspaceBuild { readonly job: ProvisionerJob } +<<<<<<< HEAD // From codersdk/workspaces.go:64:6 +======= +// From codersdk/workspaces.go:57:6 +>>>>>>> e46a9a0f (make gen) export interface WorkspaceBuildsRequest extends Pagination { readonly WorkspaceID: string } +<<<<<<< HEAD // From codersdk/workspaces.go:180:6 +======= +// From codersdk/workspaces.go:173:6 +>>>>>>> e46a9a0f (make gen) export interface WorkspaceFilter { readonly OrganizationID: string readonly Owner: string From f65e3de5251dbb374ccf04f4f3dd9cfb9f3e7087 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 17:52:10 +0100 Subject: [PATCH 07/15] fixup! make gen --- cli/ttl_test.go | 4 ++-- codersdk/workspaces.go | 2 +- site/src/api/api.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/ttl_test.go b/cli/ttl_test.go index ece7f81064dd3..46c48b877012d 100644 --- a/cli/ttl_test.go +++ b/cli/ttl_test.go @@ -72,7 +72,7 @@ func TestTTL(t *testing.T) { err := cmd.Execute() require.NoError(t, err, "unexpected error") - // Ensure autostop schedule updated + // Ensure ttl updated updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL) @@ -86,7 +86,7 @@ func TestTTL(t *testing.T) { err = cmd.Execute() require.NoError(t, err, "unexpected error") - // Ensure autostop schedule updated + // Ensure ttl updated updated, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") require.Nil(t, updated.TTL, "expected ttl to not be set") diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index e6ea460b73988..a8b39d4dc1057 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -157,7 +157,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req return nil } -// UpdateWorkspaceAutostopRequest is a request to update a workspace's autostop schedule. +// UpdateWorkspaceTTLRequest is a request to update a workspace's TTL. type UpdateWorkspaceTTLRequest struct { TTL *time.Duration `json:"ttl"` } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b2e47b628f1ad..e1aefabc57405 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -197,7 +197,7 @@ export const putWorkspaceAutostart = async ( export const putWorkspaceAutostop = async ( workspaceID: string, - autostop: TypesGen.UpdateWorkspaceAutostopRequest, + autostop: TypesGen.UpdateWorkspaceTTLRequest, ): Promise => { const payload = JSON.stringify(autostop) await axios.put(`/api/v2/workspaces/${workspaceID}/autostop`, payload, { From 77b8ad36022ba09712b01e168d1ba03355b4100a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 18:02:27 +0100 Subject: [PATCH 08/15] cli: ttl: xerrors.Errorf everywhere --- cli/ttl.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index f2b4b8959d218..29417ff943501 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -37,16 +37,16 @@ func ttlShow() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { - return err + return xerrors.Errorf("create client: %w", err) } organization, err := currentOrganization(cmd, client) if err != nil { - return err + return xerrors.Errorf("get current org: %w", err) } workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { - return err + return xerrors.Errorf("get workspace: %w", err) } if workspace.TTL == nil { @@ -69,21 +69,21 @@ func ttlset() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { - return err + return xerrors.Errorf("create client: %w", err) } organization, err := currentOrganization(cmd, client) if err != nil { - return err + return xerrors.Errorf("get current org: %w", err) } workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { - return err + return xerrors.Errorf("get workspace: %w", err) } ttl, err := time.ParseDuration(args[1]) if err != nil { - return err + return xerrors.Errorf("parse ttl: %w", err) } truncated := ttl.Truncate(time.Minute) @@ -100,7 +100,7 @@ func ttlset() *cobra.Command { TTL: &truncated, }) if err != nil { - return err + return xerrors.Errorf("update workspace ttl: %w", err) } return nil @@ -117,23 +117,23 @@ func ttlunset() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { - return err + return xerrors.Errorf("create client: %w", err) } organization, err := currentOrganization(cmd, client) if err != nil { - return err + return xerrors.Errorf("get current org: %w", err) } workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) if err != nil { - return err + return xerrors.Errorf("get workspace: %w", err) } err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ TTL: nil, }) if err != nil { - return err + return xerrors.Errorf("update workspace ttl: %w", err) } _, _ = fmt.Fprint(cmd.OutOrStdout(), "ttl unset\n", workspace.Name) From 80d7b1c7d544c1bbc6b21b4e61515a087c4d74aa Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 19 May 2022 18:15:44 +0000 Subject: [PATCH 09/15] frontend --- site/src/components/Workspace/Workspace.tsx | 2 +- .../WorkspaceSchedule.stories.tsx | 69 +++++++++++++++++-- .../WorkspaceSchedule/WorkspaceSchedule.tsx | 55 ++++++++++----- site/src/testHelpers/entities.ts | 11 +-- 4 files changed, 103 insertions(+), 34 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 5f94b749463ce..82fb546bc5f59 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -50,7 +50,7 @@ export const Workspace: React.FC = ({ - + diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx index c250580ae2a55..8c64ba563b9bc 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx @@ -1,6 +1,7 @@ import { Story } from "@storybook/react" +import dayjs from "dayjs" import React from "react" -import { MockWorkspaceAutostartEnabled } from "../../testHelpers/renderHelpers" +import * as Mocks from "../../testHelpers/renderHelpers" import { WorkspaceSchedule, WorkspaceScheduleProps } from "./WorkspaceSchedule" export default { @@ -10,8 +11,66 @@ export default { const Template: Story = (args) => -export const Example = Template.bind({}) -Example.args = { - autostart: MockWorkspaceAutostartEnabled.schedule, - autostop: "", +export const NoTTL = Template.bind({}) +NoTTL.args = { + workspace: { + ...Mocks.MockWorkspace, + ttl: undefined, + }, +} + +export const ShutdownSoon = Template.bind({}) +ShutdownSoon.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "start", + updated_at: dayjs().subtract(1, "hour").toString(), // 1 hour ago + }, + ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours + }, +} + +export const ShutdownLong = Template.bind({}) +ShutdownLong.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "start", + updated_at: dayjs().toString(), + }, + ttl: 7 * 24 * 60 * 60 * 1000 * 1_000_000, // 7 days + }, +} + +export const WorkspaceOffShort = Template.bind({}) +WorkspaceOffShort.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + updated_at: dayjs().subtract(2, "days").toString(), + }, + ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours + }, +} + +export const WorkspaceOffLong = Template.bind({}) +WorkspaceOffLong.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + updated_at: dayjs().subtract(2, "days").toString(), + }, + ttl: 2 * 365 * 24 * 60 * 60 * 1000 * 1_000_000, // 2 years + }, } diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index 240f1b7357cba..dde2fa6a067ea 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -1,22 +1,20 @@ import Box from "@material-ui/core/Box" import Typography from "@material-ui/core/Typography" import cronstrue from "cronstrue" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" +import relativeTime from "dayjs/plugin/relativeTime" import React from "react" +import * as TypesGen from "../../api/typesGenerated" import { extractTimezone, stripTimezone } from "../../util/schedule" import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection" +dayjs.extend(duration) +dayjs.extend(relativeTime) + const Language = { autoStartLabel: (schedule: string): string => { - const prefix = "Workspace start" - - if (schedule) { - return `${prefix} (${extractTimezone(schedule)})` - } else { - return prefix - } - }, - autoStopLabel: (schedule: string): string => { - const prefix = "Workspace shutdown" + const prefix = "Start" if (schedule) { return `${prefix} (${extractTimezone(schedule)})` @@ -24,17 +22,38 @@ const Language = { return prefix } }, - cronHumanDisplay: (schedule: string): string => { + autoStartDisplay: (schedule: string): string => { if (schedule) { return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false }) } return "Manual" }, + autoStopLabel: "Shutdown", + autoStopDisplay: (workspace: TypesGen.Workspace): string => { + const latest = workspace.latest_build + + if (!workspace.ttl || workspace.ttl < 1) { + return "Manual" + } + + if (latest.transition === "start") { + const now = dayjs() + const updatedAt = dayjs(latest.updated_at) + const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms") + if (now.isAfter(deadline)) { + return "WORKING ON THIS" + } else { + return now.to(deadline) + } + } + + const duration = dayjs.duration(workspace.ttl / 1_000_000, "milliseconds") + return `${duration.humanize()} after start` + }, } export interface WorkspaceScheduleProps { - autostart: string - autostop: string + workspace: TypesGen.Workspace } /** @@ -42,17 +61,17 @@ export interface WorkspaceScheduleProps { * * @remarks Visual Component */ -export const WorkspaceSchedule: React.FC = ({ autostart, autostop }) => { +export const WorkspaceSchedule: React.FC = ({ workspace }) => { return ( - {Language.autoStartLabel(autostart)} - {Language.cronHumanDisplay(autostart)} + {Language.autoStartLabel(workspace.autostart_schedule)} + {Language.autoStartDisplay(workspace.autostart_schedule)} - {Language.autoStopLabel(autostop)} - {Language.cronHumanDisplay(autostop)} + {Language.autoStopLabel} + {Language.autoStopDisplay(workspace)} ) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index e34b268f22e7a..6efb82fda970a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -100,15 +100,6 @@ export const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartReq schedule: "CRON_TZ=Canada/Eastern 30 9 * * 1-5", } -export const MockWorkspaceAutostopDisabled: TypesGen.UpdateWorkspaceAutostartRequest = { - schedule: "", -} - -export const MockWorkspaceAutostopEnabled: TypesGen.UpdateWorkspaceAutostartRequest = { - // Runs at 9:30pm Monday through Friday using America/Toronto - schedule: "CRON_TZ=America/Toronto 30 21 * * 1-5", -} - export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { build_number: 1, created_at: "2022-05-17T17:39:01.382927298Z", @@ -147,7 +138,7 @@ export const MockWorkspace: TypesGen.Workspace = { owner_id: MockUser.id, owner_name: MockUser.username, autostart_schedule: MockWorkspaceAutostartEnabled.schedule, - autostop_schedule: MockWorkspaceAutostopEnabled.schedule, + ttl: 2 * 60 * 1000 * 1_000_000, // 2 hours as nanoseconds latest_build: MockWorkspaceBuild, } From 32c92f61581af008cba9f2c5cc340c2ce2f0bb77 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 19:31:26 +0100 Subject: [PATCH 10/15] codersdk change --- coderd/autobuild/executor/lifecycle_executor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index f4af615078789..a0aa20dc3bd0f 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -404,7 +404,7 @@ func TestExecutorWorkspaceTTLTooEarly(t *testing.T) { <-time.After(5 * time.Second) ws := mustWorkspace(t, client, workspace.ID) require.Equal(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected no further workspace builds to occur") - require.Equal(t, database.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running") + require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running") } func TestExecutorAutostartMultipleOK(t *testing.T) { From b3168c15c081421ec02379f0e955c7ae490eb0b2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 19 May 2022 19:33:16 +0100 Subject: [PATCH 11/15] fixup! frontend --- site/src/testHelpers/entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 6efb82fda970a..fc67c169c15d2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -138,7 +138,7 @@ export const MockWorkspace: TypesGen.Workspace = { owner_id: MockUser.id, owner_name: MockUser.username, autostart_schedule: MockWorkspaceAutostartEnabled.schedule, - ttl: 2 * 60 * 1000 * 1_000_000, // 2 hours as nanoseconds + ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours as nanoseconds latest_build: MockWorkspaceBuild, } From d00fefbd6eb19d52bab434487b4fcbef8c822d1f Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 19 May 2022 14:44:29 -0400 Subject: [PATCH 12/15] Update site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx --- site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index dde2fa6a067ea..0a5a8c5276f9e 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -41,7 +41,7 @@ const Language = { const updatedAt = dayjs(latest.updated_at) const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms") if (now.isAfter(deadline)) { - return "WORKING ON THIS" + return "workspace is shutting down now" } else { return now.to(deadline) } From d8ccd6c6b5997e9d7d9358a79fd90f1429306e28 Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 19 May 2022 14:45:34 -0400 Subject: [PATCH 13/15] unnecessary else --- site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index 0a5a8c5276f9e..71d4bf6cd5cf4 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -42,9 +42,8 @@ const Language = { const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms") if (now.isAfter(deadline)) { return "workspace is shutting down now" - } else { - return now.to(deadline) } + return now.to(deadline) } const duration = dayjs.duration(workspace.ttl / 1_000_000, "milliseconds") From 22f0222ae27dcabbcf76ceaa4204300990e04b9c Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 19 May 2022 18:49:33 +0000 Subject: [PATCH 14/15] fixup api param --- site/src/api/api.ts | 4 ++-- site/src/api/typesGenerated.ts | 20 +++----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e1aefabc57405..71a3ee5d9a19b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -197,9 +197,9 @@ export const putWorkspaceAutostart = async ( export const putWorkspaceAutostop = async ( workspaceID: string, - autostop: TypesGen.UpdateWorkspaceTTLRequest, + ttl: TypesGen.UpdateWorkspaceTTLRequest, ): Promise => { - const payload = JSON.stringify(autostop) + const payload = JSON.stringify(ttl) await axios.put(`/api/v2/workspaces/${workspaceID}/autostop`, payload, { headers: { ...CONTENT_TYPE_JSON }, }) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 052a2db62edda..9a2c74675761d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -82,7 +82,7 @@ export interface CreateUserRequest { readonly organization_id: string } -// From codersdk/workspaces.go:36:6 +// From codersdk/workspaces.go:34:6 export interface CreateWorkspaceBuildRequest { readonly template_version_id?: string readonly transition: WorkspaceTransition @@ -285,21 +285,15 @@ export interface UpdateUserProfileRequest { readonly username: string } -// From codersdk/workspaces.go:134:6 +// From codersdk/workspaces.go:141:6 export interface UpdateWorkspaceAutostartRequest { readonly schedule: string } -<<<<<<< HEAD // From codersdk/workspaces.go:161:6 -export interface UpdateWorkspaceAutostopRequest { - readonly schedule: string -======= -// From codersdk/workspaces.go:154:6 export interface UpdateWorkspaceTTLRequest { // This is likely an enum in an external package ("time.Duration") readonly ttl?: number ->>>>>>> e46a9a0f (make gen) } // From codersdk/files.go:16:6 @@ -352,7 +346,7 @@ export interface UsersRequest extends Pagination { readonly status?: string } -// From codersdk/workspaces.go:20:6 +// From codersdk/workspaces.go:18:6 export interface Workspace { readonly id: string readonly created_at: string @@ -429,20 +423,12 @@ export interface WorkspaceBuild { readonly job: ProvisionerJob } -<<<<<<< HEAD // From codersdk/workspaces.go:64:6 -======= -// From codersdk/workspaces.go:57:6 ->>>>>>> e46a9a0f (make gen) export interface WorkspaceBuildsRequest extends Pagination { readonly WorkspaceID: string } -<<<<<<< HEAD // From codersdk/workspaces.go:180:6 -======= -// From codersdk/workspaces.go:173:6 ->>>>>>> e46a9a0f (make gen) export interface WorkspaceFilter { readonly OrganizationID: string readonly Owner: string From 50a58236a186dc6e7c6568e137840b9768798a1f Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 19 May 2022 18:56:36 +0000 Subject: [PATCH 15/15] fix FE paths for ttl --- site/src/api/api.ts | 2 +- site/src/testHelpers/handlers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 71a3ee5d9a19b..5f5786e9e495d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -200,7 +200,7 @@ export const putWorkspaceAutostop = async ( ttl: TypesGen.UpdateWorkspaceTTLRequest, ): Promise => { const payload = JSON.stringify(ttl) - await axios.put(`/api/v2/workspaces/${workspaceID}/autostop`, payload, { + await axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { headers: { ...CONTENT_TYPE_JSON }, }) } diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index be6f201a3ceeb..293b1170778ec 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -100,7 +100,7 @@ export const handlers = [ rest.put("/api/v2/workspaces/:workspaceId/autostart", async (req, res, ctx) => { return res(ctx.status(200)) }), - rest.put("/api/v2/workspaces/:workspaceId/autostop", async (req, res, ctx) => { + rest.put("/api/v2/workspaces/:workspaceId/ttl", async (req, res, ctx) => { return res(ctx.status(200)) }), rest.post("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => {