diff --git a/.vscode/settings.json b/.vscode/settings.json index fd36a7aac52e7..8375c7848ab52 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 6ea0ced1835ee..acf929a70edbb 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 ee9e6db267da3..d5621c48af114 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9322,6 +9322,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 40801470b3a95..500d775b37393 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8385,6 +8385,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 89214bd6d958e..378063ed7ec66 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 0000000000000..b62835b6903ba --- /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 768ebc27c86ee..ed8203981f8c6 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 6a82b9a1c98ec..4335041fe1ee3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4572,6 +4572,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": { @@ -4709,25 +4710,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 @@ -5572,6 +5574,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 ba896f5afc8f2..fff7d2d640695 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 8bc4b1e6aedc7..3793f87faffde 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1104,6 +1104,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 3405b6a0cfcb5..0315a9bcdca11 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -719,6 +719,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 = {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: