diff --git a/.vscode/settings.json b/.vscode/settings.json index fd36a7aac5..8375c7848a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,8 @@ "codersdk", "cronstrue", "databasefake", + "dbfake", + "dbgen", "dbtype", "DERP", "derphttp", diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 6ea0ced183..acf929a70e 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -48,6 +48,7 @@ "name": "test-workspace", "autostart_schedule": "CRON_TZ=US/Central 30 9 * * 1-5", "ttl_ms": 28800000, - "last_used_at": "[timestamp]" + "last_used_at": "[timestamp]", + "deleting_at": null } ] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 97102cdd96..f04565d150 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9455,6 +9455,11 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "deleting_at": { + "description": "DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.\nWorkspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.", + "type": "string", + "format": "date-time" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 73d5e689d6..cd13980208 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8499,6 +8499,11 @@ "type": "string", "format": "date-time" }, + "deleting_at": { + "description": "DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.\nWorkspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.", + "type": "string", + "format": "date-time" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 89214bd6d9..378063ed7e 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1169,7 +1169,10 @@ func convertWorkspace( autostartSchedule = &workspace.AutostartSchedule.String } - ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl) + var ( + ttlMillis = convertWorkspaceTTLMillis(workspace.Ttl) + deletingAt = calculateDeletingAt(workspace, template) + ) return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -1188,6 +1191,7 @@ func convertWorkspace( AutostartSchedule: autostartSchedule, TTLMillis: ttlMillis, LastUsedAt: workspace.LastUsedAt, + DeletingAt: deletingAt, } } @@ -1200,6 +1204,22 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 { return &millis } +// Calculate the time of the upcoming workspace deletion, if applicable; otherwise, return nil. +// Workspaces may have impending deletions if InactivityTTL feature is turned on and the workspace is inactive. +func calculateDeletingAt(workspace database.Workspace, template database.Template) *time.Time { + var ( + year, month, day = time.Now().Date() + beginningOfToday = time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()) + ) + // If InactivityTTL is turned off (set to 0), if the workspace has already been deleted, + // or if the workspace was used sometime within the last day, there is no impending deletion + if template.InactivityTTL == 0 || workspace.Deleted || workspace.LastUsedAt.After(beginningOfToday) { + return nil + } + + return ptr.Ref(workspace.LastUsedAt.Add(time.Duration(template.InactivityTTL) * time.Nanosecond)) +} + func validWorkspaceTTLMillis(millis *int64, templateDefault, templateMax time.Duration) (sql.NullInt64, error) { if templateDefault == 0 && templateMax != 0 || (templateMax > 0 && templateDefault > templateMax) { templateDefault = templateMax diff --git a/coderd/workspaces_internal_test.go b/coderd/workspaces_internal_test.go new file mode 100644 index 0000000000..b62835b690 --- /dev/null +++ b/coderd/workspaces_internal_test.go @@ -0,0 +1,82 @@ +package coderd + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/util/ptr" +) + +func Test_calculateDeletingAt(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + workspace database.Workspace + template database.Template + expected *time.Time + }{ + { + name: "DeletingAt", + workspace: database.Workspace{ + Deleted: false, + LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24), // 10 days ago + }, + template: database.Template{ + InactivityTTL: int64(9 * 24 * time.Hour), // 9 days + }, + expected: ptr.Ref(time.Now().Add(time.Duration(-1) * time.Hour * 24)), // yesterday + }, + { + name: "InactivityTTLUnset", + workspace: database.Workspace{ + Deleted: false, + LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24), + }, + template: database.Template{ + InactivityTTL: 0, + }, + expected: nil, + }, + { + name: "DeletedWorkspace", + workspace: database.Workspace{ + Deleted: true, + LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24), + }, + template: database.Template{ + InactivityTTL: int64(9 * 24 * time.Hour), + }, + expected: nil, + }, + { + name: "ActiveWorkspace", + workspace: database.Workspace{ + Deleted: true, + LastUsedAt: time.Now().Add(time.Duration(-5) * time.Hour), // 5 hours ago + }, + template: database.Template{ + InactivityTTL: int64(1 * 24 * time.Hour), // 1 day + }, + expected: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + found := calculateDeletingAt(tc.workspace, tc.template) + if tc.expected == nil { + require.Nil(t, found, "impending deletion should be nil") + } else { + require.NotNil(t, found) + require.WithinDuration(t, *tc.expected, *found, time.Second, "incorrect impending deletion") + } + }) + } +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 768ebc27c8..ed8203981f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -34,6 +34,10 @@ type Workspace struct { AutostartSchedule *string `json:"autostart_schedule,omitempty"` TTLMillis *int64 `json:"ttl_ms,omitempty"` LastUsedAt time.Time `json:"last_used_at" format:"date-time"` + + // DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. + // Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. + DeletingAt *time.Time `json:"deleting_at" format:"date-time"` } type WorkspacesRequest struct { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 33ace92113..6f191d774f 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4594,6 +4594,7 @@ Parameter represents a set value for the scope. { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -4731,25 +4732,26 @@ Parameter represents a set value for the scope. ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | ----------- | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | +| `id` | string | false | | | +| `last_used_at` | string | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | ## codersdk.WorkspaceAgent @@ -5596,6 +5598,7 @@ Parameter represents a set value for the scope. { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index ba896f5afc..fff7d2d640 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -56,6 +56,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -228,6 +229,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -423,6 +425,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -592,6 +595,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ { "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4e026c1498..9e368ee93d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1113,6 +1113,7 @@ export interface Workspace { readonly autostart_schedule?: string readonly ttl_ms?: number readonly last_used_at: string + readonly deleting_at?: string } // From codersdk/workspaceagents.go diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c9458a79af..eb5179853d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -721,6 +721,7 @@ export const MockWorkspace: TypesGen.Workspace = { ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild, last_used_at: "2022-05-16T15:29:10.302441433Z", + deleting_at: "0001-01-01T00:00:00Z", } export const MockStoppedWorkspace: TypesGen.Workspace = {