From 7f14b50dbed5f72018d774f9e087ee24b7d081bc Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 24 Aug 2023 13:25:54 -0500 Subject: [PATCH 01/40] chore: rename locked to dormant (#9290) * chore: rename locked to dormant - The following columns have been updated: - workspace.locked_at -> dormant_at - template.inactivity_ttl -> time_til_dormant - template.locked_ttl -> time_til_dormant_autodelete This change has also been reflected in the SDK. A route has also been updated from /workspaces//lock to /workspaces//dormant --- cli/templatecreate.go | 2 +- cli/templateedit.go | 2 +- cli/templateedit_test.go | 8 +- cli/testdata/coder_list_--output_json.golden | 2 +- coderd/apidoc/docs.go | 70 +++--- coderd/apidoc/swagger.json | 70 +++--- coderd/autobuild/lifecycle_executor.go | 56 ++--- coderd/autobuild/lifecycle_executor_test.go | 2 +- coderd/coderd.go | 2 +- coderd/database/dbauthz/dbauthz.go | 18 +- coderd/database/dbfake/dbfake.go | 86 ++++---- coderd/database/dbmetrics/dbmetrics.go | 20 +- coderd/database/dbmock/dbmock.go | 42 ++-- coderd/database/dump.sql | 10 +- .../migrations/000151_rename_locked.down.sql | 26 +++ .../migrations/000151_rename_locked.up.sql | 25 +++ coderd/database/modelmethods.go | 18 +- coderd/database/modelqueries.go | 8 +- coderd/database/models.go | 14 +- coderd/database/querier.go | 4 +- coderd/database/queries.sql.go | 180 ++++++++-------- coderd/database/queries/templates.sql | 4 +- coderd/database/queries/workspaces.sql | 48 ++--- coderd/database/sqlc.yaml | 3 +- coderd/rbac/object.go | 6 +- coderd/rbac/object_gen.go | 2 +- coderd/rbac/roles.go | 6 +- coderd/rbac/roles_test.go | 4 +- coderd/schedule/template.go | 32 +-- coderd/searchquery/search.go | 8 +- coderd/templates.go | 94 ++++---- coderd/templates_test.go | 90 ++++---- coderd/workspaces.go | 42 ++-- coderd/workspaces_test.go | 74 +++---- codersdk/organizations.go | 10 +- codersdk/templates.go | 30 +-- codersdk/workspaces.go | 29 +-- docs/admin/audit-logs.md | 26 +-- docs/api/schemas.md | 120 +++++------ docs/api/templates.md | 30 +-- docs/api/workspaces.md | 134 ++++++------ enterprise/audit/table.go | 6 +- enterprise/coderd/schedule/template.go | 32 +-- enterprise/coderd/schedule/template_test.go | 12 +- enterprise/coderd/templates_test.go | 200 +++++++++--------- enterprise/coderd/workspaces_test.go | 126 +++++------ site/src/api/api.ts | 10 +- site/src/api/typesGenerated.ts | 20 +- .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 6 +- .../components/WorkspaceActions/constants.ts | 2 +- .../ImpendingDeletionBadge.tsx | 17 -- .../ImpendingDeletionBanner.tsx | 16 +- .../src/components/WorkspaceDeletion/index.ts | 1 - .../TemplateSettingsForm.tsx | 2 +- .../TemplateSettingsPage.test.tsx | 6 +- .../InactivityDialog.stories.tsx | 19 -- .../TemplateScheduleForm/InactivityDialog.tsx | 59 ------ .../TemplateScheduleForm.tsx | 91 ++++---- .../TemplateScheduleForm/formHelpers.tsx | 8 +- .../useWorkspacesToBeDeleted.ts | 16 +- .../TemplateSchedulePage.test.tsx | 42 ++-- .../pages/WorkspacesPage/WorkspacesPage.tsx | 24 +-- .../WorkspacesPage/WorkspacesPageView.tsx | 14 +- .../pages/WorkspacesPage/filter/filter.tsx | 4 +- site/src/testHelpers/entities.ts | 4 +- site/src/utils/filters.ts | 2 +- .../xServices/workspace/workspaceXService.ts | 2 +- 67 files changed, 1083 insertions(+), 1115 deletions(-) create mode 100644 coderd/database/migrations/000151_rename_locked.down.sql create mode 100644 coderd/database/migrations/000151_rename_locked.up.sql delete mode 100644 site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx delete mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx delete mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 9a54bf814eb60..38f5d7d0d7fd0 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -134,7 +134,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { VersionID: job.ID, DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()), FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()), - InactivityTTLMillis: ptr.Ref(inactivityTTL.Milliseconds()), + TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()), DisableEveryoneGroupAccess: disableEveryone, } diff --git a/cli/templateedit.go b/cli/templateedit.go index 7ce8fb00daec0..5fcd73c432f58 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -104,7 +104,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { Weeks: restartRequirementWeeks, }, FailureTTLMillis: failureTTL.Milliseconds(), - InactivityTTLMillis: inactivityTTL.Milliseconds(), + TimeTilDormantMillis: inactivityTTL.Milliseconds(), AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, AllowUserAutostart: allowUserAutostart, AllowUserAutostop: allowUserAutostop, diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 775a25f91c6bd..0aff5166e9ca8 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -752,7 +752,7 @@ func TestTemplateEdit(t *testing.T) { ctr.DefaultTTLMillis = nil ctr.RestartRequirement = nil ctr.FailureTTLMillis = nil - ctr.InactivityTTLMillis = nil + ctr.TimeTilDormantMillis = nil }) // Test the cli command with --allow-user-autostart. @@ -798,7 +798,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) - assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis) + assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis) }) t.Run("BlockedNotEntitled", func(t *testing.T) { @@ -892,7 +892,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) - assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis) + assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis) }) t.Run("Entitled", func(t *testing.T) { t.Parallel() @@ -990,7 +990,7 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart) assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop) assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) - assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis) + assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis) }) }) } diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 49e51d408285c..f680c9e210cbc 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -52,7 +52,7 @@ "ttl_ms": 28800000, "last_used_at": "[timestamp]", "deleting_at": null, - "locked_at": null, + "dormant_at": null, "health": { "healthy": true, "failing_agents": [] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0af41e03271ea..2668bf41b024d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6082,7 +6082,7 @@ const docTemplate = `{ } } }, - "/workspaces/{workspace}/extend": { + "/workspaces/{workspace}/dormant": { "put": { "security": [ { @@ -6098,8 +6098,8 @@ const docTemplate = `{ "tags": [ "Workspaces" ], - "summary": "Extend workspace deadline by ID", - "operationId": "extend-workspace-deadline-by-id", + "summary": "Update workspace dormancy status by id.", + "operationId": "update-workspace-dormancy-status-by-id", "parameters": [ { "type": "string", @@ -6110,12 +6110,12 @@ const docTemplate = `{ "required": true }, { - "description": "Extend deadline update request", + "description": "Make a workspace dormant or active", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest" + "$ref": "#/definitions/codersdk.UpdateWorkspaceDormancy" } } ], @@ -6123,13 +6123,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } } }, - "/workspaces/{workspace}/lock": { + "/workspaces/{workspace}/extend": { "put": { "security": [ { @@ -6145,8 +6145,8 @@ const docTemplate = `{ "tags": [ "Workspaces" ], - "summary": "Update workspace lock by id.", - "operationId": "update-workspace-lock-by-id", + "summary": "Extend workspace deadline by ID", + "operationId": "extend-workspace-deadline-by-id", "parameters": [ { "type": "string", @@ -6157,12 +6157,12 @@ const docTemplate = `{ "required": true }, { - "description": "Lock or unlock a workspace", + "description": "Extend deadline update request", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.UpdateWorkspaceLock" + "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest" } } ], @@ -6170,7 +6170,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Workspace" + "$ref": "#/definitions/codersdk.Response" } } } @@ -7394,6 +7394,10 @@ const docTemplate = `{ "description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.", "type": "integer" }, + "delete_ttl_ms": { + "description": "TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder\npermanently deletes dormant workspaces created from this template.", + "type": "integer" + }, "description": { "description": "Description is a description of what the template contains. It must be\nless than 128 bytes.", "type": "string" @@ -7406,6 +7410,10 @@ const docTemplate = `{ "description": "DisplayName is the displayed name of the template.", "type": "string" }, + "dormant_ttl_ms": { + "description": "TimeTilDormantMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", + "type": "integer" + }, "failure_ttl_ms": { "description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.", "type": "integer" @@ -7414,14 +7422,6 @@ const docTemplate = `{ "description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.", "type": "string" }, - "inactivity_ttl_ms": { - "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", - "type": "integer" - }, - "locked_ttl_ms": { - "description": "LockedTTLMillis allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.", - "type": "integer" - }, "max_ttl_ms": { "description": "TODO(@dean): remove max_ttl once restart_requirement is matured", "type": "integer" @@ -9540,7 +9540,7 @@ const docTemplate = `{ "type": "string" }, "failure_ttl_ms": { - "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", + "description": "FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", "type": "integer" }, "icon": { @@ -9550,12 +9550,6 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, - "inactivity_ttl_ms": { - "type": "integer" - }, - "locked_ttl_ms": { - "type": "integer" - }, "max_ttl_ms": { "description": "TODO(@dean): remove max_ttl once restart_requirement is matured", "type": "integer" @@ -9581,6 +9575,12 @@ const docTemplate = `{ } ] }, + "time_til_dormant_autodelete_ms": { + "type": "integer" + }, + "time_til_dormant_ms": { + "type": "integer" + }, "updated_at": { "type": "string", "format": "date-time" @@ -10254,10 +10254,10 @@ const docTemplate = `{ } } }, - "codersdk.UpdateWorkspaceLock": { + "codersdk.UpdateWorkspaceDormancy": { "type": "object", "properties": { - "lock": { + "dormant": { "type": "boolean" } } @@ -10510,7 +10510,12 @@ const docTemplate = `{ "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.", + "description": "DeletingAt indicates the time at which the workspace will be permanently deleted.\nA workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)\nand a value has been specified for time_til_dormant_autodelete on its template.", + "type": "string", + "format": "date-time" + }, + "dormant_at": { + "description": "DormantAt being non-nil indicates a workspace that is dormant.\nA dormant workspace is no longer accessible must be activated.\nIt is subject to deletion if it breaches\nthe duration of the time_til_ field on its template.", "type": "string", "format": "date-time" }, @@ -10533,11 +10538,6 @@ const docTemplate = `{ "latest_build": { "$ref": "#/definitions/codersdk.WorkspaceBuild" }, - "locked_at": { - "description": "LockedAt being non-nil indicates a workspace that has been locked.\nA locked workspace is no longer accessible by a user and must be\nunlocked by an admin. It is subject to deletion if it breaches\nthe duration of the locked_ttl field on its template.", - "type": "string", - "format": "date-time" - }, "name": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 008534328fd70..ccf504f4d7cc7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5366,7 +5366,7 @@ } } }, - "/workspaces/{workspace}/extend": { + "/workspaces/{workspace}/dormant": { "put": { "security": [ { @@ -5376,8 +5376,8 @@ "consumes": ["application/json"], "produces": ["application/json"], "tags": ["Workspaces"], - "summary": "Extend workspace deadline by ID", - "operationId": "extend-workspace-deadline-by-id", + "summary": "Update workspace dormancy status by id.", + "operationId": "update-workspace-dormancy-status-by-id", "parameters": [ { "type": "string", @@ -5388,12 +5388,12 @@ "required": true }, { - "description": "Extend deadline update request", + "description": "Make a workspace dormant or active", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest" + "$ref": "#/definitions/codersdk.UpdateWorkspaceDormancy" } } ], @@ -5401,13 +5401,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } } }, - "/workspaces/{workspace}/lock": { + "/workspaces/{workspace}/extend": { "put": { "security": [ { @@ -5417,8 +5417,8 @@ "consumes": ["application/json"], "produces": ["application/json"], "tags": ["Workspaces"], - "summary": "Update workspace lock by id.", - "operationId": "update-workspace-lock-by-id", + "summary": "Extend workspace deadline by ID", + "operationId": "extend-workspace-deadline-by-id", "parameters": [ { "type": "string", @@ -5429,12 +5429,12 @@ "required": true }, { - "description": "Lock or unlock a workspace", + "description": "Extend deadline update request", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.UpdateWorkspaceLock" + "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest" } } ], @@ -5442,7 +5442,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Workspace" + "$ref": "#/definitions/codersdk.Response" } } } @@ -6587,6 +6587,10 @@ "description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.", "type": "integer" }, + "delete_ttl_ms": { + "description": "TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder\npermanently deletes dormant workspaces created from this template.", + "type": "integer" + }, "description": { "description": "Description is a description of what the template contains. It must be\nless than 128 bytes.", "type": "string" @@ -6599,6 +6603,10 @@ "description": "DisplayName is the displayed name of the template.", "type": "string" }, + "dormant_ttl_ms": { + "description": "TimeTilDormantMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", + "type": "integer" + }, "failure_ttl_ms": { "description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.", "type": "integer" @@ -6607,14 +6615,6 @@ "description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.", "type": "string" }, - "inactivity_ttl_ms": { - "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.", - "type": "integer" - }, - "locked_ttl_ms": { - "description": "LockedTTLMillis allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.", - "type": "integer" - }, "max_ttl_ms": { "description": "TODO(@dean): remove max_ttl once restart_requirement is matured", "type": "integer" @@ -8609,7 +8609,7 @@ "type": "string" }, "failure_ttl_ms": { - "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", + "description": "FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.", "type": "integer" }, "icon": { @@ -8619,12 +8619,6 @@ "type": "string", "format": "uuid" }, - "inactivity_ttl_ms": { - "type": "integer" - }, - "locked_ttl_ms": { - "type": "integer" - }, "max_ttl_ms": { "description": "TODO(@dean): remove max_ttl once restart_requirement is matured", "type": "integer" @@ -8648,6 +8642,12 @@ } ] }, + "time_til_dormant_autodelete_ms": { + "type": "integer" + }, + "time_til_dormant_ms": { + "type": "integer" + }, "updated_at": { "type": "string", "format": "date-time" @@ -9274,10 +9274,10 @@ } } }, - "codersdk.UpdateWorkspaceLock": { + "codersdk.UpdateWorkspaceDormancy": { "type": "object", "properties": { - "lock": { + "dormant": { "type": "boolean" } } @@ -9512,7 +9512,12 @@ "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.", + "description": "DeletingAt indicates the time at which the workspace will be permanently deleted.\nA workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)\nand a value has been specified for time_til_dormant_autodelete on its template.", + "type": "string", + "format": "date-time" + }, + "dormant_at": { + "description": "DormantAt being non-nil indicates a workspace that is dormant.\nA dormant workspace is no longer accessible must be activated.\nIt is subject to deletion if it breaches\nthe duration of the time_til_ field on its template.", "type": "string", "format": "date-time" }, @@ -9535,11 +9540,6 @@ "latest_build": { "$ref": "#/definitions/codersdk.WorkspaceBuild" }, - "locked_at": { - "description": "LockedAt being non-nil indicates a workspace that has been locked.\nA locked workspace is no longer accessible by a user and must be\nunlocked by an admin. It is subject to deletion if it breaches\nthe duration of the locked_ttl field on its template.", - "type": "string", - "format": "date-time" - }, "name": { "type": "string" }, diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 3ce0aad5a4202..f603d7895531d 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -175,35 +175,35 @@ func (e *Executor) runOnce(t time.Time) Stats { } } - // Lock the workspace if it has breached the template's + // Transition the workspace to dormant if it has breached the template's // threshold for inactivity. if reason == database.BuildReasonAutolock { - ws, err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{ + ws, err = tx.UpdateWorkspaceDormantDeletingAt(e.ctx, database.UpdateWorkspaceDormantDeletingAtParams{ ID: ws.ID, - LockedAt: sql.NullTime{ + DormantAt: sql.NullTime{ Time: database.Now(), Valid: true, }, }) if err != nil { - log.Error(e.ctx, "unable to lock workspace", + log.Error(e.ctx, "unable to transition workspace to dormant", slog.F("transition", nextTransition), slog.Error(err), ) return nil } - log.Info(e.ctx, "locked workspace", + log.Info(e.ctx, "dormant workspace", slog.F("last_used_at", ws.LastUsedAt), - slog.F("inactivity_ttl", templateSchedule.InactivityTTL), + slog.F("time_til_dormant", templateSchedule.TimeTilDormant), slog.F("since_last_used_at", time.Since(ws.LastUsedAt)), ) } if reason == database.BuildReasonAutodelete { log.Info(e.ctx, "deleted workspace", - slog.F("locked_at", ws.LockedAt.Time), - slog.F("locked_ttl", templateSchedule.LockedTTL), + slog.F("dormant_at", ws.DormantAt.Time), + slog.F("time_til_dormant_autodelete", templateSchedule.TimeTilDormantAutoDelete), ) } @@ -246,7 +246,7 @@ func (e *Executor) runOnce(t time.Time) Stats { // for this function to return a nil error as well as an empty transition. // In such cases it means no provisioning should occur but the workspace // may be "transitioning" to a new state (such as an inactive, stopped -// workspace transitioning to the locked state). +// workspace transitioning to the dormant state). func getNextTransition( ws database.Workspace, latestBuild database.WorkspaceBuild, @@ -265,13 +265,13 @@ func getNextTransition( return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick): return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil - case isEligibleForLockedStop(ws, templateSchedule, currentTick): + case isEligibleForDormantStop(ws, templateSchedule, currentTick): // Only stop started workspaces. if latestBuild.Transition == database.WorkspaceTransitionStart { return database.WorkspaceTransitionStop, database.BuildReasonAutolock, nil } // We shouldn't transition the workspace but we should still - // lock it. + // make it dormant. return "", database.BuildReasonAutolock, nil case isEligibleForDelete(ws, templateSchedule, currentTick): @@ -288,8 +288,8 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild return false } - // If the workspace is locked we should not autostart it. - if ws.LockedAt.Valid { + // If the workspace is dormant we should not autostart it. + if ws.DormantAt.Valid { return false } @@ -322,8 +322,8 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, return false } - // If the workspace is locked we should not autostop it. - if ws.LockedAt.Valid { + // If the workspace is dormant we should not autostop it. + if ws.DormantAt.Valid { return false } @@ -334,23 +334,23 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, !currentTick.Before(build.Deadline) } -// isEligibleForLockedStop returns true if the workspace should be locked +// isEligibleForDormantStop returns true if the workspace should be dormant // for breaching the inactivity threshold of the template. -func isEligibleForLockedStop(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool { - // Only attempt to lock workspaces not already locked. - return !ws.LockedAt.Valid && - // The template must specify an inactivity TTL. - templateSchedule.InactivityTTL > 0 && - // The workspace must breach the inactivity TTL. - currentTick.Sub(ws.LastUsedAt) > templateSchedule.InactivityTTL +func isEligibleForDormantStop(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool { + // Only attempt against workspaces not already dormant. + return !ws.DormantAt.Valid && + // The template must specify an time_til_dormant value. + templateSchedule.TimeTilDormant > 0 && + // The workspace must breach the time_til_dormant value. + currentTick.Sub(ws.LastUsedAt) > templateSchedule.TimeTilDormant } func isEligibleForDelete(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool { - // Only attempt to delete locked workspaces. - return ws.LockedAt.Valid && ws.DeletingAt.Valid && - // Locked workspaces should only be deleted if a locked_ttl is specified. - templateSchedule.LockedTTL > 0 && - // The workspace must breach the locked_ttl. + // Only attempt to delete dormant workspaces. + return ws.DormantAt.Valid && ws.DeletingAt.Valid && + // Dormant workspaces should only be deleted if a time_til_dormant_autodelete value is specified. + templateSchedule.TimeTilDormantAutoDelete > 0 && + // The workspace must breach the time_til_dormant_autodelete value. currentTick.After(ws.DeletingAt.Time) } diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index b1b854167e4b2..356926d9bbff9 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -737,7 +737,7 @@ func TestExecutorInactiveWorkspace(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) diff --git a/coderd/coderd.go b/coderd/coderd.go index 8fbad62794ab5..4aac3867b60f9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -855,7 +855,7 @@ func New(options *Options) *API { }) r.Get("/watch", api.watchWorkspace) r.Put("/extend", api.putExtendWorkspace) - r.Put("/lock", api.putWorkspaceLock) + r.Put("/dormant", api.putWorkspaceDormant) }) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1fc1c9782235e..9115e9b5ac184 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2636,18 +2636,18 @@ func (q *querier) UpdateWorkspaceDeletedByID(ctx context.Context, arg database.U return deleteQ(q.log, q.auth, fetch, q.db.UpdateWorkspaceDeletedByID)(ctx, arg) } -func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { - fetch := func(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) (database.Workspace, error) { +func (q *querier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) { + fetch := func(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } - return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) + return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceDormantDeletingAt)(ctx, arg) } -func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { - fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { +func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { + fetch := func(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } - return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg) + return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { @@ -2671,12 +2671,12 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg) } -func (q *querier) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { - fetch := func(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) (database.Template, error) { +func (q *querier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error { + fetch := func(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.TemplateID) } - return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesLockedDeletingAtByTemplateID)(ctx, arg) + return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesDormantDeletingAtByTemplateID)(ctx, arg) } func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 9455e8e69009b..b8be4b2e64ef8 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -341,7 +341,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac AutostartSchedule: w.AutostartSchedule, Ttl: w.Ttl, LastUsedAt: w.LastUsedAt, - LockedAt: w.LockedAt, + DormantAt: w.DormantAt, DeletingAt: w.DeletingAt, Count: count, } @@ -3737,14 +3737,14 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no if build.Transition == database.WorkspaceTransitionStart && !build.Deadline.IsZero() && build.Deadline.Before(now) && - !workspace.LockedAt.Valid { + !workspace.DormantAt.Valid { workspaces = append(workspaces, workspace) continue } if build.Transition == database.WorkspaceTransitionStop && workspace.AutostartSchedule.Valid && - !workspace.LockedAt.Valid { + !workspace.DormantAt.Valid { workspaces = append(workspaces, workspace) continue } @@ -3762,11 +3762,11 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no if err != nil { return nil, xerrors.Errorf("get template by ID: %w", err) } - if !workspace.LockedAt.Valid && template.InactivityTTL > 0 { + if !workspace.DormantAt.Valid && template.TimeTilDormant > 0 { workspaces = append(workspaces, workspace) continue } - if workspace.LockedAt.Valid && template.LockedTTL > 0 { + if workspace.DormantAt.Valid && template.TimeTilDormantAutoDelete > 0 { workspaces = append(workspaces, workspace) continue } @@ -5130,8 +5130,8 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database tpl.RestartRequirementDaysOfWeek = arg.RestartRequirementDaysOfWeek tpl.RestartRequirementWeeks = arg.RestartRequirementWeeks tpl.FailureTTL = arg.FailureTTL - tpl.InactivityTTL = arg.InactivityTTL - tpl.LockedTTL = arg.LockedTTL + tpl.TimeTilDormant = arg.TimeTilDormant + tpl.TimeTilDormantAutoDelete = arg.TimeTilDormantAutoDelete q.templates[idx] = tpl return nil } @@ -5699,27 +5699,7 @@ func (q *FakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for index, workspace := range q.workspaces { - if workspace.ID != arg.ID { - continue - } - workspace.LastUsedAt = arg.LastUsedAt - q.workspaces[index] = workspace - return nil - } - - return sql.ErrNoRows -} - -func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { +func (q *FakeQuerier) UpdateWorkspaceDormantDeletingAt(_ context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { return database.Workspace{}, err } @@ -5729,12 +5709,12 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat if workspace.ID != arg.ID { continue } - workspace.LockedAt = arg.LockedAt - if workspace.LockedAt.Time.IsZero() { + workspace.DormantAt = arg.DormantAt + if workspace.DormantAt.Time.IsZero() { workspace.LastUsedAt = database.Now() workspace.DeletingAt = sql.NullTime{} } - if !workspace.LockedAt.Time.IsZero() { + if !workspace.DormantAt.Time.IsZero() { var template database.TemplateTable for _, t := range q.templates { if t.ID == workspace.TemplateID { @@ -5745,10 +5725,10 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat if template.ID == uuid.Nil { return database.Workspace{}, xerrors.Errorf("unable to find workspace template") } - if template.LockedTTL > 0 { + if template.TimeTilDormantAutoDelete > 0 { workspace.DeletingAt = sql.NullTime{ Valid: true, - Time: workspace.LockedAt.Time.Add(time.Duration(template.LockedTTL)), + Time: workspace.DormantAt.Time.Add(time.Duration(template.TimeTilDormantAutoDelete)), } } } @@ -5758,6 +5738,26 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat return database.Workspace{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, workspace := range q.workspaces { + if workspace.ID != arg.ID { + continue + } + workspace.LastUsedAt = arg.LastUsedAt + q.workspaces[index] = workspace + return nil + } + + return sql.ErrNoRows +} + func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -5818,7 +5818,7 @@ func (q *FakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { +func (q *FakeQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -5832,22 +5832,22 @@ func (q *FakeQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(_ context.Con continue } - if ws.LockedAt.Time.IsZero() { + if ws.DormantAt.Time.IsZero() { continue } - if !arg.LockedAt.IsZero() { - ws.LockedAt = sql.NullTime{ + if !arg.DormantAt.IsZero() { + ws.DormantAt = sql.NullTime{ Valid: true, - Time: arg.LockedAt, + Time: arg.DormantAt, } } deletingAt := sql.NullTime{ - Valid: arg.LockedTtlMs > 0, + Valid: arg.TimeTilDormantAutodeleteMs > 0, } - if arg.LockedTtlMs > 0 { - deletingAt.Time = ws.LockedAt.Time.Add(time.Duration(arg.LockedTtlMs) * time.Millisecond) + if arg.TimeTilDormantAutodeleteMs > 0 { + deletingAt.Time = ws.DormantAt.Time.Add(time.Duration(arg.TimeTilDormantAutodeleteMs) * time.Millisecond) } ws.DeletingAt = deletingAt q.workspaces[i] = ws @@ -6224,12 +6224,12 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } // We omit locked workspaces by default. - if arg.LockedAt.IsZero() && workspace.LockedAt.Valid { + if arg.DormantAt.IsZero() && workspace.DormantAt.Valid { continue } // Filter out workspaces that are locked after the timestamp. - if !arg.LockedAt.IsZero() && workspace.LockedAt.Time.Before(arg.LockedAt) { + if !arg.DormantAt.IsZero() && workspace.DormantAt.Time.Before(arg.DormantAt) { continue } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 498ca57b504e4..8526eb4da1078 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1607,6 +1607,13 @@ func (m metricsStore) UpdateWorkspaceDeletedByID(ctx context.Context, arg databa return err } +func (m metricsStore) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) { + start := time.Now() + ws, r0 := m.s.UpdateWorkspaceDormantDeletingAt(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateWorkspaceDormantDeletingAt").Observe(time.Since(start).Seconds()) + return ws, r0 +} + func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { start := time.Now() err := m.s.UpdateWorkspaceLastUsedAt(ctx, arg) @@ -1614,13 +1621,6 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas return err } -func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { - start := time.Now() - ws, r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedDeletingAt").Observe(time.Since(start).Seconds()) - return ws, r0 -} - func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { start := time.Now() proxy, err := m.s.UpdateWorkspaceProxy(ctx, arg) @@ -1642,10 +1642,10 @@ func (m metricsStore) UpdateWorkspaceTTL(ctx context.Context, arg database.Updat return r0 } -func (m metricsStore) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { +func (m metricsStore) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error { start := time.Now() - r0 := m.s.UpdateWorkspacesLockedDeletingAtByTemplateID(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateWorkspacesLockedDeletingAtByTemplateID").Observe(time.Since(start).Seconds()) + r0 := m.s.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateWorkspacesDormantDeletingAtByTemplateID").Observe(time.Since(start).Seconds()) return r0 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index ac1e782e7d398..b0ae7955a458d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3379,6 +3379,21 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceDeletedByID(arg0, arg1 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDeletedByID), arg0, arg1) } +// UpdateWorkspaceDormantDeletingAt mocks base method. +func (m *MockStore) UpdateWorkspaceDormantDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateWorkspaceDormantDeletingAt", arg0, arg1) + ret0, _ := ret[0].(database.Workspace) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateWorkspaceDormantDeletingAt indicates an expected call of UpdateWorkspaceDormantDeletingAt. +func (mr *MockStoreMockRecorder) UpdateWorkspaceDormantDeletingAt(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDormantDeletingAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDormantDeletingAt), arg0, arg1) +} + // UpdateWorkspaceLastUsedAt mocks base method. func (m *MockStore) UpdateWorkspaceLastUsedAt(arg0 context.Context, arg1 database.UpdateWorkspaceLastUsedAtParams) error { m.ctrl.T.Helper() @@ -3393,21 +3408,6 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1) } -// UpdateWorkspaceLockedDeletingAt mocks base method. -func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateWorkspaceLockedDeletingAt", arg0, arg1) - ret0, _ := ret[0].(database.Workspace) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateWorkspaceLockedDeletingAt indicates an expected call of UpdateWorkspaceLockedDeletingAt. -func (mr *MockStoreMockRecorder) UpdateWorkspaceLockedDeletingAt(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLockedDeletingAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLockedDeletingAt), arg0, arg1) -} - // UpdateWorkspaceProxy mocks base method. func (m *MockStore) UpdateWorkspaceProxy(arg0 context.Context, arg1 database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { m.ctrl.T.Helper() @@ -3451,18 +3451,18 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceTTL(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceTTL", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceTTL), arg0, arg1) } -// UpdateWorkspacesLockedDeletingAtByTemplateID mocks base method. -func (m *MockStore) UpdateWorkspacesLockedDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { +// UpdateWorkspacesDormantDeletingAtByTemplateID mocks base method. +func (m *MockStore) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateWorkspacesLockedDeletingAtByTemplateID", arg0, arg1) + ret := m.ctrl.Call(m, "UpdateWorkspacesDormantDeletingAtByTemplateID", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// UpdateWorkspacesLockedDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesLockedDeletingAtByTemplateID. -func (mr *MockStoreMockRecorder) UpdateWorkspacesLockedDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call { +// UpdateWorkspacesDormantDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesDormantDeletingAtByTemplateID. +func (mr *MockStoreMockRecorder) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesLockedDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesLockedDeletingAtByTemplateID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDormantDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDormantDeletingAtByTemplateID), arg0, arg1) } // UpsertAppSecurityKey mocks base method. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 71106e5da771d..a2767c9cfd5e1 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -635,8 +635,8 @@ CREATE TABLE templates ( allow_user_autostart boolean DEFAULT true NOT NULL, allow_user_autostop boolean DEFAULT true NOT NULL, failure_ttl bigint DEFAULT 0 NOT NULL, - inactivity_ttl bigint DEFAULT 0 NOT NULL, - locked_ttl bigint DEFAULT 0 NOT NULL, + time_til_dormant bigint DEFAULT 0 NOT NULL, + time_til_dormant_autodelete bigint DEFAULT 0 NOT NULL, restart_requirement_days_of_week smallint DEFAULT 0 NOT NULL, restart_requirement_weeks bigint DEFAULT 0 NOT NULL ); @@ -676,8 +676,8 @@ CREATE VIEW template_with_users AS templates.allow_user_autostart, templates.allow_user_autostop, templates.failure_ttl, - templates.inactivity_ttl, - templates.locked_ttl, + templates.time_til_dormant, + templates.time_til_dormant_autodelete, templates.restart_requirement_days_of_week, templates.restart_requirement_weeks, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, @@ -1003,7 +1003,7 @@ CREATE TABLE workspaces ( autostart_schedule text, ttl bigint, last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL, - locked_at timestamp with time zone, + dormant_at timestamp with time zone, deleting_at timestamp with time zone ); diff --git a/coderd/database/migrations/000151_rename_locked.down.sql b/coderd/database/migrations/000151_rename_locked.down.sql new file mode 100644 index 0000000000000..4dfb254268fa2 --- /dev/null +++ b/coderd/database/migrations/000151_rename_locked.down.sql @@ -0,0 +1,26 @@ +BEGIN; + +ALTER TABLE templates RENAME COLUMN time_til_dormant TO inactivity_ttl; +ALTER TABLE templates RENAME COLUMN time_til_dormant_autodelete TO locked_ttl; +ALTER TABLE workspaces RENAME COLUMN dormant_at TO locked_at; + +-- Update the template_with_users view; +DROP VIEW template_with_users; +-- If you need to update this view, put 'DROP VIEW template_with_users;' before this. +CREATE VIEW + template_with_users +AS + SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username + FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id; + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; + +COMMIT; diff --git a/coderd/database/migrations/000151_rename_locked.up.sql b/coderd/database/migrations/000151_rename_locked.up.sql new file mode 100644 index 0000000000000..ae72c7efa98cb --- /dev/null +++ b/coderd/database/migrations/000151_rename_locked.up.sql @@ -0,0 +1,25 @@ +BEGIN; +ALTER TABLE templates RENAME COLUMN inactivity_ttl TO time_til_dormant; +ALTER TABLE templates RENAME COLUMN locked_ttl TO time_til_dormant_autodelete; +ALTER TABLE workspaces RENAME COLUMN locked_at TO dormant_at; + +-- Update the template_with_users view;a +DROP VIEW template_with_users; +-- If you need to update this view, put 'DROP VIEW template_with_users;' before this. +CREATE VIEW + template_with_users +AS + SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username + FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id; + +COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.'; + +COMMIT; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 364b812ac3c94..1cccdd949ecc8 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -146,8 +146,8 @@ func (w Workspace) RBACObject() rbac.Object { func (w Workspace) ExecutionRBAC() rbac.Object { // If a workspace is locked it cannot be accessed. - if w.LockedAt.Valid { - return w.LockedRBAC() + if w.DormantAt.Valid { + return w.DormantRBAC() } return rbac.ResourceWorkspaceExecution. @@ -158,8 +158,8 @@ func (w Workspace) ExecutionRBAC() rbac.Object { func (w Workspace) ApplicationConnectRBAC() rbac.Object { // If a workspace is locked it cannot be accessed. - if w.LockedAt.Valid { - return w.LockedRBAC() + if w.DormantAt.Valid { + return w.DormantRBAC() } return rbac.ResourceWorkspaceApplicationConnect. @@ -173,9 +173,9 @@ func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Objec // However we need to allow stopping a workspace by a caller once a workspace // is locked (e.g. for autobuild). Additionally, if a user wants to delete // a locked workspace, they shouldn't have to have it unlocked first. - if w.LockedAt.Valid && transition != WorkspaceTransitionStop && + if w.DormantAt.Valid && transition != WorkspaceTransitionStop && transition != WorkspaceTransitionDelete { - return w.LockedRBAC() + return w.DormantRBAC() } return rbac.ResourceWorkspaceBuild. @@ -184,8 +184,8 @@ func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Objec WithOwner(w.OwnerID.String()) } -func (w Workspace) LockedRBAC() rbac.Object { - return rbac.ResourceWorkspaceLocked. +func (w Workspace) DormantRBAC() rbac.Object { + return rbac.ResourceWorkspaceDormant. WithID(w.ID). InOrg(w.OrganizationID). WithOwner(w.OwnerID.String()) @@ -355,7 +355,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace { AutostartSchedule: r.AutostartSchedule, Ttl: r.Ttl, LastUsedAt: r.LastUsedAt, - LockedAt: r.LockedAt, + DormantAt: r.DormantAt, DeletingAt: r.DeletingAt, } } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 193a046f5cec1..5ccf3282e677c 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -81,8 +81,8 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.AllowUserAutostart, &i.AllowUserAutostop, &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, + &i.TimeTilDormant, + &i.TimeTilDormantAutoDelete, &i.RestartRequirementDaysOfWeek, &i.RestartRequirementWeeks, &i.CreatedByAvatarURL, @@ -217,7 +217,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, - arg.LockedAt, + arg.DormantAt, arg.LastUsedBefore, arg.LastUsedAfter, arg.Offset, @@ -242,7 +242,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, &i.TemplateName, &i.TemplateVersionID, diff --git a/coderd/database/models.go b/coderd/database/models.go index e795049c16413..85f90020d9fc1 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1729,8 +1729,8 @@ type Template struct { AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"` AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"` FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` - InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"` - LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` + TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"` + TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"` RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"` RestartRequirementWeeks int64 `db:"restart_requirement_weeks" json:"restart_requirement_weeks"` CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"` @@ -1761,10 +1761,10 @@ type TemplateTable struct { // Allow users to specify an autostart schedule for workspaces (enterprise). AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"` // Allow users to specify custom autostop values for workspaces (enterprise). - AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"` - FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` - InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"` - LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` + AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"` + FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` + TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"` + TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"` // A bitmap of days of week to restart the workspace on, starting with Monday as the 0th bit, and Sunday as the 6th bit. The 7th bit is unused. RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"` // The number of weeks between restarts. 0 or 1 weeks means "every week", 2 week means "every second week", etc. Weeks are counted from January 2, 2023, which is the first Monday of 2023. This is to ensure workspaces are started consistently for all customers on the same n-week cycles. @@ -1903,7 +1903,7 @@ type Workspace struct { AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` Ttl sql.NullInt64 `db:"ttl" json:"ttl"` LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` - LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a217648035e90..520266bd1d25c 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -292,13 +292,13 @@ type sqlcQuerier interface { UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error + UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) // This allows editing the properties of a workspace proxy. UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error - UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error + UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error UpsertAppSecurityKey(ctx context.Context, value string) error // The default proxy is implied and not actually stored in the database. // So we need to store it's configuration here for display purposes. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9d3fefccd842a..364ae4c546267 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4317,7 +4317,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users WHERE @@ -4350,8 +4350,8 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.AllowUserAutostart, &i.AllowUserAutostop, &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, + &i.TimeTilDormant, + &i.TimeTilDormantAutoDelete, &i.RestartRequirementDaysOfWeek, &i.RestartRequirementWeeks, &i.CreatedByAvatarURL, @@ -4362,7 +4362,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates WHERE @@ -4403,8 +4403,8 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.AllowUserAutostart, &i.AllowUserAutostop, &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, + &i.TimeTilDormant, + &i.TimeTilDormantAutoDelete, &i.RestartRequirementDaysOfWeek, &i.RestartRequirementWeeks, &i.CreatedByAvatarURL, @@ -4414,7 +4414,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates ORDER BY (name, id) ASC ` @@ -4448,8 +4448,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.AllowUserAutostart, &i.AllowUserAutostop, &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, + &i.TimeTilDormant, + &i.TimeTilDormantAutoDelete, &i.RestartRequirementDaysOfWeek, &i.RestartRequirementWeeks, &i.CreatedByAvatarURL, @@ -4470,7 +4470,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates WHERE @@ -4541,8 +4541,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.AllowUserAutostart, &i.AllowUserAutostop, &i.FailureTTL, - &i.InactivityTTL, - &i.LockedTTL, + &i.TimeTilDormant, + &i.TimeTilDormantAutoDelete, &i.RestartRequirementDaysOfWeek, &i.RestartRequirementWeeks, &i.CreatedByAvatarURL, @@ -4732,8 +4732,8 @@ SET restart_requirement_days_of_week = $7, restart_requirement_weeks = $8, failure_ttl = $9, - inactivity_ttl = $10, - locked_ttl = $11 + time_til_dormant = $10, + time_til_dormant_autodelete = $11 WHERE id = $1 ` @@ -4748,8 +4748,8 @@ type UpdateTemplateScheduleByIDParams struct { RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"` RestartRequirementWeeks int64 `db:"restart_requirement_weeks" json:"restart_requirement_weeks"` FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"` - InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"` - LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"` + TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"` + TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"` } func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error { @@ -4763,8 +4763,8 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT arg.RestartRequirementDaysOfWeek, arg.RestartRequirementWeeks, arg.FailureTTL, - arg.InactivityTTL, - arg.LockedTTL, + arg.TimeTilDormant, + arg.TimeTilDormantAutoDelete, ) return err } @@ -9104,7 +9104,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at FROM workspaces WHERE @@ -9147,7 +9147,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9155,7 +9155,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at FROM workspaces WHERE @@ -9179,7 +9179,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9187,7 +9187,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at FROM workspaces WHERE @@ -9218,7 +9218,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9226,7 +9226,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at FROM workspaces WHERE @@ -9276,7 +9276,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9284,7 +9284,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace const getWorkspaces = `-- name: GetWorkspaces :many SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at, + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, COALESCE(template_name.template_name, 'unknown') as template_name, latest_build.template_version_id, latest_build.template_version_name, @@ -9468,13 +9468,13 @@ WHERE ) > 0 ELSE true END - -- Filter by locked workspaces. By default we do not return locked + -- Filter by dormant workspaces. By default we do not return dormant -- workspaces since they are considered soft-deleted. AND CASE WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN - locked_at IS NOT NULL AND locked_at >= $10 + dormant_at IS NOT NULL AND dormant_at >= $10 ELSE - locked_at IS NULL + dormant_at IS NULL END -- Filter by last_used AND CASE @@ -9515,7 +9515,7 @@ type GetWorkspacesParams struct { Name string `db:"name" json:"name"` HasAgent string `db:"has_agent" json:"has_agent"` AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"` - LockedAt time.Time `db:"locked_at" json:"locked_at"` + DormantAt time.Time `db:"dormant_at" json:"dormant_at"` LastUsedBefore time.Time `db:"last_used_before" json:"last_used_before"` LastUsedAfter time.Time `db:"last_used_after" json:"last_used_after"` Offset int32 `db:"offset_" json:"offset_"` @@ -9534,7 +9534,7 @@ type GetWorkspacesRow struct { AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` Ttl sql.NullInt64 `db:"ttl" json:"ttl"` LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` - LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` TemplateName string `db:"template_name" json:"template_name"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` @@ -9553,7 +9553,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, - arg.LockedAt, + arg.DormantAt, arg.LastUsedBefore, arg.LastUsedAfter, arg.Offset, @@ -9578,7 +9578,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, &i.TemplateName, &i.TemplateVersionID, @@ -9600,7 +9600,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at FROM workspaces LEFT JOIN @@ -9649,17 +9649,17 @@ WHERE ) OR -- If the workspace's template has an inactivity_ttl set - -- it may be eligible for locking. + -- it may be eligible for dormancy. ( - templates.inactivity_ttl > 0 AND - workspaces.locked_at IS NULL + templates.time_til_dormant > 0 AND + workspaces.dormant_at IS NULL ) OR - -- If the workspace's template has a locked_ttl set - -- and the workspace is already locked + -- If the workspace's template has a time_til_dormant_autodelete set + -- and the workspace is already dormant. ( - templates.locked_ttl > 0 AND - workspaces.locked_at IS NOT NULL + templates.time_til_dormant_autodelete > 0 AND + workspaces.dormant_at IS NOT NULL ) ) AND workspaces.deleted = 'false' ` @@ -9685,7 +9685,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ); err != nil { return nil, err @@ -9716,7 +9716,7 @@ INSERT INTO last_used_at ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at ` type InsertWorkspaceParams struct { @@ -9758,7 +9758,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9790,7 +9790,7 @@ SET WHERE id = $1 AND deleted = false -RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at +RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at ` type UpdateWorkspaceParams struct { @@ -9813,7 +9813,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err @@ -9857,52 +9857,33 @@ func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateW return err } -const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec -UPDATE - workspaces -SET - last_used_at = $2 -WHERE - id = $1 -` - -type UpdateWorkspaceLastUsedAtParams struct { - ID uuid.UUID `db:"id" json:"id"` - LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` -} - -func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt) - return err -} - -const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :one +const updateWorkspaceDormantDeletingAt = `-- name: UpdateWorkspaceDormantDeletingAt :one UPDATE workspaces SET - locked_at = $2, - -- When a workspace is unlocked we want to update the last_used_at to avoid the workspace getting re-locked. - -- if we're locking the workspace then we leave it alone. + dormant_at = $2, + -- When a workspace is active we want to update the last_used_at to avoid the workspace going + -- immediately dormant. If we're transition the workspace to dormant then we leave it alone. last_used_at = CASE WHEN $2::timestamptz IS NULL THEN now() at time zone 'utc' ELSE last_used_at END, - -- If locked_at is null (meaning unlocked) or the template-defined locked_ttl is 0 we should set - -- deleting_at to NULL else set it to the locked_at + locked_ttl duration. - deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.locked_ttl = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.locked_ttl / 1000000 END + -- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set + -- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration. + deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.time_til_dormant_autodelete / 1000000 END FROM templates WHERE workspaces.template_id = templates.id AND workspaces.id = $1 -RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at +RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at ` -type UpdateWorkspaceLockedDeletingAtParams struct { - ID uuid.UUID `db:"id" json:"id"` - LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` +type UpdateWorkspaceDormantDeletingAtParams struct { + ID uuid.UUID `db:"id" json:"id"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` } -func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) { - row := q.db.QueryRowContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt) +func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) { + row := q.db.QueryRowContext(ctx, updateWorkspaceDormantDeletingAt, arg.ID, arg.DormantAt) var i Workspace err := row.Scan( &i.ID, @@ -9916,12 +9897,31 @@ func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg Up &i.AutostartSchedule, &i.Ttl, &i.LastUsedAt, - &i.LockedAt, + &i.DormantAt, &i.DeletingAt, ) return i, err } +const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec +UPDATE + workspaces +SET + last_used_at = $2 +WHERE + id = $1 +` + +type UpdateWorkspaceLastUsedAtParams struct { + ID uuid.UUID `db:"id" json:"id"` + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` +} + +func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt) + return err +} + const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec UPDATE workspaces @@ -9941,28 +9941,28 @@ func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspace return err } -const updateWorkspacesLockedDeletingAtByTemplateID = `-- name: UpdateWorkspacesLockedDeletingAtByTemplateID :exec +const updateWorkspacesDormantDeletingAtByTemplateID = `-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :exec UPDATE workspaces SET deleting_at = CASE WHEN $1::bigint = 0 THEN NULL WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN ($2::timestamptz) + interval '1 milliseconds' * $1::bigint - ELSE locked_at + interval '1 milliseconds' * $1::bigint + ELSE dormant_at + interval '1 milliseconds' * $1::bigint END, - locked_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE locked_at END + dormant_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE dormant_at END WHERE template_id = $3 AND - locked_at IS NOT NULL + dormant_at IS NOT NULL ` -type UpdateWorkspacesLockedDeletingAtByTemplateIDParams struct { - LockedTtlMs int64 `db:"locked_ttl_ms" json:"locked_ttl_ms"` - LockedAt time.Time `db:"locked_at" json:"locked_at"` - TemplateID uuid.UUID `db:"template_id" json:"template_id"` +type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct { + TimeTilDormantAutodeleteMs int64 `db:"time_til_dormant_autodelete_ms" json:"time_til_dormant_autodelete_ms"` + DormantAt time.Time `db:"dormant_at" json:"dormant_at"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` } -func (q *sqlQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspacesLockedDeletingAtByTemplateID, arg.LockedTtlMs, arg.LockedAt, arg.TemplateID) +func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 7f4c9ce5de4ab..5387bea009c2d 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -121,8 +121,8 @@ SET restart_requirement_days_of_week = $7, restart_requirement_weeks = $8, failure_ttl = $9, - inactivity_ttl = $10, - locked_ttl = $11 + time_til_dormant = $10, + time_til_dormant_autodelete = $11 WHERE id = $1 ; diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 1ff5971d3266d..0aa073301eb8f 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -259,13 +259,13 @@ WHERE ) > 0 ELSE true END - -- Filter by locked workspaces. By default we do not return locked + -- Filter by dormant workspaces. By default we do not return dormant -- workspaces since they are considered soft-deleted. AND CASE - WHEN @locked_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN - locked_at IS NOT NULL AND locked_at >= @locked_at + WHEN @dormant_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN + dormant_at IS NOT NULL AND dormant_at >= @dormant_at ELSE - locked_at IS NULL + dormant_at IS NULL END -- Filter by last_used AND CASE @@ -479,31 +479,31 @@ WHERE ) OR -- If the workspace's template has an inactivity_ttl set - -- it may be eligible for locking. + -- it may be eligible for dormancy. ( - templates.inactivity_ttl > 0 AND - workspaces.locked_at IS NULL + templates.time_til_dormant > 0 AND + workspaces.dormant_at IS NULL ) OR - -- If the workspace's template has a locked_ttl set - -- and the workspace is already locked + -- If the workspace's template has a time_til_dormant_autodelete set + -- and the workspace is already dormant. ( - templates.locked_ttl > 0 AND - workspaces.locked_at IS NOT NULL + templates.time_til_dormant_autodelete > 0 AND + workspaces.dormant_at IS NOT NULL ) ) AND workspaces.deleted = 'false'; --- name: UpdateWorkspaceLockedDeletingAt :one +-- name: UpdateWorkspaceDormantDeletingAt :one UPDATE workspaces SET - locked_at = $2, - -- When a workspace is unlocked we want to update the last_used_at to avoid the workspace getting re-locked. - -- if we're locking the workspace then we leave it alone. + dormant_at = $2, + -- When a workspace is active we want to update the last_used_at to avoid the workspace going + -- immediately dormant. If we're transition the workspace to dormant then we leave it alone. last_used_at = CASE WHEN $2::timestamptz IS NULL THEN now() at time zone 'utc' ELSE last_used_at END, - -- If locked_at is null (meaning unlocked) or the template-defined locked_ttl is 0 we should set - -- deleting_at to NULL else set it to the locked_at + locked_ttl duration. - deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.locked_ttl = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.locked_ttl / 1000000 END + -- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set + -- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration. + deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.time_til_dormant_autodelete / 1000000 END FROM templates WHERE @@ -512,19 +512,19 @@ AND workspaces.id = $1 RETURNING workspaces.*; --- name: UpdateWorkspacesLockedDeletingAtByTemplateID :exec +-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :exec UPDATE workspaces SET deleting_at = CASE - WHEN @locked_ttl_ms::bigint = 0 THEN NULL - WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN (@locked_at::timestamptz) + interval '1 milliseconds' * @locked_ttl_ms::bigint - ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint + WHEN @time_til_dormant_autodelete_ms::bigint = 0 THEN NULL + WHEN @dormant_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN (@dormant_at::timestamptz) + interval '1 milliseconds' * @time_til_dormant_autodelete_ms::bigint + ELSE dormant_at + interval '1 milliseconds' * @time_til_dormant_autodelete_ms::bigint END, - locked_at = CASE WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @locked_at::timestamptz ELSE locked_at END + dormant_at = CASE WHEN @dormant_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @dormant_at::timestamptz ELSE dormant_at END WHERE template_id = @template_id AND - locked_at IS NOT NULL; + dormant_at IS NOT NULL; -- name: UpdateTemplateWorkspacesLastUsedAt :exec UPDATE workspaces diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index ca1a2324e9ab0..7718b01e0335e 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -67,9 +67,8 @@ overrides: motd_file: MOTDFile uuid: UUID failure_ttl: FailureTTL - inactivity_ttl: InactivityTTL + time_til_dormant_autodelete: TimeTilDormantAutoDelete eof: EOF - locked_ttl: LockedTTL template_ids: TemplateIDs active_user_ids: ActiveUserIDs diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 39f57c7fcc6da..1e3f1f45e59ea 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -37,10 +37,10 @@ var ( Type: "workspace_build", } - // ResourceWorkspaceLocked is returned if a workspace is locked. + // ResourceWorkspaceDormant is returned if a workspace is dormant. // It grants restricted permissions on workspace builds. - ResourceWorkspaceLocked = Object{ - Type: "workspace_locked", + ResourceWorkspaceDormant = Object{ + Type: "workspace_dormant", } // ResourceWorkspaceProxy CRUD. Org diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 10506b3f719c2..86a03d4552d45 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -26,8 +26,8 @@ func AllResources() []Object { ResourceWorkspace, ResourceWorkspaceApplicationConnect, ResourceWorkspaceBuild, + ResourceWorkspaceDormant, ResourceWorkspaceExecution, - ResourceWorkspaceLocked, ResourceWorkspaceProxy, } } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 93aeaca017592..4f159480a0491 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -121,7 +121,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { opts = &RoleOptions{} } - ownerAndAdminExceptions := []Object{ResourceWorkspaceLocked} + ownerAndAdminExceptions := []Object{ResourceWorkspaceDormant} if opts.NoOwnerWorkspaceExec { ownerAndAdminExceptions = append(ownerAndAdminExceptions, ResourceWorkspaceExecution, @@ -150,7 +150,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceProvisionerDaemon.Type: {ActionRead}, }), Org: map[string][]Permission{}, - User: append(allPermsExcept(ResourceWorkspaceLocked, ResourceUser, ResourceOrganizationMember), + User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember), Permissions(map[string][]Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -246,7 +246,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { Site: []Permission{}, Org: map[string][]Permission{ // Org admins should not have workspace exec perms. - organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceLocked), + organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceDormant), }, User: []Permission{}, } diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index ad03529d81c0f..fc47413fd19f2 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -319,9 +319,9 @@ func TestRolePermissions(t *testing.T) { }, }, { - Name: "WorkspaceLocked", + Name: "WorkspaceDormant", Actions: rbac.AllActions(), - Resource: rbac.ResourceWorkspaceLocked.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), + Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]authSubject{ true: {}, false: {memberMe, orgAdmin, userAdmin, otherOrgAdmin, otherOrgMember, orgMemberMe, owner, templateAdmin}, diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 0e3774b798358..9c1b2fa5aa787 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -99,24 +99,24 @@ type TemplateScheduleOptions struct { // FailureTTL dictates the duration after which failed workspaces will be // stopped automatically. FailureTTL time.Duration `json:"failure_ttl"` - // InactivityTTL dictates the duration after which inactive workspaces will - // be locked. - InactivityTTL time.Duration `json:"inactivity_ttl"` - // LockedTTL dictates the duration after which locked workspaces will be + // TimeTilDormant dictates the duration after which inactive workspaces will + // go dormant. + TimeTilDormant time.Duration `json:"time_til_dormant"` + // TimeTilDormantAutoDelete dictates the duration after which dormant workspaces will be // permanently deleted. - LockedTTL time.Duration `json:"locked_ttl"` + TimeTilDormantAutoDelete time.Duration `json:"time_til_dormant_autodelete"` // UpdateWorkspaceLastUsedAt updates the template's workspaces' // last_used_at field. This is useful for preventing updates to the - // templates inactivity_ttl immediately triggering a lock action against + // templates inactivity_ttl immediately triggering a dormant action against // workspaces whose last_used_at field violates the new template // inactivity_ttl threshold. UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"` - // UpdateWorkspaceLockedAt updates the template's workspaces' - // locked_at field. This is useful for preventing updates to the + // UpdateWorkspaceDormantAt updates the template's workspaces' + // dormant_at field. This is useful for preventing updates to the // templates locked_ttl immediately triggering a delete action against - // workspaces whose locked_at field violates the new template locked_ttl + // workspaces whose dormant_at field violates the new template time_til_dormant_autodelete // threshold. - UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"` + UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"` } // TemplateScheduleStore provides an interface for retrieving template @@ -150,16 +150,16 @@ func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, te UserAutostopEnabled: true, DefaultTTL: time.Duration(tpl.DefaultTTL), // Disregard the values in the database, since RestartRequirement, - // FailureTTL, InactivityTTL, and LockedTTL are enterprise features. + // FailureTTL, TimeTilDormant, and TimeTilDormantAutoDelete are enterprise features. UseRestartRequirement: false, MaxTTL: 0, RestartRequirement: TemplateRestartRequirement{ DaysOfWeek: 0, Weeks: 0, }, - FailureTTL: 0, - InactivityTTL: 0, - LockedTTL: 0, + FailureTTL: 0, + TimeTilDormant: 0, + TimeTilDormantAutoDelete: 0, }, nil } @@ -186,8 +186,8 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp AllowUserAutostart: tpl.AllowUserAutostart, AllowUserAutostop: tpl.AllowUserAutostop, FailureTTL: tpl.FailureTTL, - InactivityTTL: tpl.InactivityTTL, - LockedTTL: tpl.LockedTTL, + TimeTilDormant: tpl.TimeTilDormant, + TimeTilDormantAutoDelete: tpl.TimeTilDormantAutoDelete, }) if err != nil { return xerrors.Errorf("update template schedule: %w", err) diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 17d1990880727..efdde1bb1d2e7 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -114,16 +114,16 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT filter.Name = parser.String(values, "", "name") filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus])) filter.HasAgent = parser.String(values, "", "has-agent") - filter.LockedAt = parser.Time(values, time.Time{}, "locked_at", "2006-01-02") + filter.DormantAt = parser.Time(values, time.Time{}, "dormant_at", "2006-01-02") filter.LastUsedAfter = parser.Time3339Nano(values, time.Time{}, "last_used_after") filter.LastUsedBefore = parser.Time3339Nano(values, time.Time{}, "last_used_before") if _, ok := values["deleting_by"]; ok { postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02")) - // We want to make sure to grab locked workspaces since they + // We want to make sure to grab dormant workspaces since they // are omitted by default. - if filter.LockedAt.IsZero() { - filter.LockedAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + if filter.DormantAt.IsZero() { + filter.DormantAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) } } diff --git a/coderd/templates.go b/coderd/templates.go index f51f42668e1a1..7d6dc46d2bf90 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -219,8 +219,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque restartRequirementDaysOfWeek []string restartRequirementWeeks int64 failureTTL time.Duration - inactivityTTL time.Duration - lockedTTL time.Duration + dormantTTL time.Duration + dormantAutoDeletionTTL time.Duration ) if createTemplate.DefaultTTLMillis != nil { defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond @@ -232,11 +232,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque if createTemplate.FailureTTLMillis != nil { failureTTL = time.Duration(*createTemplate.FailureTTLMillis) * time.Millisecond } - if createTemplate.InactivityTTLMillis != nil { - inactivityTTL = time.Duration(*createTemplate.InactivityTTLMillis) * time.Millisecond + if createTemplate.TimeTilDormantMillis != nil { + dormantTTL = time.Duration(*createTemplate.TimeTilDormantMillis) * time.Millisecond } - if createTemplate.LockedTTLMillis != nil { - lockedTTL = time.Duration(*createTemplate.LockedTTLMillis) * time.Millisecond + if createTemplate.TimeTilDormantAutoDeleteMillis != nil { + dormantAutoDeletionTTL = time.Duration(*createTemplate.TimeTilDormantAutoDeleteMillis) * time.Millisecond } var ( @@ -270,11 +270,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque if failureTTL < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."}) } - if inactivityTTL < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."}) + if dormantTTL < 0 { + validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."}) } - if lockedTTL < 0 { - validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."}) + if dormantAutoDeletionTTL < 0 { + validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."}) } if len(validErrs) > 0 { @@ -340,9 +340,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque DaysOfWeek: restartRequirementDaysOfWeekParsed, Weeks: restartRequirementWeeks, }, - FailureTTL: failureTTL, - InactivityTTL: inactivityTTL, - LockedTTL: lockedTTL, + FailureTTL: failureTTL, + TimeTilDormant: dormantTTL, + TimeTilDormantAutoDelete: dormantAutoDeletionTTL, }) if err != nil { return xerrors.Errorf("set template schedule options: %s", err) @@ -533,13 +533,13 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.FailureTTLMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."}) } - if req.InactivityTTLMillis < 0 { + if req.TimeTilDormantMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."}) } - if req.InactivityTTLMillis < 0 { + if req.TimeTilDormantMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."}) } - if req.LockedTTLMillis < 0 { + if req.TimeTilDormantAutoDeleteMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."}) } @@ -565,8 +565,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { restartRequirementDaysOfWeekParsed == scheduleOpts.RestartRequirement.DaysOfWeek && req.RestartRequirement.Weeks == scheduleOpts.RestartRequirement.Weeks && req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() && - req.InactivityTTLMillis == time.Duration(template.InactivityTTL).Milliseconds() && - req.LockedTTLMillis == time.Duration(template.LockedTTL).Milliseconds() { + req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() && + req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() { return nil } @@ -598,16 +598,16 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond - inactivityTTL := time.Duration(req.InactivityTTLMillis) * time.Millisecond - lockedTTL := time.Duration(req.LockedTTLMillis) * time.Millisecond + inactivityTTL := time.Duration(req.TimeTilDormantMillis) * time.Millisecond + timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * time.Millisecond if defaultTTL != time.Duration(template.DefaultTTL) || maxTTL != time.Duration(template.MaxTTL) || restartRequirementDaysOfWeekParsed != scheduleOpts.RestartRequirement.DaysOfWeek || req.RestartRequirement.Weeks != scheduleOpts.RestartRequirement.Weeks || failureTTL != time.Duration(template.FailureTTL) || - inactivityTTL != time.Duration(template.InactivityTTL) || - lockedTTL != time.Duration(template.LockedTTL) || + inactivityTTL != time.Duration(template.TimeTilDormant) || + timeTilDormantAutoDelete != time.Duration(template.TimeTilDormantAutoDelete) || req.AllowUserAutostart != template.AllowUserAutostart || req.AllowUserAutostop != template.AllowUserAutostop { updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{ @@ -623,10 +623,10 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { Weeks: req.RestartRequirement.Weeks, }, FailureTTL: failureTTL, - InactivityTTL: inactivityTTL, - LockedTTL: lockedTTL, + TimeTilDormant: inactivityTTL, + TimeTilDormantAutoDelete: timeTilDormantAutoDelete, UpdateWorkspaceLastUsedAt: req.UpdateWorkspaceLastUsedAt, - UpdateWorkspaceLockedAt: req.UpdateWorkspaceLockedAt, + UpdateWorkspaceDormantAt: req.UpdateWorkspaceDormantAt, }) if err != nil { return xerrors.Errorf("set template schedule options: %w", err) @@ -738,28 +738,28 @@ func (api *API) convertTemplate( buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID) return codersdk.Template{ - ID: template.ID, - CreatedAt: template.CreatedAt, - UpdatedAt: template.UpdatedAt, - OrganizationID: template.OrganizationID, - Name: template.Name, - DisplayName: template.DisplayName, - Provisioner: codersdk.ProvisionerType(template.Provisioner), - ActiveVersionID: template.ActiveVersionID, - ActiveUserCount: activeCount, - BuildTimeStats: buildTimeStats, - Description: template.Description, - Icon: template.Icon, - DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(), - MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(), - CreatedByID: template.CreatedBy, - CreatedByName: template.CreatedByUsername, - AllowUserAutostart: template.AllowUserAutostart, - AllowUserAutostop: template.AllowUserAutostop, - AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, - FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(), - InactivityTTLMillis: time.Duration(template.InactivityTTL).Milliseconds(), - LockedTTLMillis: time.Duration(template.LockedTTL).Milliseconds(), + ID: template.ID, + CreatedAt: template.CreatedAt, + UpdatedAt: template.UpdatedAt, + OrganizationID: template.OrganizationID, + Name: template.Name, + DisplayName: template.DisplayName, + Provisioner: codersdk.ProvisionerType(template.Provisioner), + ActiveVersionID: template.ActiveVersionID, + ActiveUserCount: activeCount, + BuildTimeStats: buildTimeStats, + Description: template.Description, + Icon: template.Icon, + DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(), + MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(), + CreatedByID: template.CreatedBy, + CreatedByName: template.CreatedByUsername, + AllowUserAutostart: template.AllowUserAutostart, + AllowUserAutostop: template.AllowUserAutostop, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(), + TimeTilDormantMillis: time.Duration(template.TimeTilDormant).Milliseconds(), + TimeTilDormantAutoDeleteMillis: time.Duration(template.TimeTilDormantAutoDelete).Milliseconds(), RestartRequirement: codersdk.TemplateRestartRequirement{ DaysOfWeek: codersdk.BitmapToWeekdays(uint8(template.RestartRequirementDaysOfWeek)), Weeks: template.RestartRequirementWeeks, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 92a67710902dd..fcdb7e64e2e78 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -270,8 +270,8 @@ func TestPostTemplateByOrganization(t *testing.T) { RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: options.RestartRequirement.Weeks, FailureTTL: int64(options.FailureTTL), - InactivityTTL: int64(options.InactivityTTL), - LockedTTL: int64(options.LockedTTL), + TimeTilDormant: int64(options.TimeTilDormant), + TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err @@ -320,8 +320,8 @@ func TestPostTemplateByOrganization(t *testing.T) { RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: options.RestartRequirement.Weeks, FailureTTL: int64(options.FailureTTL), - InactivityTTL: int64(options.InactivityTTL), - LockedTTL: int64(options.LockedTTL), + TimeTilDormant: int64(options.TimeTilDormant), + TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err @@ -598,8 +598,8 @@ func TestPatchTemplateMeta(t *testing.T) { RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: options.RestartRequirement.Weeks, FailureTTL: int64(options.FailureTTL), - InactivityTTL: int64(options.InactivityTTL), - LockedTTL: int64(options.LockedTTL), + TimeTilDormant: int64(options.TimeTilDormant), + TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err @@ -697,9 +697,9 @@ func TestPatchTemplateMeta(t *testing.T) { t.Parallel() const ( - failureTTL = 7 * 24 * time.Hour - inactivityTTL = 180 * 24 * time.Hour - lockedTTL = 360 * 24 * time.Hour + failureTTL = 7 * 24 * time.Hour + inactivityTTL = 180 * 24 * time.Hour + timeTilDormantAutoDelete = 360 * 24 * time.Hour ) t.Run("OK", func(t *testing.T) { @@ -711,12 +711,12 @@ func TestPatchTemplateMeta(t *testing.T) { SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { if atomic.AddInt64(&setCalled, 1) == 2 { require.Equal(t, failureTTL, options.FailureTTL) - require.Equal(t, inactivityTTL, options.InactivityTTL) - require.Equal(t, lockedTTL, options.LockedTTL) + require.Equal(t, inactivityTTL, options.TimeTilDormant) + require.Equal(t, timeTilDormantAutoDelete, options.TimeTilDormantAutoDelete) } template.FailureTTL = int64(options.FailureTTL) - template.InactivityTTL = int64(options.InactivityTTL) - template.LockedTTL = int64(options.LockedTTL) + template.TimeTilDormant = int64(options.TimeTilDormant) + template.TimeTilDormantAutoDelete = int64(options.TimeTilDormantAutoDelete) return template, nil }, }, @@ -725,31 +725,31 @@ func TestPatchTemplateMeta(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) - ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) - ctr.LockedTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, - DefaultTTLMillis: 0, - RestartRequirement: &template.RestartRequirement, - AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, - FailureTTLMillis: failureTTL.Milliseconds(), - InactivityTTLMillis: inactivityTTL.Milliseconds(), - LockedTTLMillis: lockedTTL.Milliseconds(), + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + DefaultTTLMillis: 0, + RestartRequirement: &template.RestartRequirement, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + FailureTTLMillis: failureTTL.Milliseconds(), + TimeTilDormantMillis: inactivityTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(), }) require.NoError(t, err) require.EqualValues(t, 2, atomic.LoadInt64(&setCalled)) require.Equal(t, failureTTL.Milliseconds(), got.FailureTTLMillis) - require.Equal(t, inactivityTTL.Milliseconds(), got.InactivityTTLMillis) - require.Equal(t, lockedTTL.Milliseconds(), got.LockedTTLMillis) + require.Equal(t, inactivityTTL.Milliseconds(), got.TimeTilDormantMillis) + require.Equal(t, timeTilDormantAutoDelete.Milliseconds(), got.TimeTilDormantAutoDeleteMillis) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { @@ -760,29 +760,29 @@ func TestPatchTemplateMeta(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) - ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) - ctr.LockedTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, - DefaultTTLMillis: template.DefaultTTLMillis, - RestartRequirement: &template.RestartRequirement, - AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, - FailureTTLMillis: failureTTL.Milliseconds(), - InactivityTTLMillis: inactivityTTL.Milliseconds(), - LockedTTLMillis: lockedTTL.Milliseconds(), + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + DefaultTTLMillis: template.DefaultTTLMillis, + RestartRequirement: &template.RestartRequirement, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + FailureTTLMillis: failureTTL.Milliseconds(), + TimeTilDormantMillis: inactivityTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(), }) require.NoError(t, err) require.Zero(t, got.FailureTTLMillis) - require.Zero(t, got.InactivityTTLMillis) - require.Zero(t, got.LockedTTLMillis) + require.Zero(t, got.TimeTilDormantMillis) + require.Zero(t, got.TimeTilDormantAutoDeleteMillis) }) }) @@ -989,8 +989,8 @@ func TestPatchTemplateMeta(t *testing.T) { RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: options.RestartRequirement.Weeks, FailureTTL: int64(options.FailureTTL), - InactivityTTL: int64(options.InactivityTTL), - LockedTTL: int64(options.LockedTTL), + TimeTilDormant: int64(options.TimeTilDormant), + TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err @@ -1058,8 +1058,8 @@ func TestPatchTemplateMeta(t *testing.T) { RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: options.RestartRequirement.Weeks, FailureTTL: int64(options.FailureTTL), - InactivityTTL: int64(options.InactivityTTL), - LockedTTL: int64(options.LockedTTL), + TimeTilDormant: int64(options.TimeTilDormant), + TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0f493e5995b24..92e4c029f3777 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -765,43 +765,43 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) } -// @Summary Update workspace lock by id. -// @ID update-workspace-lock-by-id +// @Summary Update workspace dormancy status by id. +// @ID update-workspace-dormancy-status-by-id // @Security CoderSessionToken // @Accept json // @Produce json // @Tags Workspaces // @Param workspace path string true "Workspace ID" format(uuid) -// @Param request body codersdk.UpdateWorkspaceLock true "Lock or unlock a workspace" +// @Param request body codersdk.UpdateWorkspaceDormancy true "Make a workspace dormant or active" // @Success 200 {object} codersdk.Workspace -// @Router /workspaces/{workspace}/lock [put] -func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { +// @Router /workspaces/{workspace}/dormant [put] +func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspace := httpmw.WorkspaceParam(r) - var req codersdk.UpdateWorkspaceLock + var req codersdk.UpdateWorkspaceDormancy if !httpapi.Read(ctx, rw, r, &req) { return } // If the workspace is already in the desired state do nothing! - if workspace.LockedAt.Valid == req.Lock { + if workspace.DormantAt.Valid == req.Dormant { httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{ Message: "Nothing to do!", }) return } - lockedAt := sql.NullTime{ - Valid: req.Lock, + dormantAt := sql.NullTime{ + Valid: req.Dormant, } - if req.Lock { - lockedAt.Time = database.Now() + if req.Dormant { + dormantAt.Time = database.Now() } - workspace, err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{ - ID: workspace.ID, - LockedAt: lockedAt, + workspace, err := api.Database.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{ + ID: workspace.ID, + DormantAt: dormantAt, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -1153,14 +1153,14 @@ func convertWorkspace( autostartSchedule = &workspace.AutostartSchedule.String } - var lockedAt *time.Time - if workspace.LockedAt.Valid { - lockedAt = &workspace.LockedAt.Time + var dormantAt *time.Time + if workspace.DormantAt.Valid { + dormantAt = &workspace.DormantAt.Time } - var deletedAt *time.Time + var deletingAt *time.Time if workspace.DeletingAt.Valid { - deletedAt = &workspace.DeletingAt.Time + deletingAt = &workspace.DeletingAt.Time } failingAgents := []uuid.UUID{} @@ -1192,8 +1192,8 @@ func convertWorkspace( AutostartSchedule: autostartSchedule, TTLMillis: ttlMillis, LastUsedAt: workspace.LastUsedAt, - DeletingAt: deletedAt, - LockedAt: lockedAt, + DeletingAt: deletingAt, + DormantAt: dormantAt, Health: codersdk.WorkspaceHealth{ Healthy: len(failingAgents) == 0, FailingAgents: failingAgents, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index b42f4517db82d..ebedb8497deae 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1363,9 +1363,9 @@ func TestWorkspaceFilterManual(t *testing.T) { TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { if atomic.AddInt64(&setCalled, 1) == 2 { - assert.Equal(t, inactivityTTL, options.InactivityTTL) + assert.Equal(t, inactivityTTL, options.TimeTilDormant) } - template.InactivityTTL = int64(options.InactivityTTL) + template.TimeTilDormant = int64(options.TimeTilDormant) return template, nil }, }, @@ -1385,11 +1385,11 @@ func TestWorkspaceFilterManual(t *testing.T) { defer cancel() template, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - InactivityTTLMillis: inactivityTTL.Milliseconds(), + TimeTilDormantMillis: inactivityTTL.Milliseconds(), }) assert.NoError(t, err) - assert.Equal(t, inactivityTTL.Milliseconds(), template.InactivityTTLMillis) + assert.Equal(t, inactivityTTL.Milliseconds(), template.TimeTilDormantMillis) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) @@ -1404,11 +1404,11 @@ func TestWorkspaceFilterManual(t *testing.T) { assert.NoError(t, err) // we are expecting that no workspaces are returned as user is unlicensed - // and template.InactivityTTL should be 0 + // and template.TimeTilDormant should be 0 assert.Len(t, res.Workspaces, 0) }) - t.Run("LockedAt", func(t *testing.T) { + t.Run("DormantAt", func(t *testing.T) { // this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ @@ -1428,24 +1428,24 @@ func TestWorkspaceFilterManual(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + dormantWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID) - // Create another workspace to validate that we do not return unlocked workspaces. + // Create another workspace to validate that we do not return active workspaces. _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID) - err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ - FilterQuery: fmt.Sprintf("locked_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")), + FilterQuery: fmt.Sprintf("dormant_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")), }) require.NoError(t, err) require.Len(t, res.Workspaces, 1) - require.NotNil(t, res.Workspaces[0].LockedAt) + require.NotNil(t, res.Workspaces[0].DormantAt) }) t.Run("LastUsed", func(t *testing.T) { @@ -2782,21 +2782,21 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) { require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters) } -func TestWorkspaceLock(t *testing.T) { +func TestWorkspaceDormant(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() var ( - client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user = coderdtest.CreateFirstUser(t, client) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - lockedTTL = time.Minute + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user = coderdtest.CreateFirstUser(t, client) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + timeTilDormantAutoDelete = time.Minute ) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](timeTilDormantAutoDelete.Milliseconds()) }) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) @@ -2805,32 +2805,32 @@ func TestWorkspaceLock(t *testing.T) { defer cancel() lastUsedAt := workspace.LastUsedAt - err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") - // The template doesn't have a locked_ttl set so this should be nil. + // The template doesn't have a time_til_dormant_autodelete set so this should be nil. require.Nil(t, workspace.DeletingAt) - require.NotNil(t, workspace.LockedAt) - require.WithinRange(t, *workspace.LockedAt, time.Now().Add(-time.Second*10), time.Now()) + require.NotNil(t, workspace.DormantAt) + require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now()) require.Equal(t, lastUsedAt, workspace.LastUsedAt) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) lastUsedAt = workspace.LastUsedAt - err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: false, + err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: false, }) require.NoError(t, err) workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") - require.Nil(t, workspace.LockedAt) - // The template doesn't have a locked_ttl set so this should be nil. + require.Nil(t, workspace.DormantAt) + // The template doesn't have a time_til_dormant_autodelete set so this should be nil. require.Nil(t, workspace.DeletingAt) - // The last_used_at should get updated when we unlock the workspace. + // The last_used_at should get updated when we activate the workspace. require.True(t, workspace.LastUsedAt.After(lastUsedAt)) }) @@ -2849,23 +2849,23 @@ func TestWorkspaceLock(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) - // Should be able to stop a workspace while it is locked. + // Should be able to stop a workspace while it is dormant. coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) - // Should not be able to start a workspace while it is locked. + // Should not be able to start a workspace while it is dormant. _, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: codersdk.WorkspaceTransition(database.WorkspaceTransitionStart), }) require.Error(t, err) - err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: false, + err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: false, }) require.NoError(t, err) coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 96b026a3197a5..0b4af0e67056a 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -108,12 +108,12 @@ type CreateTemplateRequest struct { // FailureTTLMillis allows optionally specifying the max lifetime before Coder // stops all resources for failed workspaces created from this template. FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"` - // InactivityTTLMillis allows optionally specifying the max lifetime before Coder + // TimeTilDormantMillis allows optionally specifying the max lifetime before Coder // locks inactive workspaces created from this template. - InactivityTTLMillis *int64 `json:"inactivity_ttl_ms,omitempty"` - // LockedTTLMillis allows optionally specifying the max lifetime before Coder - // permanently deletes locked workspaces created from this template. - LockedTTLMillis *int64 `json:"locked_ttl_ms,omitempty"` + TimeTilDormantMillis *int64 `json:"dormant_ttl_ms,omitempty"` + // TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder + // permanently deletes dormant workspaces created from this template. + TimeTilDormantAutoDeleteMillis *int64 `json:"delete_ttl_ms,omitempty"` // DisableEveryoneGroupAccess allows optionally disabling the default // behavior of granting the 'everyone' group access to use the template. diff --git a/codersdk/templates.go b/codersdk/templates.go index f566ac4f2ce32..406933c72b75f 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -44,12 +44,12 @@ type Template struct { AllowUserAutostop bool `json:"allow_user_autostop"` AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"` - // FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their + // FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their // values are used if your license is entitled to use the advanced // template scheduling feature. - FailureTTLMillis int64 `json:"failure_ttl_ms"` - InactivityTTLMillis int64 `json:"inactivity_ttl_ms"` - LockedTTLMillis int64 `json:"locked_ttl_ms"` + FailureTTLMillis int64 `json:"failure_ttl_ms"` + TimeTilDormantMillis int64 `json:"time_til_dormant_ms"` + TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms"` } // WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with @@ -185,22 +185,22 @@ type UpdateTemplateMeta struct { // RestartRequirement can only be set if your license includes the advanced // template scheduling feature. If you attempt to set this value while // unlicensed, it will be ignored. - RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"` - AllowUserAutostart bool `json:"allow_user_autostart,omitempty"` - AllowUserAutostop bool `json:"allow_user_autostop,omitempty"` - AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"` - FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"` - InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"` - LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"` + RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"` + AllowUserAutostart bool `json:"allow_user_autostart,omitempty"` + AllowUserAutostop bool `json:"allow_user_autostop,omitempty"` + AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"` + FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"` + TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"` + TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"` // UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces // spawned from the template. This is useful for preventing workspaces being // immediately locked when updating the inactivity_ttl field to a new, shorter // value. UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"` - // UpdateWorkspaceLockedAt updates the locked_at field of workspaces spawned - // from the template. This is useful for preventing locked workspaces being immediately - // deleted when updating the locked_ttl field to a new, shorter value. - UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"` + // UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned + // from the template. This is useful for preventing dormant workspaces being immediately + // deleted when updating the dormant_ttl field to a new, shorter value. + UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"` } type TemplateExample struct { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 0f43143bfe0fb..d7b191c6273b6 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -35,14 +35,15 @@ type Workspace struct { 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 indicates the time at which the workspace will be permanently deleted. + // A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) + // and a value has been specified for time_til_dormant_autodelete on its template. DeletingAt *time.Time `json:"deleting_at" format:"date-time"` - // LockedAt being non-nil indicates a workspace that has been locked. - // A locked workspace is no longer accessible by a user and must be - // unlocked by an admin. It is subject to deletion if it breaches - // the duration of the locked_ttl field on its template. - LockedAt *time.Time `json:"locked_at" format:"date-time"` + // DormantAt being non-nil indicates a workspace that is dormant. + // A dormant workspace is no longer accessible must be activated. + // It is subject to deletion if it breaches + // the duration of the time_til_ field on its template. + DormantAt *time.Time `json:"dormant_at" format:"date-time"` // Health shows the health of the workspace and information about // what is causing an unhealthy status. Health WorkspaceHealth `json:"health"` @@ -293,14 +294,16 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx return nil } -// UpdateWorkspaceLock is a request to lock or unlock a workspace. -type UpdateWorkspaceLock struct { - Lock bool `json:"lock"` +// UpdateWorkspaceDormancy is a request to activate or make a workspace dormant. +// A value of false will activate a dormant workspace. +type UpdateWorkspaceDormancy struct { + Dormant bool `json:"dormant"` } -// UpdateWorkspaceLock locks or unlocks a workspace. -func (c *Client) UpdateWorkspaceLock(ctx context.Context, id uuid.UUID, req UpdateWorkspaceLock) error { - path := fmt.Sprintf("/api/v2/workspaces/%s/lock", id.String()) +// UpdateWorkspaceDormancy sets a workspace as dormant if dormant=true and activates a dormant workspace +// if dormant=false. +func (c *Client) UpdateWorkspaceDormancy(ctx context.Context, id uuid.UUID, req UpdateWorkspaceDormancy) error { + path := fmt.Sprintf("/api/v2/workspaces/%s/dormant", id.String()) res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { return xerrors.Errorf("update workspace lock: %w", err) diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 3ad9395e3556f..6d7293731f6cf 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -8,19 +8,19 @@ We track the following resources: -| Resource | | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
restart_requirement_days_of_weektrue
restart_requirement_weekstrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
git_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
idtrue
last_used_atfalse
locked_attrue
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| +| Resource | | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
restart_requirement_days_of_weektrue
restart_requirement_weekstrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| +| TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
git_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| +| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
wildcard_hostnametrue
| diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 3cb49b84bf833..f62da873c5c3d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1479,13 +1479,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, "default_ttl_ms": 0, + "delete_ttl_ms": 0, "description": "string", "disable_everyone_group_access": true, "display_name": "string", + "dormant_ttl_ms": 0, "failure_ttl_ms": 0, "icon": "string", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "restart_requirement": { @@ -1504,13 +1504,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. | | `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". | | `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. | +| `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. | | `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. | | `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. | | `display_name` | string | false | | Display name is the displayed name of the template. | +| `dormant_ttl_ms` | integer | false | | Dormant ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | | `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. | | `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. | -| `locked_ttl_ms` | integer | false | | Locked ttl ms allows optionally specifying the max lifetime before Coder permanently deletes locked workspaces created from this template. | | `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured | | `name` | string | true | | Name is the name of the template. | | `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement allows optionally specifying the restart requirement for workspaces created from this template. This is an enterprise feature. | @@ -4239,8 +4239,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -4249,37 +4247,39 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | -| `active_version_id` | string | false | | | -| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | -| `allow_user_autostop` | boolean | false | | | -| `allow_user_cancel_workspace_jobs` | boolean | false | | | -| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | -| `created_at` | string | false | | | -| `created_by_id` | string | false | | | -| `created_by_name` | string | false | | | -| `default_ttl_ms` | integer | false | | | -| `description` | string | false | | | -| `display_name` | string | false | | | -| `failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | -| `icon` | string | false | | | -| `id` | string | false | | | -| `inactivity_ttl_ms` | integer | false | | | -| `locked_ttl_ms` | integer | false | | | -| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `provisioner` | string | false | | | -| `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `active_version_id` | string | false | | | +| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `allow_user_autostop` | boolean | false | | | +| `allow_user_cancel_workspace_jobs` | boolean | false | | | +| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | +| `created_at` | string | false | | | +| `created_by_id` | string | false | | | +| `created_by_name` | string | false | | | +| `default_ttl_ms` | integer | false | | | +| `description` | string | false | | | +| `display_name` | string | false | | | +| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | +| `icon` | string | false | | | +| `id` | string | false | | | +| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `provisioner` | string | false | | | +| `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `time_til_dormant_autodelete_ms` | integer | false | | | +| `time_til_dormant_ms` | integer | false | | | +| `updated_at` | string | false | | | #### Enumerated Values @@ -5051,19 +5051,19 @@ If the schedule is empty, the user will be updated to use the default schedule.| | ---------- | ------ | -------- | ------------ | ----------- | | `schedule` | string | false | | | -## codersdk.UpdateWorkspaceLock +## codersdk.UpdateWorkspaceDormancy ```json { - "lock": true + "dormant": true } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------ | ------- | -------- | ------------ | ----------- | -| `lock` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +| --------- | ------- | -------- | ------------ | ----------- | +| `dormant` | boolean | false | | | ## codersdk.UpdateWorkspaceRequest @@ -5357,6 +5357,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -5491,7 +5492,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -5509,28 +5509,28 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| 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. | -| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | -| `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 at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. | +| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time*til* field on its template. | +| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | +| `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 @@ -6493,6 +6493,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -6623,7 +6624,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, diff --git a/docs/api/templates.md b/docs/api/templates.md index 407ab84eba439..bb67535e786c2 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -50,8 +50,6 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -60,6 +58,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ] @@ -93,11 +93,9 @@ Status Code **200** | `» default_ttl_ms` | integer | false | | | | `» description` | string | false | | | | `» display_name` | string | false | | | -| `» failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | +| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | | `» icon` | string | false | | | | `» id` | string(uuid) | false | | | -| `» inactivity_ttl_ms` | integer | false | | | -| `» locked_ttl_ms` | integer | false | | | | `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured | | `» name` | string | false | | | | `» organization_id` | string(uuid) | false | | | @@ -106,6 +104,8 @@ Status Code **200** | `»» days_of_week` | array | false | | »days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. | | Restarts will only happen on weekdays in this list on weeks which line up with Weeks. | | `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. | +| `» time_til_dormant_autodelete_ms` | integer | false | | | +| `» time_til_dormant_ms` | integer | false | | | | `» updated_at` | string(date-time) | false | | | #### Enumerated Values @@ -138,13 +138,13 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "allow_user_autostop": true, "allow_user_cancel_workspace_jobs": true, "default_ttl_ms": 0, + "delete_ttl_ms": 0, "description": "string", "disable_everyone_group_access": true, "display_name": "string", + "dormant_ttl_ms": 0, "failure_ttl_ms": 0, "icon": "string", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "restart_requirement": { @@ -192,8 +192,6 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -202,6 +200,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ``` @@ -324,8 +324,6 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -334,6 +332,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ``` @@ -629,8 +629,6 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -639,6 +637,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ``` @@ -744,8 +744,6 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "failure_ttl_ms": 0, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "inactivity_ttl_ms": 0, - "locked_ttl_ms": 0, "max_ttl_ms": 0, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -754,6 +752,8 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "days_of_week": ["monday"], "weeks": 0 }, + "time_til_dormant_autodelete_ms": 0, + "time_til_dormant_ms": 0, "updated_at": "2019-08-24T14:15:22Z" } ``` diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 01b85e21b3527..7c4e9319cd2b8 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -48,6 +48,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", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -182,7 +183,6 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -236,6 +236,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", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -370,7 +371,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -427,6 +427,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", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -557,7 +558,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -612,6 +612,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", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -746,7 +747,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -842,88 +842,34 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/autostart \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Extend workspace deadline by ID - -### Code samples - -```shell -# Example request using curl -curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`PUT /workspaces/{workspace}/extend` - -> Body parameter - -```json -{ - "deadline": "2019-08-24T14:15:22Z" -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ----------- | ---- | ---------------------------------------------------------------------------------- | -------- | ------------------------------ | -| `workspace` | path | string(uuid) | true | Workspace ID | -| `body` | body | [codersdk.PutExtendWorkspaceRequest](schemas.md#codersdkputextendworkspacerequest) | true | Extend deadline update request | - -### Example responses - -> 200 Response - -```json -{ - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Update workspace lock by id. +## Update workspace dormancy status by id. ### Code samples ```shell # Example request using curl -curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ +curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`PUT /workspaces/{workspace}/lock` +`PUT /workspaces/{workspace}/dormant` > Body parameter ```json { - "lock": true + "dormant": true } ``` ### Parameters -| Name | In | Type | Required | Description | -| ----------- | ---- | ---------------------------------------------------------------------- | -------- | -------------------------- | -| `workspace` | path | string(uuid) | true | Workspace ID | -| `body` | body | [codersdk.UpdateWorkspaceLock](schemas.md#codersdkupdateworkspacelock) | true | Lock or unlock a workspace | +| Name | In | Type | Required | Description | +| ----------- | ---- | ------------------------------------------------------------------------------ | -------- | ---------------------------------- | +| `workspace` | path | string(uuid) | true | Workspace ID | +| `body` | body | [codersdk.UpdateWorkspaceDormancy](schemas.md#codersdkupdateworkspacedormancy) | true | Make a workspace dormant or active | ### Example responses @@ -934,6 +880,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "dormant_at": "2019-08-24T14:15:22Z", "health": { "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], "healthy": false @@ -1068,7 +1015,6 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, - "locked_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, @@ -1092,6 +1038,60 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Extend workspace deadline by ID + +### Code samples + +```shell +# Example request using curl +curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PUT /workspaces/{workspace}/extend` + +> Body parameter + +```json +{ + "deadline": "2019-08-24T14:15:22Z" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------- | ---- | ---------------------------------------------------------------------------------- | -------- | ------------------------------ | +| `workspace` | path | string(uuid) | true | Workspace ID | +| `body` | body | [codersdk.PutExtendWorkspaceRequest](schemas.md#codersdkputextendworkspacerequest) | true | Extend deadline update request | + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Update workspace TTL by ID ### Code samples diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 2e732a1d53a1d..a1dfef2d053d3 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -82,8 +82,8 @@ var auditableResourcesTypes = map[any]map[string]Action{ "allow_user_autostop": ActionTrack, "allow_user_cancel_workspace_jobs": ActionTrack, "failure_ttl": ActionTrack, - "inactivity_ttl": ActionTrack, - "locked_ttl": ActionTrack, + "time_til_dormant": ActionTrack, + "time_til_dormant_autodelete": ActionTrack, }, &database.TemplateVersion{}: { "id": ActionTrack, @@ -127,7 +127,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "autostart_schedule": ActionTrack, "ttl": ActionTrack, "last_used_at": ActionIgnore, - "locked_at": ActionTrack, + "dormant_at": ActionTrack, "deleting_at": ActionTrack, }, &database.WorkspaceBuild{}: { diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index c5613c44e7880..f37d9ded8d187 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -83,9 +83,9 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S DaysOfWeek: uint8(tpl.RestartRequirementDaysOfWeek), Weeks: tpl.RestartRequirementWeeks, }, - FailureTTL: time.Duration(tpl.FailureTTL), - InactivityTTL: time.Duration(tpl.InactivityTTL), - LockedTTL: time.Duration(tpl.LockedTTL), + FailureTTL: time.Duration(tpl.FailureTTL), + TimeTilDormant: time.Duration(tpl.TimeTilDormant), + TimeTilDormantAutoDelete: time.Duration(tpl.TimeTilDormantAutoDelete), }, nil } @@ -99,8 +99,8 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S int16(opts.RestartRequirement.DaysOfWeek) == tpl.RestartRequirementDaysOfWeek && opts.RestartRequirement.Weeks == tpl.RestartRequirementWeeks && int64(opts.FailureTTL) == tpl.FailureTTL && - int64(opts.InactivityTTL) == tpl.InactivityTTL && - int64(opts.LockedTTL) == tpl.LockedTTL && + int64(opts.TimeTilDormant) == tpl.TimeTilDormant && + int64(opts.TimeTilDormantAutoDelete) == tpl.TimeTilDormantAutoDelete && opts.UserAutostartEnabled == tpl.AllowUserAutostart && opts.UserAutostopEnabled == tpl.AllowUserAutostop { // Avoid updating the UpdatedAt timestamp if nothing will be changed. @@ -127,29 +127,29 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S RestartRequirementDaysOfWeek: int16(opts.RestartRequirement.DaysOfWeek), RestartRequirementWeeks: opts.RestartRequirement.Weeks, FailureTTL: int64(opts.FailureTTL), - InactivityTTL: int64(opts.InactivityTTL), - LockedTTL: int64(opts.LockedTTL), + TimeTilDormant: int64(opts.TimeTilDormant), + TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete), }) if err != nil { return xerrors.Errorf("update template schedule: %w", err) } - var lockedAt time.Time - if opts.UpdateWorkspaceLockedAt { - lockedAt = database.Now() + var dormantAt time.Time + if opts.UpdateWorkspaceDormantAt { + dormantAt = database.Now() } - // If we updated the locked_ttl we need to update all the workspaces deleting_at + // If we updated the time_til_dormant_autodelete we need to update all the workspaces deleting_at // to ensure workspaces are being cleaned up correctly. Similarly if we are // disabling it (by passing 0), then we want to delete nullify the deleting_at // fields of all the template workspaces. - err = tx.UpdateWorkspacesLockedDeletingAtByTemplateID(ctx, database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams{ - TemplateID: tpl.ID, - LockedTtlMs: opts.LockedTTL.Milliseconds(), - LockedAt: lockedAt, + err = tx.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams{ + TemplateID: tpl.ID, + TimeTilDormantAutodeleteMs: opts.TimeTilDormantAutoDelete.Milliseconds(), + DormantAt: dormantAt, }) if err != nil { - return xerrors.Errorf("update deleting_at of all workspaces for new locked_ttl %q: %w", opts.LockedTTL, err) + return xerrors.Errorf("update deleting_at of all workspaces for new time_til_dormant_autodelete %q: %w", opts.TimeTilDormantAutoDelete, err) } if opts.UpdateWorkspaceLastUsedAt { diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index aff7e2364fcec..14f7a384b0c12 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -225,9 +225,9 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) { DaysOfWeek: 0b01111111, Weeks: 0, }, - FailureTTL: 0, - InactivityTTL: 0, - LockedTTL: 0, + FailureTTL: 0, + TimeTilDormant: 0, + TimeTilDormantAutoDelete: 0, }) require.NoError(t, err) @@ -500,9 +500,9 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) { DaysOfWeek: 0b01111111, Weeks: 0, }, - FailureTTL: 0, - InactivityTTL: 0, - LockedTTL: 0, + FailureTTL: 0, + TimeTilDormant: 0, + TimeTilDormantAutoDelete: 0, }) require.NoError(t, err) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index af364d3578b1c..52d29acf76cd6 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -202,41 +202,41 @@ func TestTemplates(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - require.EqualValues(t, 0, template.InactivityTTLMillis) + require.EqualValues(t, 0, template.TimeTilDormantMillis) require.EqualValues(t, 0, template.FailureTTLMillis) - require.EqualValues(t, 0, template.LockedTTLMillis) + require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis) var ( failureTTL int64 = 1 inactivityTTL int64 = 2 - lockedTTL int64 = 3 + dormantTTL int64 = 3 ) updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - Name: template.Name, - DisplayName: template.DisplayName, - Description: template.Description, - Icon: template.Icon, - AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, - InactivityTTLMillis: inactivityTTL, - FailureTTLMillis: failureTTL, - LockedTTLMillis: lockedTTL, + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + TimeTilDormantMillis: inactivityTTL, + FailureTTLMillis: failureTTL, + TimeTilDormantAutoDeleteMillis: dormantTTL, }) require.NoError(t, err) require.Equal(t, failureTTL, updated.FailureTTLMillis) - require.Equal(t, inactivityTTL, updated.InactivityTTLMillis) - require.Equal(t, lockedTTL, updated.LockedTTLMillis) + require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis) + require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis) // Validate fetching the template returns the same values as updating // the template. template, err = client.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, failureTTL, updated.FailureTTLMillis) - require.Equal(t, inactivityTTL, updated.InactivityTTLMillis) - require.Equal(t, lockedTTL, updated.LockedTTLMillis) + require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis) + require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis) }) - t.Run("UpdateLockedTTL", func(t *testing.T) { + t.Run("UpdateTimeTilDormantAutoDelete", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) @@ -254,62 +254,62 @@ func TestTemplates(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - require.Nil(t, unlockedWorkspace.DeletingAt) - require.Nil(t, lockedWorkspace.DeletingAt) + activeWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + dormantWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + require.Nil(t, activeWS.DeletingAt) + require.Nil(t, dormantWS.DeletingAt) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWS.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWS.LatestBuild.ID) - err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) - lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, lockedWorkspace.LockedAt) - // The deleting_at field should be nil since there is no template locked_ttl set. - require.Nil(t, lockedWorkspace.DeletingAt) + dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) + require.NotNil(t, dormantWS.DormantAt) + // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. + require.Nil(t, dormantWS.DeletingAt) - lockedTTL := time.Minute + dormantTTL := time.Minute updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - LockedTTLMillis: lockedTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), }) require.NoError(t, err) - require.Equal(t, lockedTTL.Milliseconds(), updated.LockedTTLMillis) + require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) - unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) - require.Nil(t, unlockedWorkspace.LockedAt) - require.Nil(t, unlockedWorkspace.DeletingAt) + activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) + require.Nil(t, activeWS.DormantAt) + require.Nil(t, activeWS.DeletingAt) - updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, updatedLockedWorkspace.LockedAt) - require.NotNil(t, updatedLockedWorkspace.DeletingAt) - require.Equal(t, updatedLockedWorkspace.LockedAt.Add(lockedTTL), *updatedLockedWorkspace.DeletingAt) - require.Equal(t, updatedLockedWorkspace.LockedAt, lockedWorkspace.LockedAt) + updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID) + require.NotNil(t, updatedDormantWorkspace.DormantAt) + require.NotNil(t, updatedDormantWorkspace.DeletingAt) + require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt) + require.Equal(t, updatedDormantWorkspace.DormantAt, dormantWS.DormantAt) - // Disable the locked_ttl on the template, then we can assert that the workspaces + // Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces // no longer have a deleting_at field. updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - LockedTTLMillis: 0, + TimeTilDormantAutoDeleteMillis: 0, }) require.NoError(t, err) - require.EqualValues(t, 0, updated.LockedTTLMillis) + require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis) - // The unlocked workspace should remain unchanged. - unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) - require.Nil(t, unlockedWorkspace.LockedAt) - require.Nil(t, unlockedWorkspace.DeletingAt) + // The active workspace should remain unchanged. + activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) + require.Nil(t, activeWS.DormantAt) + require.Nil(t, activeWS.DeletingAt) - // Fetch the locked workspace. It should still be locked, but it should no + // Fetch the dormant workspace. It should still be dormant, but it should no // longer be scheduled for deletion. - lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, lockedWorkspace.LockedAt) - require.Nil(t, lockedWorkspace.DeletingAt) + dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) + require.NotNil(t, dormantWS.DormantAt) + require.Nil(t, dormantWS.DeletingAt) }) - t.Run("UpdateLockedAt", func(t *testing.T) { + t.Run("UpdateDormantAt", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) @@ -327,42 +327,42 @@ func TestTemplates(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - require.Nil(t, unlockedWorkspace.DeletingAt) - require.Nil(t, lockedWorkspace.DeletingAt) + activeWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + dormantWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + require.Nil(t, activeWS.DeletingAt) + require.Nil(t, dormantWS.DeletingAt) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWS.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWS.LatestBuild.ID) - err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) - lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, lockedWorkspace.LockedAt) - // The deleting_at field should be nil since there is no template locked_ttl set. - require.Nil(t, lockedWorkspace.DeletingAt) + dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID) + require.NotNil(t, dormantWS.DormantAt) + // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. + require.Nil(t, dormantWS.DeletingAt) - lockedTTL := time.Minute + dormantTTL := time.Minute updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - LockedTTLMillis: lockedTTL.Milliseconds(), - UpdateWorkspaceLockedAt: true, + TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), + UpdateWorkspaceDormantAt: true, }) require.NoError(t, err) - require.Equal(t, lockedTTL.Milliseconds(), updated.LockedTTLMillis) + require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis) - unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) - require.Nil(t, unlockedWorkspace.LockedAt) - require.Nil(t, unlockedWorkspace.DeletingAt) + activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID) + require.Nil(t, activeWS.DormantAt) + require.Nil(t, activeWS.DeletingAt) - updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, updatedLockedWorkspace.LockedAt) - require.NotNil(t, updatedLockedWorkspace.DeletingAt) - // Validate that the workspace locked_at value is updated. - require.True(t, updatedLockedWorkspace.LockedAt.After(*lockedWorkspace.LockedAt)) - require.Equal(t, updatedLockedWorkspace.LockedAt.Add(lockedTTL), *updatedLockedWorkspace.DeletingAt) + updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID) + require.NotNil(t, updatedDormantWorkspace.DormantAt) + require.NotNil(t, updatedDormantWorkspace.DeletingAt) + // Validate that the workspace dormant_at value is updated. + require.True(t, updatedDormantWorkspace.DormantAt.After(*dormantWS.DormantAt)) + require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt) }) t.Run("UpdateLastUsedAt", func(t *testing.T) { @@ -383,43 +383,43 @@ func TestTemplates(t *testing.T) { coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - require.Nil(t, unlockedWorkspace.DeletingAt) - require.Nil(t, lockedWorkspace.DeletingAt) + activeWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + dormantWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + require.Nil(t, activeWorkspace.DeletingAt) + require.Nil(t, dormantWorkspace.DeletingAt) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID) - _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID) - err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) - lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, lockedWorkspace.LockedAt) - // The deleting_at field should be nil since there is no template locked_ttl set. - require.Nil(t, lockedWorkspace.DeletingAt) + dormantWorkspace = coderdtest.MustWorkspace(t, client, dormantWorkspace.ID) + require.NotNil(t, dormantWorkspace.DormantAt) + // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set. + require.Nil(t, dormantWorkspace.DeletingAt) inactivityTTL := time.Minute updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - InactivityTTLMillis: inactivityTTL.Milliseconds(), + TimeTilDormantMillis: inactivityTTL.Milliseconds(), UpdateWorkspaceLastUsedAt: true, }) require.NoError(t, err) - require.Equal(t, inactivityTTL.Milliseconds(), updated.InactivityTTLMillis) + require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis) - updatedUnlockedWS := coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) - require.Nil(t, updatedUnlockedWS.LockedAt) - require.Nil(t, updatedUnlockedWS.DeletingAt) - require.True(t, updatedUnlockedWS.LastUsedAt.After(unlockedWorkspace.LastUsedAt)) + updatedActiveWS := coderdtest.MustWorkspace(t, client, activeWorkspace.ID) + require.Nil(t, updatedActiveWS.DormantAt) + require.Nil(t, updatedActiveWS.DeletingAt) + require.True(t, updatedActiveWS.LastUsedAt.After(activeWorkspace.LastUsedAt)) - updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, updatedLockedWorkspace.LockedAt) - require.Nil(t, updatedLockedWorkspace.DeletingAt) - // Validate that the workspace locked_at value is updated. - require.Equal(t, updatedLockedWorkspace.LockedAt, lockedWorkspace.LockedAt) - require.True(t, updatedLockedWorkspace.LastUsedAt.After(lockedWorkspace.LastUsedAt)) + updatedDormantWS := coderdtest.MustWorkspace(t, client, dormantWorkspace.ID) + require.NotNil(t, updatedDormantWS.DormantAt) + require.Nil(t, updatedDormantWS.DeletingAt) + // Validate that the workspace dormant_at value is updated. + require.Equal(t, updatedDormantWS.DormantAt, dormantWorkspace.DormantAt) + require.True(t, updatedDormantWS.LastUsedAt.After(dormantWorkspace.LastUsedAt)) }) } diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index c5a8aec6b8535..373b79c78d59f 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -217,9 +217,9 @@ func TestWorkspaceAutobuild(t *testing.T) { }) // Create a template without setting a failure_ttl. template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - require.Zero(t, template.InactivityTTLMillis) + require.Zero(t, template.TimeTilDormantMillis) require.Zero(t, template.FailureTTLMillis) - require.Zero(t, template.LockedTTLMillis) + require.Zero(t, template.TimeTilDormantAutoDeleteMillis) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) @@ -259,7 +259,7 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -275,12 +275,12 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Len(t, stats.Transitions, 1) require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop) - // The workspace should be locked. + // The workspace should be dormant. ws = coderdtest.MustWorkspace(t, client, ws.ID) - require.NotNil(t, ws.LockedAt) + require.NotNil(t, ws.DormantAt) lastUsedAt := ws.LastUsedAt - err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{Lock: false}) + err := client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{Dormant: false}) require.NoError(t, err) // Assert that we updated our last_used_at so that we don't immediately @@ -315,7 +315,7 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) @@ -331,13 +331,13 @@ func TestWorkspaceAutobuild(t *testing.T) { // This is kind of a dumb test but it exists to offer some marginal // confidence that a bug in the auto-deletion logic doesn't delete running // workspaces. - t.Run("UnlockedWorkspacesNotDeleted", func(t *testing.T) { + t.Run("ActiveWorkspacesNotDeleted", func(t *testing.T) { t.Parallel() var ( - ticker = make(chan time.Time) - statCh = make(chan autobuild.Stats) - lockedTTL = time.Minute + ticker = make(chan time.Time) + statCh = make(chan autobuild.Stats) + autoDeleteTTL = time.Minute ) client, user := coderdenttest.New(t, &coderdenttest.Options{ @@ -357,16 +357,16 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](autoDeleteTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) - require.Nil(t, ws.LockedAt) + require.Nil(t, ws.DormantAt) require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) - ticker <- ws.LastUsedAt.Add(lockedTTL * 2) + ticker <- ws.LastUsedAt.Add(autoDeleteTTL * 2) stats := <-statCh - // Expect no transitions since workspace is unlocked. + // Expect no transitions since workspace is active. require.Len(t, stats.Transitions, 0) }) @@ -399,7 +399,7 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -417,13 +417,13 @@ func TestWorkspaceAutobuild(t *testing.T) { // Expect no transitions since workspace is stopped. require.Len(t, stats.Transitions, 0) ws = coderdtest.MustWorkspace(t, client, ws.ID) - // The workspace should still be locked even though we didn't + // The workspace should still be dormant even though we didn't // transition the workspace. - require.NotNil(t, ws.LockedAt) + require.NotNil(t, ws.DormantAt) }) // Test the flow of a workspace transitioning from - // inactive -> locked -> deleted. + // inactive -> dormant -> deleted. t.Run("WorkspaceInactiveDeleteTransition", func(t *testing.T) { t.Parallel() @@ -451,8 +451,8 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.InactivityTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds()) - ctr.LockedTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds()) + ctr.TimeTilDormantMillis = ptr.Ref[int64](transitionTTL.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](transitionTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -469,14 +469,14 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop) ws = coderdtest.MustWorkspace(t, client, ws.ID) - // The workspace should be locked. - require.NotNil(t, ws.LockedAt) + // The workspace should be dormant. + require.NotNil(t, ws.DormantAt) // Wait for the autobuilder to stop the workspace. _ = coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) - // Simulate the workspace being locked beyond the threshold. - ticker <- ws.LockedAt.Add(2 * transitionTTL) + // Simulate the workspace being dormant beyond the threshold. + ticker <- ws.DormantAt.Add(2 * transitionTTL) stats = <-statCh require.Len(t, stats.Transitions, 1) // The workspace should be scheduled for deletion. @@ -494,13 +494,13 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Equal(t, http.StatusGone, cerr.StatusCode()) }) - t.Run("LockedTTTooEarly", func(t *testing.T) { + t.Run("DormantTTLTooEarly", func(t *testing.T) { t.Parallel() var ( - ticker = make(chan time.Time) - statCh = make(chan autobuild.Stats) - lockedTTL = time.Minute + ticker = make(chan time.Time) + statCh = make(chan autobuild.Stats) + dormantTTL = time.Minute ) client, user := coderdenttest.New(t, &coderdenttest.Options{ @@ -520,7 +520,7 @@ func TestWorkspaceAutobuild(t *testing.T) { ProvisionApply: echo.ProvisionComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](dormantTTL.Milliseconds()) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) @@ -528,34 +528,34 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) ctx := testutil.Context(t, testutil.WaitMedium) - err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) ws = coderdtest.MustWorkspace(t, client, ws.ID) - require.NotNil(t, ws.LockedAt) + require.NotNil(t, ws.DormantAt) // Ensure we haven't breached our threshold. - ticker <- ws.LockedAt.Add(-lockedTTL * 2) + ticker <- ws.DormantAt.Add(-dormantTTL * 2) stats := <-statCh // Expect no transitions since not enough time has elapsed. require.Len(t, stats.Transitions, 0) _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - LockedTTLMillis: lockedTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), }) require.NoError(t, err) // Simlute the workspace breaching the threshold. - ticker <- ws.LockedAt.Add(lockedTTL * 2) + ticker <- ws.DormantAt.Add(dormantTTL * 2) stats = <-statCh require.Len(t, stats.Transitions, 1) require.Equal(t, database.WorkspaceTransitionDelete, stats.Transitions[ws.ID]) }) - // Assert that a locked workspace does not autostart. - t.Run("LockedNoAutostart", func(t *testing.T) { + // Assert that a dormant workspace does not autostart. + t.Run("DormantNoAutostart", func(t *testing.T) { t.Parallel() var ( @@ -594,7 +594,7 @@ func TestWorkspaceAutobuild(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) - // Assert that autostart works when the workspace isn't locked.. + // Assert that autostart works when the workspace isn't dormant.. tickCh <- sched.Next(ws.LatestBuild.CreatedAt) stats := <-statsCh require.NoError(t, stats.Error) @@ -606,9 +606,9 @@ func TestWorkspaceAutobuild(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) // Now that we've validated that the workspace is eligible for autostart - // lets cause it to become locked. + // lets cause it to become dormant. _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - InactivityTTLMillis: inactiveTTL.Milliseconds(), + TimeTilDormantMillis: inactiveTTL.Milliseconds(), }) require.NoError(t, err) @@ -620,12 +620,12 @@ func TestWorkspaceAutobuild(t *testing.T) { require.Contains(t, stats.Transitions, ws.ID) require.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[ws.ID]) - // The workspace should be locked now. + // The workspace should be dormant now. ws = coderdtest.MustWorkspace(t, client, ws.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID) - require.NotNil(t, ws.LockedAt) + require.NotNil(t, ws.DormantAt) - // Assert that autostart is no longer triggered since workspace is locked. + // Assert that autostart is no longer triggered since workspace is dormant. tickCh <- sched.Next(ws.LatestBuild.CreatedAt) stats = <-statsCh require.Len(t, stats.Transitions, 0) @@ -638,7 +638,7 @@ func TestWorkspacesFiltering(t *testing.T) { t.Run("DeletingBy", func(t *testing.T) { t.Parallel() - lockedTTL := 24 * time.Hour + dormantTTL := 24 * time.Hour client, user := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ @@ -660,10 +660,10 @@ func TestWorkspacesFiltering(t *testing.T) { defer cancel() template, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ - LockedTTLMillis: lockedTTL.Milliseconds(), + TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(), }) require.NoError(t, err) - require.Equal(t, lockedTTL.Milliseconds(), template.LockedTTLMillis) + require.Equal(t, dormantTTL.Milliseconds(), template.TimeTilDormantAutoDeleteMillis) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) @@ -671,8 +671,8 @@ func TestWorkspacesFiltering(t *testing.T) { // stop build so workspace is inactive stopBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop) coderdtest.AwaitWorkspaceBuildJob(t, client, stopBuild.ID) - err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) @@ -680,7 +680,7 @@ func TestWorkspacesFiltering(t *testing.T) { res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ // adding a second to time.Now() to give some buffer in case test runs quickly - FilterQuery: fmt.Sprintf("deleting_by:%s", time.Now().Add(time.Second).Add(lockedTTL).Format("2006-01-02")), + FilterQuery: fmt.Sprintf("deleting_by:%s", time.Now().Add(time.Second).Add(dormantTTL).Format("2006-01-02")), }) require.NoError(t, err) require.Len(t, res.Workspaces, 1) @@ -746,7 +746,7 @@ func TestWorkspacesWithoutTemplatePerms(t *testing.T) { func TestWorkspaceLock(t *testing.T) { t.Parallel() - t.Run("TemplateLockedTTL", func(t *testing.T) { + t.Run("TemplateTimeTilDormantAutoDelete", func(t *testing.T) { t.Parallel() var ( client, user = coderdenttest.New(t, &coderdenttest.Options{ @@ -761,13 +761,13 @@ func TestWorkspaceLock(t *testing.T) { }, }) - version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - lockedTTL = time.Minute + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + dormantTTL = time.Minute ) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds()) + ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](dormantTTL.Milliseconds()) }) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) @@ -777,30 +777,30 @@ func TestWorkspaceLock(t *testing.T) { defer cancel() lastUsedAt := workspace.LastUsedAt - err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: true, + err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, }) require.NoError(t, err) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") require.NotNil(t, workspace.DeletingAt) - require.NotNil(t, workspace.LockedAt) - require.Equal(t, workspace.LockedAt.Add(lockedTTL), *workspace.DeletingAt) - require.WithinRange(t, *workspace.LockedAt, time.Now().Add(-time.Second*10), time.Now()) + require.NotNil(t, workspace.DormantAt) + require.Equal(t, workspace.DormantAt.Add(dormantTTL), *workspace.DeletingAt) + require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now()) // Locking a workspace shouldn't update the last_used_at. require.Equal(t, lastUsedAt, workspace.LastUsedAt) workspace = coderdtest.MustWorkspace(t, client, workspace.ID) lastUsedAt = workspace.LastUsedAt - err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{ - Lock: false, + err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: false, }) require.NoError(t, err) workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") - require.Nil(t, workspace.LockedAt) + require.Nil(t, workspace.DormantAt) // Unlocking a workspace should cause the deleting_at to be unset. require.Nil(t, workspace.DeletingAt) // The last_used_at should get updated when we unlock the workspace. diff --git a/site/src/api/api.ts b/site/src/api/api.ts index eefbcfba275e7..3567e4f977332 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -554,16 +554,16 @@ export const cancelWorkspaceBuild = async ( return response.data } -export const updateWorkspaceLock = async ( +export const updateWorkspaceDormancy = async ( workspaceId: string, - lock: boolean, + dormant: boolean, ): Promise => { - const data: TypesGen.UpdateWorkspaceLock = { - lock: lock, + const data: TypesGen.UpdateWorkspaceDormancy = { + dormant: dormant, } const response = await axios.put( - `/api/v2/workspaces/${workspaceId}/lock`, + `/api/v2/workspaces/${workspaceId}/dormant`, data, ) return response.data diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 39f5401d5ff29..7315caf7ded97 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -200,8 +200,8 @@ export interface CreateTemplateRequest { readonly allow_user_autostart?: boolean readonly allow_user_autostop?: boolean readonly failure_ttl_ms?: number - readonly inactivity_ttl_ms?: number - readonly locked_ttl_ms?: number + readonly dormant_ttl_ms?: number + readonly delete_ttl_ms?: number readonly disable_everyone_group_access: boolean } @@ -917,8 +917,8 @@ export interface Template { readonly allow_user_autostop: boolean readonly allow_user_cancel_workspace_jobs: boolean readonly failure_ttl_ms: number - readonly inactivity_ttl_ms: number - readonly locked_ttl_ms: number + readonly time_til_dormant_ms: number + readonly time_til_dormant_autodelete_ms: number } // From codersdk/templates.go @@ -1152,10 +1152,10 @@ export interface UpdateTemplateMeta { readonly allow_user_autostop?: boolean readonly allow_user_cancel_workspace_jobs?: boolean readonly failure_ttl_ms?: number - readonly inactivity_ttl_ms?: number - readonly locked_ttl_ms?: number + readonly time_til_dormant_ms?: number + readonly time_til_dormant_autodelete_ms?: number readonly update_workspace_last_used_at: boolean - readonly update_workspace_locked_at: boolean + readonly update_workspace_dormant_at: boolean } // From codersdk/users.go @@ -1180,8 +1180,8 @@ export interface UpdateWorkspaceAutostartRequest { } // From codersdk/workspaces.go -export interface UpdateWorkspaceLock { - readonly lock: boolean +export interface UpdateWorkspaceDormancy { + readonly dormant: boolean } // From codersdk/workspaceproxy.go @@ -1310,7 +1310,7 @@ export interface Workspace { readonly ttl_ms?: number readonly last_used_at: string readonly deleting_at?: string - readonly locked_at?: string + readonly dormant_at?: string readonly health: WorkspaceHealth } diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index 5a1cfcc80060e..b16437ea62375 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -160,7 +160,7 @@ export interface ScheduleDialogProps extends ConfirmDialogProps { readonly inactiveWorkspacesToGoDormantInWeek: number readonly dormantWorkspacesToBeDeleted: number readonly dormantWorkspacesToBeDeletedInWeek: number - readonly updateLockedWorkspaces: (confirm: boolean) => void + readonly updateDormantWorkspaces: (confirm: boolean) => void readonly updateInactiveWorkspaces: (confirm: boolean) => void readonly dormantValueChanged: boolean readonly deletionValueChanged: boolean @@ -180,7 +180,7 @@ export const ScheduleDialog: FC> = ({ inactiveWorkspacesToGoDormantInWeek, dormantWorkspacesToBeDeleted, dormantWorkspacesToBeDeletedInWeek, - updateLockedWorkspaces, + updateDormantWorkspaces, updateInactiveWorkspaces, dormantValueChanged, deletionValueChanged, @@ -250,7 +250,7 @@ export const ScheduleDialog: FC> = ({ { - updateLockedWorkspaces(e.target.checked) + updateDormantWorkspaces(e.target.checked) }} /> } diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index 1d2eeb9d4811e..5e8b1345ee898 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -34,7 +34,7 @@ export const actionsByWorkspaceStatus = ( workspace: Workspace, status: WorkspaceStatus, ): WorkspaceAbilities => { - if (workspace.locked_at) { + if (workspace.dormant_at) { return { actions: [ButtonTypesEnum.activate], canCancel: false, diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx deleted file mode 100644 index dc09566fc69e9..0000000000000 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Workspace } from "api/typesGenerated" -import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" -import { Pill } from "components/Pill/Pill" -import LockIcon from "@mui/icons-material/Lock" - -export const LockedBadge = ({ - workspace, -}: { - workspace: Workspace -}): JSX.Element | null => { - const experimentEnabled = useIsWorkspaceActionsEnabled() - if (!workspace.locked_at || !experimentEnabled) { - return null - } - - return } text="Locked" type="error" /> -} diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index b65f3a5cdc28b..f49ec057d0878 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -27,8 +27,8 @@ export const DormantWorkspaceBanner = ({ return null } - const hasLockedWorkspaces = workspaces.find( - (workspace) => workspace.locked_at, + const hasDormantWorkspaces = workspaces.find( + (workspace) => workspace.dormant_at, ) const hasDeletionScheduledWorkspaces = workspaces.find( @@ -38,7 +38,7 @@ export const DormantWorkspaceBanner = ({ if ( // Only show this if the experiment is included. !experimentEnabled || - !hasLockedWorkspaces || + !hasDormantWorkspaces || // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion !shouldRedisplayBanner ) { @@ -59,16 +59,16 @@ export const DormantWorkspaceBanner = ({ if ( hasDeletionScheduledWorkspaces && hasDeletionScheduledWorkspaces.deleting_at && - hasDeletionScheduledWorkspaces.locked_at + hasDeletionScheduledWorkspaces.dormant_at ) { return `This workspace has been dormant for ${formatDistanceToNow( - Date.parse(hasDeletionScheduledWorkspaces.locked_at), + Date.parse(hasDeletionScheduledWorkspaces.dormant_at), )} and is scheduled to be deleted on ${formatDate( hasDeletionScheduledWorkspaces.deleting_at, )} . To keep it you must activate the workspace.` - } else if (hasLockedWorkspaces && hasLockedWorkspaces.locked_at) { + } else if (hasDormantWorkspaces && hasDormantWorkspaces.dormant_at) { return `This workspace has been dormant for ${formatDistanceToNow( - Date.parse(hasLockedWorkspaces.locked_at), + Date.parse(hasDormantWorkspaces.dormant_at), )} and cannot be interacted with. Dormant workspaces are eligible for @@ -88,7 +88,7 @@ export const DormantWorkspaceBanner = ({ There are{" "} workspaces {" "} diff --git a/site/src/components/WorkspaceDeletion/index.ts b/site/src/components/WorkspaceDeletion/index.ts index d58af8e83ac57..a8c14bd12da69 100644 --- a/site/src/components/WorkspaceDeletion/index.ts +++ b/site/src/components/WorkspaceDeletion/index.ts @@ -1,4 +1,3 @@ export * from "./ImpendingDeletionStat" -export * from "./ImpendingDeletionBadge" export * from "./ImpendingDeletionText" export * from "./ImpendingDeletionBanner" diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 4ae79d145baaa..dd4535bc29b89 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -73,7 +73,7 @@ export const TemplateSettingsForm: FC = ({ allow_user_cancel_workspace_jobs: template.allow_user_cancel_workspace_jobs, update_workspace_last_used_at: false, - update_workspace_locked_at: false, + update_workspace_dormant_at: false, }, validationSchema, onSubmit, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 9f9f0d231f9c4..9c43034905e68 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -31,10 +31,10 @@ const validFormValues: FormValues = { weeks: 1, }, failure_ttl_ms: 0, - inactivity_ttl_ms: 0, - locked_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, update_workspace_last_used_at: false, - update_workspace_locked_at: false, + update_workspace_dormant_at: false, } const renderTemplateSettingsPage = async () => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx deleted file mode 100644 index 6128700299e28..0000000000000 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react" -import { InactivityDialog } from "./InactivityDialog" - -const meta: Meta = { - title: "InactivityDialog", - component: InactivityDialog, -} - -export default meta -type Story = StoryObj - -export const OpenDialog: Story = { - args: { - submitValues: () => null, - isInactivityDialogOpen: true, - setIsInactivityDialogOpen: () => null, - workspacesToBeLockedToday: 2, - }, -} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx deleted file mode 100644 index a9407f6d5eab5..0000000000000 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" - -export const InactivityDialog = ({ - submitValues, - isInactivityDialogOpen, - setIsInactivityDialogOpen, - workspacesToBeLockedToday, -}: { - submitValues: () => void - isInactivityDialogOpen: boolean - setIsInactivityDialogOpen: (arg0: boolean) => void - workspacesToBeLockedToday: number -}) => { - return ( - { - submitValues() - setIsInactivityDialogOpen(false) - }} - onClose={() => setIsInactivityDialogOpen(false)} - title="Lock inactive workspaces" - confirmText="Lock Workspaces" - description={`There are ${ - workspacesToBeLockedToday ? workspacesToBeLockedToday : "" - } workspaces that already match this filter and will be locked upon form submission. Are you sure you want to proceed?`} - /> - ) -} - -export const DeleteLockedDialog = ({ - submitValues, - isLockedDialogOpen, - setIsLockedDialogOpen, - workspacesToBeDeletedToday, -}: { - submitValues: () => void - isLockedDialogOpen: boolean - setIsLockedDialogOpen: (arg0: boolean) => void - workspacesToBeDeletedToday: number -}) => { - return ( - { - submitValues() - setIsLockedDialogOpen(false) - }} - onClose={() => setIsLockedDialogOpen(false)} - title="Delete Locked Workspaces" - confirmText="Delete Workspaces" - description={`There are ${ - workspacesToBeDeletedToday ? workspacesToBeDeletedToday : "" - } workspaces that already match this filter and will be deleted upon form submission. Are you sure you want to proceed?`} - /> - ) -} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 180d81df978b8..e13535d68270b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -17,7 +17,7 @@ import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import Switch from "@mui/material/Switch" import { - useWorkspacesToBeLocked, + useWorkspacesToGoDormant, useWorkspacesToBeDeleted, } from "./useWorkspacesToBeDeleted" import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers" @@ -29,7 +29,7 @@ const MS_HOUR_CONVERSION = 3600000 const MS_DAY_CONVERSION = 86400000 const FAILURE_CLEANUP_DEFAULT = 7 const INACTIVITY_CLEANUP_DEFAULT = 180 -const LOCKED_CLEANUP_DEFAULT = 30 +const DORMANT_AUTODELETION_DEFAULT = 30 export interface TemplateScheduleForm { template: Template @@ -67,11 +67,11 @@ export const TemplateScheduleForm: FC = ({ failure_ttl_ms: allowAdvancedScheduling ? template.failure_ttl_ms / MS_DAY_CONVERSION : 0, - inactivity_ttl_ms: allowAdvancedScheduling - ? template.inactivity_ttl_ms / MS_DAY_CONVERSION + time_til_dormant_ms: allowAdvancedScheduling + ? template.time_til_dormant_ms / MS_DAY_CONVERSION : 0, - locked_ttl_ms: allowAdvancedScheduling - ? template.locked_ttl_ms / MS_DAY_CONVERSION + time_til_dormant_autodelete_ms: allowAdvancedScheduling + ? template.time_til_dormant_autodelete_ms / MS_DAY_CONVERSION : 0, restart_requirement: { @@ -84,18 +84,21 @@ export const TemplateScheduleForm: FC = ({ failure_cleanup_enabled: allowAdvancedScheduling && Boolean(template.failure_ttl_ms), inactivity_cleanup_enabled: - allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms), - locked_cleanup_enabled: - allowAdvancedScheduling && Boolean(template.locked_ttl_ms), + allowAdvancedScheduling && Boolean(template.time_til_dormant_ms), + dormant_autodeletion_cleanup_enabled: + allowAdvancedScheduling && + Boolean(template.time_til_dormant_autodelete_ms), update_workspace_last_used_at: false, - update_workspace_locked_at: false, + update_workspace_dormant_at: false, }, validationSchema, onSubmit: () => { const dormancyChanged = - form.initialValues.inactivity_ttl_ms !== form.values.inactivity_ttl_ms + form.initialValues.time_til_dormant_ms !== + form.values.time_til_dormant_ms const deletionChanged = - form.initialValues.locked_ttl_ms !== form.values.locked_ttl_ms + form.initialValues.time_til_dormant_autodelete_ms !== + form.values.time_til_dormant_autodelete_ms const dormancyScheduleChanged = form.values.inactivity_cleanup_enabled && @@ -128,13 +131,13 @@ export const TemplateScheduleForm: FC = ({ const weekFromNow = new Date(now) weekFromNow.setDate(now.getDate() + 7) - const workspacesToDormancyNow = useWorkspacesToBeLocked( + const workspacesToDormancyNow = useWorkspacesToGoDormant( template, form.values, now, ) - const workspacesToDormancyInWeek = useWorkspacesToBeLocked( + const workspacesToDormancyInWeek = useWorkspacesToGoDormant( template, form.values, weekFromNow, @@ -175,17 +178,17 @@ export const TemplateScheduleForm: FC = ({ failure_ttl_ms: form.values.failure_ttl_ms ? form.values.failure_ttl_ms * MS_DAY_CONVERSION : undefined, - inactivity_ttl_ms: form.values.inactivity_ttl_ms - ? form.values.inactivity_ttl_ms * MS_DAY_CONVERSION + time_til_dormant_ms: form.values.time_til_dormant_ms + ? form.values.time_til_dormant_ms * MS_DAY_CONVERSION : undefined, - locked_ttl_ms: form.values.locked_ttl_ms - ? form.values.locked_ttl_ms * MS_DAY_CONVERSION + time_til_dormant_autodelete_ms: form.values.time_til_dormant_autodelete_ms + ? form.values.time_til_dormant_autodelete_ms * MS_DAY_CONVERSION : undefined, allow_user_autostart: form.values.allow_user_autostart, allow_user_autostop: form.values.allow_user_autostop, update_workspace_last_used_at: form.values.update_workspace_last_used_at, - update_workspace_locked_at: form.values.update_workspace_locked_at, + update_workspace_dormant_at: form.values.update_workspace_dormant_at, }) } @@ -211,37 +214,37 @@ export const TemplateScheduleForm: FC = ({ const handleToggleInactivityCleanup = async (e: ChangeEvent) => { form.handleChange(e) if (!form.values.inactivity_cleanup_enabled) { - // fill inactivity_ttl_ms with defaults + // fill time_til_dormant_ms with defaults await form.setValues({ ...form.values, inactivity_cleanup_enabled: true, - inactivity_ttl_ms: INACTIVITY_CLEANUP_DEFAULT, + time_til_dormant_ms: INACTIVITY_CLEANUP_DEFAULT, }) } else { - // clear inactivity_ttl_ms + // clear time_til_dormant_ms await form.setValues({ ...form.values, inactivity_cleanup_enabled: false, - inactivity_ttl_ms: 0, + time_til_dormant_ms: 0, }) } } - const handleToggleLockedCleanup = async (e: ChangeEvent) => { + const handleToggleDormantAutoDeletion = async (e: ChangeEvent) => { form.handleChange(e) - if (!form.values.locked_cleanup_enabled) { + if (!form.values.dormant_autodeletion_cleanup_enabled) { // fill failure_ttl_ms with defaults await form.setValues({ ...form.values, - locked_cleanup_enabled: true, - locked_ttl_ms: LOCKED_CLEANUP_DEFAULT, + dormant_autodeletion_cleanup_enabled: true, + time_til_dormant_autodelete_ms: DORMANT_AUTODELETION_DEFAULT, }) } else { // clear failure_ttl_ms await form.setValues({ ...form.values, - locked_cleanup_enabled: false, - locked_ttl_ms: 0, + dormant_autodeletion_cleanup_enabled: false, + time_til_dormant_autodelete_ms: 0, }) } } @@ -398,10 +401,10 @@ export const TemplateScheduleForm: FC = ({ /> , )} disabled={ @@ -423,21 +426,24 @@ export const TemplateScheduleForm: FC = ({ control={ } label="Enable Dormancy Auto-Deletion" /> , )} - disabled={isSubmitting || !form.values.locked_cleanup_enabled} + disabled={ + isSubmitting || + !form.values.dormant_autodeletion_cleanup_enabled + } fullWidth inputProps={{ min: 0, step: "any" }} label="Time until deletion (days)" @@ -455,7 +461,7 @@ export const TemplateScheduleForm: FC = ({ // These fields are request-scoped so they should be reset // after every submission. form - .setFieldValue("update_workspace_locked_at", false) + .setFieldValue("update_workspace_dormant_at", false) .catch((error) => { throw error }) @@ -478,18 +484,19 @@ export const TemplateScheduleForm: FC = ({ setIsScheduleDialogOpen(false) }} title="Workspace Scheduling" - updateLockedWorkspaces={(update: boolean) => - form.setFieldValue("update_workspace_locked_at", update) + updateDormantWorkspaces={(update: boolean) => + form.setFieldValue("update_workspace_dormant_at", update) } updateInactiveWorkspaces={(update: boolean) => form.setFieldValue("update_workspace_last_used_at", update) } dormantValueChanged={ - form.initialValues.inactivity_ttl_ms !== - form.values.inactivity_ttl_ms + form.initialValues.time_til_dormant_ms !== + form.values.time_til_dormant_ms } deletionValueChanged={ - form.initialValues.locked_ttl_ms !== form.values.locked_ttl_ms + form.initialValues.time_til_dormant_autodelete_ms !== + form.values.time_til_dormant_autodelete_ms } /> )} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx index d36fcd85b021c..8480b933c44da 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx @@ -5,7 +5,7 @@ import i18next from "i18next" export interface TemplateScheduleFormValues extends UpdateTemplateMeta { failure_cleanup_enabled: boolean inactivity_cleanup_enabled: boolean - locked_cleanup_enabled: boolean + dormant_autodeletion_cleanup_enabled: boolean } const MAX_TTL_DAYS = 30 @@ -50,7 +50,7 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => } }, ), - inactivity_ttl_ms: Yup.number() + time_til_dormant_ms: Yup.number() .min(0, "Dormancy threshold days must not be less than 0.") .test( "positive-if-enabled", @@ -64,14 +64,14 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => } }, ), - locked_ttl_ms: Yup.number() + time_til_dormant_autodelete_ms: Yup.number() .min(0, "Dormancy auto-deletion days must not be less than 0.") .test( "positive-if-enabled", "Dormancy auto-deletion days must be greater than zero when enabled.", function (value) { const parent = this.parent as TemplateScheduleFormValues - if (parent.locked_cleanup_enabled) { + if (parent.dormant_autodeletion_cleanup_enabled) { return Boolean(value) } else { return true diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts index 346d371f02951..b04cf02e7d618 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts @@ -3,7 +3,7 @@ import { Workspace, Template } from "api/typesGenerated" import { TemplateScheduleFormValues } from "./formHelpers" import { useWorkspacesData } from "pages/WorkspacesPage/data" -export const useWorkspacesToBeLocked = ( +export const useWorkspacesToGoDormant = ( template: Template, formValues: TemplateScheduleFormValues, fromDate: Date, @@ -15,17 +15,17 @@ export const useWorkspacesToBeLocked = ( }) return data?.workspaces?.filter((workspace: Workspace) => { - if (!formValues.inactivity_ttl_ms) { + if (!formValues.time_til_dormant_ms) { return } - if (workspace.locked_at) { + if (workspace.dormant_at) { return } const proposedLocking = new Date( new Date(workspace.last_used_at).getTime() + - formValues.inactivity_ttl_ms * DayInMS, + formValues.time_til_dormant_ms * DayInMS, ) if (compareAsc(proposedLocking, fromDate) < 1) { @@ -44,16 +44,16 @@ export const useWorkspacesToBeDeleted = ( const { data } = useWorkspacesData({ page: 0, limit: 0, - query: "template:" + template.name + " locked_at:1970-01-01", + query: "template:" + template.name + " dormant_at:1970-01-01", }) return data?.workspaces?.filter((workspace: Workspace) => { - if (!workspace.locked_at || !formValues.locked_ttl_ms) { + if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) { return false } const proposedLocking = new Date( - new Date(workspace.locked_at).getTime() + - formValues.locked_ttl_ms * DayInMS, + new Date(workspace.dormant_at).getTime() + + formValues.time_til_dormant_autodelete_ms * DayInMS, ) if (compareAsc(proposedLocking, fromDate) < 1) { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index dd03610717829..476d5085d0f4f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -21,10 +21,10 @@ const validFormValues = { default_ttl_ms: 1, max_ttl_ms: 2, failure_ttl_ms: 7, - inactivity_ttl_ms: 180, - locked_ttl_ms: 30, + time_til_dormant_ms: 180, + time_til_dormant_autodelete_ms: 30, update_workspace_last_used_at: false, - update_workspace_locked_at: false, + update_workspace_dormant_at: false, } const renderTemplateSchedulePage = async () => { @@ -39,14 +39,14 @@ const fillAndSubmitForm = async ({ default_ttl_ms, max_ttl_ms, failure_ttl_ms, - inactivity_ttl_ms, - locked_ttl_ms, + time_til_dormant_ms, + time_til_dormant_autodelete_ms, }: { default_ttl_ms: number max_ttl_ms: number failure_ttl_ms: number - inactivity_ttl_ms: number - locked_ttl_ms: number + time_til_dormant_ms: number + time_til_dormant_autodelete_ms: number }) => { const user = userEvent.setup() const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) @@ -67,19 +67,22 @@ const fillAndSubmitForm = async ({ const inactivityTtlField = screen.getByRole("checkbox", { name: /Dormancy Threshold/i, }) - await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) + await user.type(inactivityTtlField, time_til_dormant_ms.toString()) - const lockedTtlField = screen.getByRole("checkbox", { + const dormancyAutoDeletionField = screen.getByRole("checkbox", { name: /Dormancy Auto-Deletion/i, }) - await user.type(lockedTtlField, locked_ttl_ms.toString()) + await user.type( + dormancyAutoDeletionField, + time_til_dormant_autodelete_ms.toString(), + ) const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, ) await user.click(submitButton) - // User needs to confirm inactivity and locked ttl + // User needs to confirm dormancy and autodeletion fields. const confirmButton = await screen.findByTestId("confirm-button") await user.click(confirmButton) } @@ -140,8 +143,9 @@ describe("TemplateSchedulePage", () => { "test-template", expect.objectContaining({ failure_ttl_ms: validFormValues.failure_ttl_ms * 86400000, - inactivity_ttl_ms: validFormValues.inactivity_ttl_ms * 86400000, - locked_ttl_ms: validFormValues.locked_ttl_ms * 86400000, + time_til_dormant_ms: validFormValues.time_til_dormant_ms * 86400000, + time_til_dormant_autodelete_ms: + validFormValues.time_til_dormant_autodelete_ms * 86400000, }), ), ) @@ -217,7 +221,7 @@ describe("TemplateSchedulePage", () => { it("allows an inactivity ttl of 7 days", () => { const values: UpdateTemplateMeta = { ...validFormValues, - inactivity_ttl_ms: 86400000 * 7, + time_til_dormant_ms: 86400000 * 7, } const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() @@ -226,7 +230,7 @@ describe("TemplateSchedulePage", () => { it("allows an inactivity ttl of 0", () => { const values: UpdateTemplateMeta = { ...validFormValues, - inactivity_ttl_ms: 0, + time_til_dormant_ms: 0, } const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() @@ -235,7 +239,7 @@ describe("TemplateSchedulePage", () => { it("disallows a negative inactivity ttl", () => { const values: UpdateTemplateMeta = { ...validFormValues, - inactivity_ttl_ms: -1, + time_til_dormant_ms: -1, } const validate = () => getValidationSchema().validateSync(values) expect(validate).toThrowError( @@ -246,7 +250,7 @@ describe("TemplateSchedulePage", () => { it("allows a dormancy ttl of 7 days", () => { const values: UpdateTemplateMeta = { ...validFormValues, - locked_ttl_ms: 86400000 * 7, + time_til_dormant_autodelete_ms: 86400000 * 7, } const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() @@ -255,7 +259,7 @@ describe("TemplateSchedulePage", () => { it("allows a dormancy ttl of 0", () => { const values: UpdateTemplateMeta = { ...validFormValues, - locked_ttl_ms: 0, + time_til_dormant_autodelete_ms: 0, } const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() @@ -264,7 +268,7 @@ describe("TemplateSchedulePage", () => { it("disallows a negative inactivity ttl", () => { const values: UpdateTemplateMeta = { ...validFormValues, - locked_ttl_ms: -1, + time_til_dormant_autodelete_ms: -1, } const validate = () => getValidationSchema().validateSync(values) expect(validate).toThrowError( diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index ed810e36769b3..9684e156c8ce7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -23,7 +23,7 @@ import { displayError } from "components/GlobalSnackbar/utils" import { getErrorMessage } from "api/errors" const WorkspacesPage: FC = () => { - const [lockedWorkspaces, setLockedWorkspaces] = useState([]) + const [dormantWorkspaces, setDormantWorkspaces] = useState([]) // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to // each hook. @@ -36,23 +36,23 @@ const WorkspacesPage: FC = () => { }) const experimentEnabled = useIsWorkspaceActionsEnabled() - // If workspace actions are enabled we need to fetch the locked + // If workspace actions are enabled we need to fetch the dormant // workspaces as well. This lets us determine whether we should // show a banner to the user indicating that some of their workspaces // are at risk of being deleted. useEffect(() => { if (experimentEnabled) { - const includesLocked = filterProps.filter.query.includes("locked_at") - const lockedQuery = includesLocked + const includesDormant = filterProps.filter.query.includes("dormant_at") + const dormantQuery = includesDormant ? filterProps.filter.query - : filterProps.filter.query + " locked_at:1970-01-01" + : filterProps.filter.query + " dormant_at:1970-01-01" - if (includesLocked && data) { - setLockedWorkspaces(data.workspaces) + if (includesDormant && data) { + setDormantWorkspaces(data.workspaces) } else { - getWorkspaces({ q: lockedQuery }) + getWorkspaces({ q: dormantQuery }) .then((resp) => { - setLockedWorkspaces(resp.workspaces) + setDormantWorkspaces(resp.workspaces) }) .catch(() => { // TODO @@ -60,8 +60,8 @@ const WorkspacesPage: FC = () => { } } else { // If the experiment isn't included then we'll pretend - // like locked workspaces don't exist. - setLockedWorkspaces([]) + // like dormant workspaces don't exist. + setDormantWorkspaces([]) } }, [experimentEnabled, data, filterProps.filter.query]) const updateWorkspace = useWorkspaceUpdate(queryKey) @@ -90,7 +90,7 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} onCheckChange={setCheckedWorkspaces} workspaces={data?.workspaces} - lockedWorkspaces={lockedWorkspaces} + dormantWorkspaces={dormantWorkspaces} error={error} count={data?.count} page={pagination.page} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 9190db98c9575..cb3a8cd2f2e62 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -38,7 +38,7 @@ export const Language = { export interface WorkspacesPageViewProps { error: unknown workspaces?: Workspace[] - lockedWorkspaces?: Workspace[] + dormantWorkspaces?: Workspace[] checkedWorkspaces: Workspace[] count?: number filterProps: ComponentProps @@ -55,7 +55,7 @@ export const WorkspacesPageView: FC< React.PropsWithChildren > = ({ workspaces, - lockedWorkspaces, + dormantWorkspaces, error, limit, count, @@ -70,12 +70,12 @@ export const WorkspacesPageView: FC< }) => { const { saveLocal } = useLocalStorage() - const workspacesDeletionScheduled = lockedWorkspaces + const workspacesDeletionScheduled = dormantWorkspaces ?.filter((workspace) => workspace.deleting_at) .map((workspace) => workspace.id) - const hasLockedWorkspace = - lockedWorkspaces !== undefined && lockedWorkspaces.length > 0 + const hasDormantWorkspace = + dormantWorkspaces !== undefined && dormantWorkspaces.length > 0 return ( @@ -102,8 +102,8 @@ export const WorkspacesPageView: FC< {/* determines its own visibility */} saveLocal( "dismissedWorkspaceList", diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 8d98c0b03182f..2a1b519a0ceec 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -47,8 +47,8 @@ export const WorkspacesFilter = ({ const presets = [...PRESET_FILTERS] if (useIsWorkspaceActionsEnabled()) { presets.push({ - query: workspaceFilterQuery.locked, - name: "Locked workspaces", + query: workspaceFilterQuery.dormant, + name: "Dormant workspaces", }) } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 2a77a174e8831..2e3438c1fe293 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -445,8 +445,8 @@ export const MockTemplate: TypesGen.Template = { icon: "/icon/code.svg", allow_user_cancel_workspace_jobs: true, failure_ttl_ms: 0, - inactivity_ttl_ms: 0, - locked_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, allow_user_autostart: false, allow_user_autostop: false, } diff --git a/site/src/utils/filters.ts b/site/src/utils/filters.ts index 27f32130aafe6..8f477278b578c 100644 --- a/site/src/utils/filters.ts +++ b/site/src/utils/filters.ts @@ -14,7 +14,7 @@ export const workspaceFilterQuery = { all: "", running: "status:running", failed: "status:failed", - locked: "locked_at:1970-01-01", + dormant: "dormant_at:1970-01-01", } export const userFilterQuery = { diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index d3e9f7204e25c..1706d1af7304f 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -697,7 +697,7 @@ export const workspaceMachine = createMachine( }, activateWorkspace: (context) => async (send) => { if (context.workspace) { - const activateWorkspacePromise = await API.updateWorkspaceLock( + const activateWorkspacePromise = await API.updateWorkspaceDormancy( context.workspace.id, false, ) From 630ec55c48acf18af80d9aef3e7686960ffda08e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Thu, 24 Aug 2023 15:18:42 -0500 Subject: [PATCH 02/40] fix(coderd): remove rate limits from agent metadata (#9308) Include the full update message in the PubSub notification so that we don't have to refresh metadata from the DB and can avoid rate limiting. --- coderd/workspaceagents.go | 174 +++++++++++++++++---------------- coderd/workspaceagents_test.go | 2 +- 2 files changed, 93 insertions(+), 83 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 3f90abb3a4b9b..c127b2342d4a3 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -6,7 +6,6 @@ import ( "database/sql" "encoding/json" "errors" - "flag" "fmt" "io" "net" @@ -23,10 +22,9 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" - "golang.org/x/exp/slices" + "golang.org/x/exp/maps" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" - "golang.org/x/time/rate" "golang.org/x/xerrors" "nhooyr.io/websocket" "tailscale.com/tailcfg" @@ -39,7 +37,6 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/ptr" - "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" @@ -1528,7 +1525,11 @@ func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Reque key := chi.URLParam(r, "key") const ( - maxValueLen = 32 << 10 + // maxValueLen is set to 2048 to stay under the 8000 byte Postgres + // NOTIFY limit. Since both value and error can be set, the real + // payload limit is 2 * 2048 * 4/3 = 5461 bytes + a few hundred bytes for JSON + // syntax, key names, and metadata. + maxValueLen = 2048 maxErrorLen = maxValueLen ) @@ -1571,7 +1572,13 @@ func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Reque slog.F("value", ellipse(datum.Value, 16)), ) - err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), []byte(datum.Key)) + datumJSON, err := json.Marshal(datum) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), datumJSON) if err != nil { httpapi.InternalServerError(rw, err) return @@ -1597,7 +1604,42 @@ func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Requ ) ) - sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r) + // We avoid channel-based synchronization here to avoid backpressure problems. + var ( + metadataMapMu sync.Mutex + metadataMap = make(map[string]database.WorkspaceAgentMetadatum) + // pendingChanges must only be mutated when metadataMapMu is held. + pendingChanges atomic.Bool + ) + + // Send metadata on updates, we must ensure subscription before sending + // initial metadata to guarantee that events in-between are not missed. + cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, byt []byte) { + var update database.UpdateWorkspaceAgentMetadataParams + err := json.Unmarshal(byt, &update) + if err != nil { + api.Logger.Error(ctx, "failed to unmarshal pubsub message", slog.Error(err)) + return + } + + log.Debug(ctx, "received metadata update", "key", update.Key) + + metadataMapMu.Lock() + defer metadataMapMu.Unlock() + md := metadataMap[update.Key] + md.Value = update.Value + md.Error = update.Error + md.CollectedAt = update.CollectedAt + metadataMap[update.Key] = md + pendingChanges.Store(true) + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + defer cancelSub() + + sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error setting up server-sent events.", @@ -1607,97 +1649,61 @@ func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Requ } // Prevent handler from returning until the sender is closed. defer func() { - <-senderClosed + <-sseSenderClosed }() - const refreshInterval = time.Second * 5 - refreshTicker := time.NewTicker(refreshInterval) - defer refreshTicker.Stop() + // We send updates exactly every second. + const sendInterval = time.Second * 1 + sendTicker := time.NewTicker(sendInterval) + defer sendTicker.Stop() - var ( - lastDBMetaMu sync.Mutex - lastDBMeta []database.WorkspaceAgentMetadatum - ) + // We always use the original Request context because it contains + // the RBAC actor. + md, err := api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID) + if err != nil { + // If we can't successfully pull the initial metadata, pubsub + // updates will be no-op so we may as well terminate the + // connection early. + httpapi.InternalServerError(rw, err) + return + } - sendMetadata := func(pull bool) { - log.Debug(ctx, "sending metadata update", "pull", pull) - lastDBMetaMu.Lock() - defer lastDBMetaMu.Unlock() + metadataMapMu.Lock() + for _, datum := range md { + metadataMap[datum.Key] = datum + } + metadataMapMu.Unlock() - var err error - if pull { - // We always use the original Request context because it contains - // the RBAC actor. - lastDBMeta, err = api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID) - if err != nil { - _ = sendEvent(ctx, codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypeError, - Data: codersdk.Response{ - Message: "Internal error getting metadata.", - Detail: err.Error(), - }, - }) - return - } - slices.SortFunc(lastDBMeta, func(a, b database.WorkspaceAgentMetadatum) int { - return slice.Ascending(a.Key, b.Key) - }) + // Send initial metadata. - // Avoid sending refresh if the client is about to get a - // fresh update. - refreshTicker.Reset(refreshInterval) - } + var lastSend time.Time + sendMetadata := func() { + metadataMapMu.Lock() + values := maps.Values(metadataMap) + pendingChanges.Store(false) + metadataMapMu.Unlock() - _ = sendEvent(ctx, codersdk.ServerSentEvent{ + lastSend = time.Now() + _ = sseSendEvent(ctx, codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, - Data: convertWorkspaceAgentMetadata(lastDBMeta), + Data: convertWorkspaceAgentMetadata(values), }) } - // Note: we previously used a debounce here, but when the rate of metadata updates was too - // high the debounce would never fire. - // - // The rate-limit has its own caveat. If the agent sends a burst of metadata - // but then goes quiet, we will never pull the new metadata and the frontend - // will go stale until refresh. This would only happen if the agent was - // under extreme load. Under normal operations, the interval between metadata - // updates is constant so there is no burst phenomenon. - pubsubRatelimit := rate.NewLimiter(rate.Every(time.Second), 2) - if flag.Lookup("test.v") != nil { - // We essentially disable the rate-limit in tests for determinism. - pubsubRatelimit = rate.NewLimiter(rate.Every(time.Second*100), 100) - } - - // Send metadata on updates, we must ensure subscription before sending - // initial metadata to guarantee that events in-between are not missed. - cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) { - allow := pubsubRatelimit.Allow() - log.Debug(ctx, "received metadata update", "allow", allow) - if allow { - sendMetadata(true) - } - }) - if err != nil { - httpapi.InternalServerError(rw, err) - return - } - defer cancelSub() - - // Send initial metadata. - sendMetadata(true) + sendMetadata() for { select { - case <-senderClosed: + case <-sendTicker.C: + // We send an update even if there's no change every 5 seconds + // to ensure that the frontend always has an accurate "Result.Age". + if !pendingChanges.Load() && time.Since(lastSend) < time.Second*5 { + continue + } + sendMetadata() + case <-sseSenderClosed: return - case <-refreshTicker.C: } - - // Avoid spamming the DB with reads we know there are no updates. We want - // to continue sending updates to the frontend so that "Result.Age" - // is always accurate. This way, the frontend doesn't need to - // sync its own clock with the backend. - sendMetadata(false) } } @@ -1721,6 +1727,10 @@ func convertWorkspaceAgentMetadata(db []database.WorkspaceAgentMetadatum) []code }, }) } + // Sorting prevents the metadata from jumping around in the frontend. + sort.Slice(result, func(i, j int) bool { + return result[i].Description.Key < result[j].Description.Key + }) return result } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index d1b379d3d74e7..48a399cee3db5 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1153,7 +1153,7 @@ func TestWorkspaceAgent_Metadata(t *testing.T) { require.Len(t, update, 3) check(wantMetadata1, update[0], true) - const maxValueLen = 32 << 10 + const maxValueLen = 2048 tooLongValueMetadata := wantMetadata1 tooLongValueMetadata.Value = strings.Repeat("a", maxValueLen*2) tooLongValueMetadata.Error = "" From 7ddb216d871d16f15f2b5b4394069e578137d30c Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 24 Aug 2023 21:26:30 -0500 Subject: [PATCH 03/40] chore: revert nix-related CI changes (#9321) * chore: revert nix-related CI changes - Reverts using nix to run CI-dependencies. - Running 'make gen' in a dogfood workspace resulted in inconsistent results for protobuf-related files making it difficult to pass CI. This PR imports the minimum changes necessary to make CI compatible with dogfood. --- .github/workflows/ci.yaml | 53 +++++++++++++++++---- provisionerd/proto/provisionerd.pb.go | 4 +- provisionerd/proto/provisionerd_drpc.pb.go | 2 +- provisionersdk/proto/provisioner.pb.go | 4 +- provisionersdk/proto/provisioner_drpc.pb.go | 10 +++- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e02e3fd63fd7..66bf59853bb97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -169,14 +169,35 @@ jobs: with: fetch-depth: 1 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v4 + - name: Setup Node + uses: ./.github/actions/setup-node + + - name: Setup Go + uses: ./.github/actions/setup-go - - name: Run the Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@v2 + - name: Setup sqlc + uses: ./.github/actions/setup-sqlc + + - name: go install tools + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 + go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 + go install golang.org/x/tools/cmd/goimports@latest + go install github.com/mikefarah/yq/v4@v4.30.6 + go install github.com/golang/mock/mockgen@v1.6.0 + + - name: Install Protoc + run: | + mkdir -p /tmp/proto + pushd /tmp/proto + curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip + unzip protoc.zip + cp -r ./bin/* /usr/local/bin + cp -r ./include /usr/local/bin/include + popd - name: make gen - run: "nix-shell --command 'make --output-sync -j -B gen'" + run: "make --output-sync -j -B gen" - name: Check for unstaged files run: ./scripts/check_unstaged.sh @@ -508,15 +529,27 @@ jobs: - name: Setup Terraform uses: ./.github/actions/setup-tf - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v4 + - name: go install tools + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 + go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 + go install golang.org/x/tools/cmd/goimports@latest + go install github.com/mikefarah/yq/v4@v4.30.6 + go install github.com/golang/mock/mockgen@v1.6.0 - - name: Run the Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@v2 + - name: Install Protoc + run: | + mkdir -p /tmp/proto + pushd /tmp/proto + curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip + unzip protoc.zip + cp -r ./bin/* /usr/local/bin + cp -r ./include /usr/local/bin/include + popd - name: Build run: | - nix-shell --command 'make -B site/out/index.html' + make -B site/out/index.html - run: pnpm playwright:install working-directory: site diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 7d949b9c9c2f0..29a1e7dc505a9 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v3.21.12 +// protoc-gen-go v1.30.0 +// protoc v4.23.3 // source: provisionerd/proto/provisionerd.proto package proto diff --git a/provisionerd/proto/provisionerd_drpc.pb.go b/provisionerd/proto/provisionerd_drpc.pb.go index 058af595809b8..ed3155fb21eaa 100644 --- a/provisionerd/proto/provisionerd_drpc.pb.go +++ b/provisionerd/proto/provisionerd_drpc.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-drpc. DO NOT EDIT. -// protoc-gen-go-drpc version: (devel) +// protoc-gen-go-drpc version: v0.0.33 // source: provisionerd/proto/provisionerd.proto package proto diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index c334ad13a5ac9..f39e9731e6101 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v3.21.12 +// protoc-gen-go v1.30.0 +// protoc v4.23.3 // source: provisionersdk/proto/provisioner.proto package proto diff --git a/provisionersdk/proto/provisioner_drpc.pb.go b/provisionersdk/proto/provisioner_drpc.pb.go index d307402447c78..d8b40060cd376 100644 --- a/provisionersdk/proto/provisioner_drpc.pb.go +++ b/provisionersdk/proto/provisioner_drpc.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go-drpc. DO NOT EDIT. -// protoc-gen-go-drpc version: (devel) +// protoc-gen-go-drpc version: v0.0.33 // source: provisionersdk/proto/provisioner.proto package proto @@ -76,6 +76,10 @@ type drpcProvisioner_ParseClient struct { drpc.Stream } +func (x *drpcProvisioner_ParseClient) GetStream() drpc.Stream { + return x.Stream +} + func (x *drpcProvisioner_ParseClient) Recv() (*Parse_Response, error) { m := new(Parse_Response) if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil { @@ -107,6 +111,10 @@ type drpcProvisioner_ProvisionClient struct { drpc.Stream } +func (x *drpcProvisioner_ProvisionClient) GetStream() drpc.Stream { + return x.Stream +} + func (x *drpcProvisioner_ProvisionClient) Send(m *Provision_Request) error { return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) } From 4bed4920129545e362ae2908c6aefd3671e6710c Mon Sep 17 00:00:00 2001 From: sharkymark Date: Thu, 24 Aug 2023 22:23:59 -0500 Subject: [PATCH 04/40] docs: ui option for adding licenses (#9322) --- docs/enterprise.md | 15 ++++++++++++--- docs/images/add-license-ui.png | Bin 0 -> 57668 bytes package.json | 3 +++ pnpm-lock.yaml | 11 +++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 docs/images/add-license-ui.png diff --git a/docs/enterprise.md b/docs/enterprise.md index f2a84adf86c20..60b4af4be19e0 100644 --- a/docs/enterprise.md +++ b/docs/enterprise.md @@ -20,11 +20,20 @@ paid license. [Contact Sales](https://coder.com/contact) for pricing or | Deployment | [Isolated Terraform Runners](./admin/provisioners.md) | ❌ | ✅ | | Deployment | [Workspace Proxies](./admin/workspace-proxies.md) | ❌ | ✅ | -> Previous plans to restrict OIDC and Git Auth features in OSS have been removed -> as of 2023-01-11 - ## Adding your license key +There are two ways to add an enterprise license to a Coder deployment: In the +Coder UI or with the Coder CLI. + +### Coder UI + +Click Deployment, Licenses, Add a license then drag or select the license file +with the `jwt` extension. + +![Add License UI](./images/add-license-ui.png) + +### Coder CLI + ### Requirements - Your license key diff --git a/docs/images/add-license-ui.png b/docs/images/add-license-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..03ff419d15a59d9b000587ad83cebbc58e2955a7 GIT binary patch literal 57668 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 vIYh6^UXn5hn;Pq`O= v0.9.1'} + deprecated: deprecated in favor of builtin child_process.execFile + dev: false + /prettier@3.0.0: resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} engines: {node: '>=14'} From 60d5002eb68e6cfc1ccce578a7d00bbb5d11c6e5 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 25 Aug 2023 10:10:15 +0400 Subject: [PATCH 05/40] refactor: change template archive extraction to be on provisioner (#9264) * refactor provisionersdk protocol Signed-off-by: Spike Curtis * refactor provisioners to use new protocol Signed-off-by: Spike Curtis * refactor provisionerd to use new protocol Signed-off-by: Spike Curtis * refactor tests & proto renames * Fixes from self-review Signed-off-by: Spike Curtis * appease fmt & link Signed-off-by: Spike Curtis * code review fixes & e2e fixes Signed-off-by: Spike Curtis * More fmt Signed-off-by: Spike Curtis * Code review fixes Signed-off-by: Spike Curtis * new gen; use uuid for session workdir Signed-off-by: Spike Curtis * Revert nix-based gen CI task until dogfood is on nix Signed-off-by: Spike Curtis * revert deleting dogfood Docker stuff Signed-off-by: Spike Curtis * Revert "revert deleting dogfood Docker stuff" This reverts commit 97621581670e21945c78ac46955c1862727914c7. --------- Signed-off-by: Spike Curtis --- Makefile | 8 +- cli/agent_test.go | 18 +- cli/configssh_test.go | 21 +- cli/create_test.go | 95 +- cli/gitssh_test.go | 2 +- cli/portforward_test.go | 2 +- cli/restart_test.go | 42 +- cli/server.go | 13 +- cli/show_test.go | 7 +- cli/ssh_test.go | 8 +- cli/start_test.go | 24 +- cli/state_test.go | 16 +- cli/templatecreate_test.go | 77 +- cli/templatepull_test.go | 10 +- cli/templatepush_test.go | 31 +- cli/update_test.go | 55 +- coderd/activitybump_test.go | 23 +- coderd/autobuild/lifecycle_executor_test.go | 22 +- coderd/coderd_test.go | 2 +- coderd/coderdtest/coderdtest.go | 20 +- coderd/gitauth_test.go | 30 +- coderd/gitsshkey_test.go | 2 +- coderd/insights_test.go | 16 +- .../prometheusmetrics_test.go | 10 +- .../provisionerdserver/provisionerdserver.go | 6 +- .../provisionerdserver_test.go | 8 +- coderd/provisionerjobs_test.go | 16 +- coderd/templates_test.go | 2 +- coderd/templateversions_test.go | 88 +- coderd/workspaceagents_test.go | 88 +- coderd/workspaceapps/apptest/setup.go | 8 +- coderd/workspaceapps/db_test.go | 8 +- coderd/workspacebuilds_test.go | 65 +- coderd/workspaceresourceauth_test.go | 18 +- coderd/workspaces_test.go | 108 +- enterprise/cli/provisionerdaemons.go | 16 +- enterprise/coderd/appearance_test.go | 2 +- enterprise/coderd/provisionerdaemons_test.go | 6 +- enterprise/coderd/workspaceagents_test.go | 6 +- enterprise/coderd/workspacequota_test.go | 70 +- enterprise/coderd/workspaces_test.go | 40 +- provisioner/echo/serve.go | 291 ++-- provisioner/echo/serve_test.go | 306 ++-- provisioner/terraform/executor.go | 122 +- .../terraform/executor_internal_test.go | 4 +- provisioner/terraform/parse.go | 22 +- provisioner/terraform/parse_test.go | 210 +-- provisioner/terraform/provision.go | 252 ++- provisioner/terraform/provision_test.go | 521 +++--- provisioner/terraform/serve.go | 2 +- provisioner/terraform/testdata/fake_cancel.sh | 15 +- .../terraform/testdata/fake_cancel_hang.sh | 12 +- provisionerd/proto/provisionerd.pb.go | 390 +++-- provisionerd/proto/provisionerd.proto | 16 +- provisionerd/provisionerd.go | 9 - provisionerd/provisionerd_test.go | 452 +++--- provisionerd/runner/runner.go | 406 ++--- provisionersdk/errors.go | 19 + provisionersdk/proto/provisioner.pb.go | 1417 ++++++++--------- provisionersdk/proto/provisioner.proto | 183 ++- provisionersdk/proto/provisioner_drpc.pb.go | 124 +- provisionersdk/serve.go | 27 +- provisionersdk/serve_test.go | 50 +- provisionersdk/session.go | 318 ++++ provisionersdk/transport.go | 2 +- scaletest/agentconn/run_test.go | 8 +- scaletest/createworkspaces/run_test.go | 34 +- scaletest/reconnectingpty/run_test.go | 8 +- scaletest/workspacebuild/run_test.go | 18 +- scaletest/workspacetraffic/run_test.go | 16 +- site/e2e/helpers.ts | 201 ++- site/e2e/provisionerGenerated.ts | 314 ++-- site/e2e/tests/app.spec.ts | 2 +- site/e2e/tests/createWorkspace.spec.ts | 2 +- site/e2e/tests/outdatedAgent.spec.ts | 2 +- site/e2e/tests/outdatedCLI.spec.ts | 2 +- site/e2e/tests/webTerminal.spec.ts | 2 +- 77 files changed, 3426 insertions(+), 3462 deletions(-) create mode 100644 provisionersdk/errors.go create mode 100644 provisionersdk/session.go diff --git a/Makefile b/Makefile index 62528464cb419..56acd83ff70c8 100644 --- a/Makefile +++ b/Makefile @@ -456,10 +456,10 @@ DB_GEN_FILES := \ # all gen targets should be added here and to gen/mark-fresh gen: \ - coderd/database/dump.sql \ - $(DB_GEN_FILES) \ provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ + coderd/database/dump.sql \ + $(DB_GEN_FILES) \ site/src/api/typesGenerated.ts \ coderd/rbac/object_gen.go \ docs/admin/prometheus.md \ @@ -478,10 +478,10 @@ gen: \ # used during releases so we don't run generation scripts. gen/mark-fresh: files="\ - coderd/database/dump.sql \ - $(DB_GEN_FILES) \ provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ + coderd/database/dump.sql \ + $(DB_GEN_FILES) \ site/src/api/typesGenerated.ts \ coderd/rbac/object_gen.go \ docs/admin/prometheus.md \ diff --git a/cli/agent_test.go b/cli/agent_test.go index d33bb55d00b2f..7073f7c0f18ca 100644 --- a/cli/agent_test.go +++ b/cli/agent_test.go @@ -75,9 +75,9 @@ func TestWorkspaceAgent(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", @@ -127,9 +127,9 @@ func TestWorkspaceAgent(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", @@ -179,9 +179,9 @@ func TestWorkspaceAgent(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", diff --git a/cli/configssh_test.go b/cli/configssh_test.go index a592efa424300..44246da2596e7 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -82,9 +82,9 @@ func TestConfigSSH(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -720,22 +720,11 @@ func TestConfigSSH_Hostnames(t *testing.T) { resources = append(resources, resource) } - provisionResponse := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: resources, - }, - }, - }} - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) // authToken := uuid.NewString() - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: provisionResponse, - ProvisionApply: provisionResponse, - }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, + echo.WithResources(resources)) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) diff --git a/cli/create_test.go b/cli/create_test.go index f3436c0268f3d..bdd229775ec68 100644 --- a/cli/create_test.go +++ b/cli/create_test.go @@ -29,11 +29,7 @@ func TestCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - ProvisionPlan: provisionCompleteWithAgent, - }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent()) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) args := []string{ @@ -84,11 +80,7 @@ func TestCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) owner := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - ProvisionPlan: provisionCompleteWithAgent, - }) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent()) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) _, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) @@ -141,11 +133,7 @@ func TestCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - ProvisionPlan: provisionCompleteWithAgent, - }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent()) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { var defaultTTLMillis int64 = 2 * 60 * 60 * 1000 // 2 hours @@ -240,6 +228,22 @@ func TestCreate(t *testing.T) { }) } +func prepareEchoResponses(parameters []*proto.RichParameter) *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Parameters: parameters, + }, + }, + }, + }, + ProvisionApply: echo.ApplyComplete, + } +} + func TestCreateWithRichParameters(t *testing.T) { t.Parallel() @@ -258,27 +262,12 @@ func TestCreateWithRichParameters(t *testing.T) { immutableParameterValue = "4" ) - echoResponses := &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: []*proto.RichParameter{ - {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, - {Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true}, - {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, - }, - }, - }, - }, - }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, - } + echoResponses := prepareEchoResponses([]*proto.RichParameter{ + {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, + {Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true}, + {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, + }, + ) t.Run("InputParameters", func(t *testing.T) { t.Parallel() @@ -427,28 +416,6 @@ func TestCreateValidateRichParameters(t *testing.T) { {Name: boolParameterName, Type: "bool", Mutable: true}, } - prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses { - return &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: richParameters, - }, - }, - }, - }, - ProvisionApply: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }, - }, - } - } - t.Run("ValidateString", func(t *testing.T) { t.Parallel() @@ -626,20 +593,16 @@ func TestCreateWithGitAuth(t *testing.T) { t.Parallel() echoResponses := &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ GitAuthProviders: []string{"github"}, }, }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionApply: echo.ApplyComplete, } client := coderdtest.New(t, &coderdtest.Options{ diff --git a/cli/gitssh_test.go b/cli/gitssh_test.go index 9c38ef945ba7b..3e5045acf0288 100644 --- a/cli/gitssh_test.go +++ b/cli/gitssh_test.go @@ -48,7 +48,7 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str agentToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(agentToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/cli/portforward_test.go b/cli/portforward_test.go index ce480768525f2..030133a7ae317 100644 --- a/cli/portforward_test.go +++ b/cli/portforward_test.go @@ -302,7 +302,7 @@ func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) codersdk. agentToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(agentToken), }) diff --git a/cli/restart_test.go b/cli/restart_test.go index b68a3f843ba51..43b512c1bc30b 100644 --- a/cli/restart_test.go +++ b/cli/restart_test.go @@ -20,30 +20,14 @@ import ( func TestRestart(t *testing.T) { t.Parallel() - echoResponses := &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: []*proto.RichParameter{ - { - Name: ephemeralParameterName, - Description: ephemeralParameterDescription, - Mutable: true, - Ephemeral: true, - }, - }, - }, - }, - }, + echoResponses := prepareEchoResponses([]*proto.RichParameter{ + { + Name: ephemeralParameterName, + Description: ephemeralParameterDescription, + Mutable: true, + Ephemeral: true, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, - } + }) t.Run("OK", func(t *testing.T) { t.Parallel() @@ -187,10 +171,10 @@ func TestRestartWithParameters(t *testing.T) { echoResponses := &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: immutableParameterName, @@ -202,11 +186,7 @@ func TestRestartWithParameters(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionApply: echo.ApplyComplete, } t.Run("DoNotAskForImmutables", func(t *testing.T) { diff --git a/cli/server.go b/cli/server.go index 1cd6ec475747a..9d6b4f975cc79 100644 --- a/cli/server.go +++ b/cli/server.go @@ -41,7 +41,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/spf13/afero" "go.opentelemetry.io/otel/trace" "golang.org/x/mod/semver" "golang.org/x/oauth2" @@ -1304,7 +1303,11 @@ func newProvisionerDaemon( defer wg.Done() defer cancel() - err := echo.Serve(ctx, afero.NewOsFs(), &provisionersdk.ServeOptions{Listener: echoServer}) + err := echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: echoServer, + WorkDirectory: workDir, + Logger: logger.Named("echo"), + }) if err != nil { select { case errCh <- err: @@ -1336,10 +1339,11 @@ func newProvisionerDaemon( err := terraform.Serve(ctx, &terraform.ServeOptions{ ServeOptions: &provisionersdk.ServeOptions{ - Listener: terraformServer, + Listener: terraformServer, + Logger: logger.Named("terraform"), + WorkDirectory: workDir, }, CachePath: tfDir, - Logger: logger.Named("terraform"), Tracer: tracer, }) if err != nil && !xerrors.Is(err, context.Canceled) { @@ -1366,7 +1370,6 @@ func newProvisionerDaemon( UpdateInterval: time.Second, ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value(), Provisioners: provisioners, - WorkDirectory: workDir, TracerProvider: coderAPI.TracerProvider, Metrics: &metrics, }), nil diff --git a/cli/show_test.go b/cli/show_test.go index c77e5dc0171cb..ccbe182cc7ed9 100644 --- a/cli/show_test.go +++ b/cli/show_test.go @@ -7,7 +7,6 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/pty/ptytest" ) @@ -17,11 +16,7 @@ func TestShow(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - ProvisionPlan: provisionCompleteWithAgent, - }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent()) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 5e670d44d0689..971dc2873ffdc 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -56,10 +56,10 @@ func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.A agentToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "dev", Type: "google_compute_instance", diff --git a/cli/start_test.go b/cli/start_test.go index e92d70cd71624..dff4048f3e765 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -33,10 +33,10 @@ func TestStart(t *testing.T) { echoResponses := &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: ephemeralParameterName, @@ -49,11 +49,7 @@ func TestStart(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionApply: echo.ApplyComplete, } t.Run("BuildOptions", func(t *testing.T) { @@ -151,10 +147,10 @@ func TestStartWithParameters(t *testing.T) { echoResponses := &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: immutableParameterName, @@ -166,11 +162,7 @@ func TestStartWithParameters(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionApply: echo.ApplyComplete, } t.Run("DoNotAskForImmutables", func(t *testing.T) { diff --git a/cli/state_test.go b/cli/state_test.go index 6273135d39c84..a240a6d2c81ae 100644 --- a/cli/state_test.go +++ b/cli/state_test.go @@ -25,9 +25,9 @@ func TestStatePull(t *testing.T) { wantState := []byte("some state") version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ State: wantState, }, }, @@ -53,9 +53,9 @@ func TestStatePull(t *testing.T) { wantState := []byte("some state") version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ State: wantState, }, }, @@ -83,7 +83,7 @@ func TestStatePush(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -108,7 +108,7 @@ func TestStatePush(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go index 1c3d6d6c954bc..ba5dad7b4ac6a 100644 --- a/cli/templatecreate_test.go +++ b/cli/templatecreate_test.go @@ -19,26 +19,52 @@ import ( "github.com/coder/coder/v2/testutil" ) -var provisionCompleteWithAgent = []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{ - { - Type: "compute", - Name: "main", - Agents: []*proto.Agent{ +func completeWithAgent() *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ { - Name: "smith", - OperatingSystem: "linux", - Architecture: "i386", + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, }, }, }, }, }, }, - }, + } } func TestTemplateCreate(t *testing.T) { @@ -47,10 +73,7 @@ func TestTemplateCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) args := []string{ "templates", "create", @@ -85,10 +108,7 @@ func TestTemplateCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) args := []string{ "templates", @@ -128,10 +148,7 @@ func TestTemplateCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) args := []string{ "templates", @@ -167,10 +184,7 @@ func TestTemplateCreate(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) coderdtest.CreateFirstUser(t, client) - source, err := echo.Tar(&echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source, err := echo.Tar(completeWithAgent()) require.NoError(t, err) args := []string{ @@ -196,10 +210,7 @@ func TestTemplateCreate(t *testing.T) { coderdtest.CreateFirstUser(t, client) create := func() error { - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) args := []string{ "templates", "create", diff --git a/cli/templatepull_test.go b/cli/templatepull_test.go index f6d8ececc9356..95b0a6cf9aa30 100644 --- a/cli/templatepull_test.go +++ b/cli/templatepull_test.go @@ -205,9 +205,9 @@ func TestTemplatePull(t *testing.T) { // a template version source. func genTemplateVersionSource() *echo.Responses { return &echo.Responses{ - Parse: []*proto.Parse_Response{ + Parse: []*proto.Response{ { - Type: &proto.Parse_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Output: uuid.NewString(), }, @@ -215,11 +215,11 @@ func genTemplateVersionSource() *echo.Responses { }, { - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{}, + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{}, }, }, }, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, } } diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 4f2e00a359be2..4c41597802bb2 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -38,7 +38,7 @@ func TestTemplatePush(t *testing.T) { // Test the cli command. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example") clitest.SetupConfig(t, client, root) @@ -82,7 +82,7 @@ func TestTemplatePush(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) wantMessage := strings.Repeat("a", 72) @@ -121,7 +121,7 @@ func TestTemplatePush(t *testing.T) { template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -168,7 +168,7 @@ func TestTemplatePush(t *testing.T) { // Test the cli command. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) @@ -211,7 +211,7 @@ func TestTemplatePush(t *testing.T) { // Test the cli command. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) @@ -248,7 +248,7 @@ func TestTemplatePush(t *testing.T) { // Test the cli command. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) inv, root := clitest.New(t, "templates", "push", template.Name, "--activate=false", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example") clitest.SetupConfig(t, client, root) @@ -293,7 +293,7 @@ func TestTemplatePush(t *testing.T) { // Test the cli command. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, @@ -340,7 +340,7 @@ func TestTemplatePush(t *testing.T) { source, err := echo.Tar(&echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, }) require.NoError(t, err) @@ -619,10 +619,7 @@ func TestTemplatePush(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: provisionCompleteWithAgent, - }) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) const templateName = "my-template" args := []string{ @@ -665,16 +662,16 @@ func TestTemplatePush(t *testing.T) { func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses { return &echo.Responses{ - Parse: []*proto.Parse_Response{ + Parse: []*proto.Response{ { - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{ TemplateVariables: templateVariables, }, }, }, }, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, } } diff --git a/cli/update_test.go b/cli/update_test.go index 57e49d0db3766..0efa1f997cb69 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -57,8 +57,8 @@ func TestUpdate(t *testing.T) { version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, + ProvisionPlan: echo.PlanComplete, }, template.ID) _ = coderdtest.AwaitTemplateVersionJob(t, client, version2.ID) @@ -100,28 +100,13 @@ func TestUpdateWithRichParameters(t *testing.T) { immutableParameterValue = "4" ) - echoResponses := &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: []*proto.RichParameter{ - {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, - {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, - {Name: secondParameterName, Description: secondParameterDescription, Mutable: true}, - {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true}, - }, - }, - }, - }, - }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, - } + echoResponses := prepareEchoResponses([]*proto.RichParameter{ + {Name: firstParameterName, Description: firstParameterDescription, Mutable: true}, + {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false}, + {Name: secondParameterName, Description: secondParameterDescription, Mutable: true}, + {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true}, + }, + ) t.Run("ImmutableCannotBeCustomized", func(t *testing.T) { t.Parallel() @@ -313,28 +298,6 @@ func TestUpdateValidateRichParameters(t *testing.T) { {Name: boolParameterName, Type: "bool", Mutable: true}, } - prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses { - return &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: richParameters, - }, - }, - }, - }, - ProvisionApply: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }, - }, - } - } - t.Run("ValidateString", func(t *testing.T) { t.Parallel() diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 15965f5ab1fea..8ce018a4e9a20 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -17,7 +17,6 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisioner/echo" - "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) @@ -60,25 +59,9 @@ func TestWorkspaceActivityBump(t *testing.T) { ttlMillis := int64(ttl / time.Millisecond) agentToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agents: []*proto.Agent{{ - Id: uuid.NewString(), - Name: "agent", - Auth: &proto.Agent_Token{ - Token: agentToken, - }, - }}, - }}, - }, - }, - }}, + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(agentToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 356926d9bbff9..7159f3b7d5665 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -683,8 +683,8 @@ func TestExecutorFailedWorkspace(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionFailed, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyFailed, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) @@ -733,8 +733,8 @@ func TestExecutorInactiveWorkspace(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) @@ -766,22 +766,16 @@ func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client, user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: richParameters, }, }, }, }, - ProvisionApply: []*proto.Provision_Response{ - { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }, - }, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 1924c68439508..6edf4657cc903 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -181,7 +181,7 @@ func TestDERPForceWebSockets(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index b915b9ffbd3da..18062a549a8bd 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -37,7 +37,6 @@ import ( "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -469,10 +468,13 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer { _ = echoServer.Close() cancelFunc() }) - fs := afero.NewMemMapFs() + // seems t.TempDir() is not safe to call from a different goroutine + workDir := t.TempDir() go func() { - err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{ - Listener: echoServer, + err := echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: echoServer, + WorkDirectory: workDir, + Logger: coderAPI.Logger.Named("echo").Leveled(slog.LevelDebug), }) assert.NoError(t, err) }() @@ -480,7 +482,6 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer { closer := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { return coderAPI.CreateInMemoryProvisionerDaemon(ctx, 0) }, &provisionerd.Options{ - Filesystem: fs, Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug), JobPollInterval: 50 * time.Millisecond, UpdateInterval: 250 * time.Millisecond, @@ -488,7 +489,6 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer { Provisioners: provisionerd.Provisioners{ string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), }, - WorkDirectory: t.TempDir(), }) t.Cleanup(func() { _ = closer.Close() @@ -506,11 +506,11 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui cancelFunc() <-serveDone }) - fs := afero.NewMemMapFs() go func() { defer close(serveDone) - err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{ - Listener: echoServer, + err := echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: echoServer, + WorkDirectory: t.TempDir(), }) assert.NoError(t, err) }() @@ -522,7 +522,6 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui Tags: tags, }) }, &provisionerd.Options{ - Filesystem: fs, Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), JobPollInterval: 50 * time.Millisecond, UpdateInterval: 250 * time.Millisecond, @@ -530,7 +529,6 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui Provisioners: provisionerd.Provisioners{ string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), }, - WorkDirectory: t.TempDir(), }) t.Cleanup(func() { _ = closer.Close() diff --git a/coderd/gitauth_test.go b/coderd/gitauth_test.go index 6aa4d4735b06f..c0ad89a1b53cc 100644 --- a/coderd/gitauth_test.go +++ b/coderd/gitauth_test.go @@ -23,7 +23,6 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/provisioner/echo" - "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) @@ -227,7 +226,7 @@ func TestGitAuthCallback(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -256,24 +255,9 @@ func TestGitAuthCallback(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agents: []*proto.Agent{{ - Id: uuid.NewString(), - Auth: &proto.Agent_Token{ - Token: authToken, - }, - }}, - }}, - }, - }, - }}, + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -342,7 +326,7 @@ func TestGitAuthCallback(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -400,7 +384,7 @@ func TestGitAuthCallback(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -443,7 +427,7 @@ func TestGitAuthCallback(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index a406ed6a1d5d2..be1f43c52eb2f 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -108,7 +108,7 @@ func TestAgentGitSSHKey(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 351905e9b698e..83498bbb365fd 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -48,7 +48,7 @@ func TestDeploymentInsights(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -134,7 +134,7 @@ func TestUserLatencyInsights(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -498,18 +498,18 @@ func TestTemplateInsights_Golden(t *testing.T) { // Create the template version and template. version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: parameters, }, }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: resources, }, }, diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index c6d65418cb4b6..bf6f475ad1be6 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -268,10 +268,10 @@ func TestAgents(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -494,7 +494,7 @@ func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user coders version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index efb942f8a32ae..f61606a1425b9 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -280,7 +280,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac RichParameterValues: convertRichParameterValues(workspaceBuildParameters), VariableValues: asVariableValues(templateVariables), GitAuthProviders: gitAuthProviders, - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: server.AccessURL.String(), WorkspaceTransition: transition, WorkspaceName: workspace.Name, @@ -316,7 +316,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac TemplateDryRun: &proto.AcquiredJob_TemplateDryRun{ RichParameterValues: convertRichParameterValues(input.RichParameterValues), VariableValues: asVariableValues(templateVariables), - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: server.AccessURL.String(), WorkspaceName: input.WorkspaceName, }, @@ -337,7 +337,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac protoJob.Type = &proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ UserVariableValues: convertVariableValues(userVariableValues), - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: server.AccessURL.String(), }, }, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index f53933722458c..5a317cd531530 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -267,7 +267,7 @@ func TestAcquireJob(t *testing.T) { Id: gitAuthProvider, AccessToken: "access_token", }}, - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: srv.AccessURL.String(), WorkspaceTransition: sdkproto.WorkspaceTransition_START, WorkspaceName: workspace.Name, @@ -359,7 +359,7 @@ func TestAcquireJob(t *testing.T) { want, err := json.Marshal(&proto.AcquiredJob_TemplateDryRun_{ TemplateDryRun: &proto.AcquiredJob_TemplateDryRun{ - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: srv.AccessURL.String(), WorkspaceName: "testing", }, @@ -391,7 +391,7 @@ func TestAcquireJob(t *testing.T) { want, err := json.Marshal(&proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: srv.AccessURL.String(), }, }, @@ -434,7 +434,7 @@ func TestAcquireJob(t *testing.T) { UserVariableValues: []*sdkproto.VariableValue{ {Name: "first", Sensitive: true, Value: "first_value"}, }, - Metadata: &sdkproto.Provision_Metadata{ + Metadata: &sdkproto.Metadata{ CoderUrl: srv.AccessURL.String(), }, }, diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 0bfd00e46a143..5d1715a9fe52d 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -20,16 +20,16 @@ func TestProvisionerJobLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "log-output", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, }) @@ -59,16 +59,16 @@ func TestProvisionerJobLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "log-output", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, }) diff --git a/coderd/templates_test.go b/coderd/templates_test.go index fcdb7e64e2e78..403370b5da670 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -1203,7 +1203,7 @@ func TestTemplateMetrics(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index f81b29ee82d4c..d06e68fabb368 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -136,8 +136,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) data, err := echo.Tar(&echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: echo.ProvisionComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: echo.ApplyComplete, + ProvisionPlan: echo.PlanComplete, }) require.NoError(t, err) @@ -245,8 +245,8 @@ func TestPatchCancelTemplateVersion(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }}, @@ -284,8 +284,8 @@ func TestPatchCancelTemplateVersion(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }}, @@ -346,9 +346,9 @@ func TestTemplateVersionsGitAuth(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ GitAuthProviders: []string{"github"}, }, }, @@ -400,9 +400,9 @@ func TestTemplateVersionResources(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -439,17 +439,17 @@ func TestTemplateVersionLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "example", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -610,15 +610,15 @@ func TestTemplateVersionDryRun(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{resource}, }, }, @@ -677,8 +677,8 @@ func TestTemplateVersionDryRun(t *testing.T) { // This import job will never finish version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }}, @@ -705,15 +705,15 @@ func TestTemplateVersionDryRun(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }, }, @@ -776,15 +776,15 @@ func TestTemplateVersionDryRun(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }, }, @@ -1040,21 +1040,17 @@ func TestTemplateVersionVariables(t *testing.T) { createEchoResponses := func(templateVariables []*proto.TemplateVariable) *echo.Responses { return &echo.Responses{ - Parse: []*proto.Parse_Response{ + Parse: []*proto.Response{ { - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{ TemplateVariables: templateVariables, }, }, }, }, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, } } @@ -1418,10 +1414,10 @@ func TestTemplateVersionParameters_Order(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: firstParameterName, @@ -1453,11 +1449,7 @@ func TestTemplateVersionParameters_Order(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, + ProvisionApply: echo.ApplyComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 48a399cee3db5..43694e95e67a9 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -43,10 +43,10 @@ func TestWorkspaceAgent(t *testing.T) { tmpDir := t.TempDir() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -87,10 +87,10 @@ func TestWorkspaceAgent(t *testing.T) { tmpDir := t.TempDir() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -132,10 +132,10 @@ func TestWorkspaceAgent(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -188,10 +188,10 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -252,10 +252,10 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -319,7 +319,7 @@ func TestWorkspaceAgentListen(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -360,7 +360,7 @@ func TestWorkspaceAgentListen(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) @@ -371,10 +371,10 @@ func TestWorkspaceAgentListen(t *testing.T) { version = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -419,7 +419,7 @@ func TestWorkspaceAgentTailnet(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -471,7 +471,7 @@ func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -548,10 +548,10 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -806,9 +806,9 @@ func TestWorkspaceAgentAppHealth(t *testing.T) { } version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -893,7 +893,7 @@ func TestWorkspaceAgentReportStats(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -942,7 +942,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1014,10 +1014,10 @@ func TestWorkspaceAgent_Metadata(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -1184,7 +1184,7 @@ func TestWorkspaceAgent_Startup(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1238,7 +1238,7 @@ func TestWorkspaceAgent_Startup(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1293,7 +1293,7 @@ func TestWorkspaceAgent_UpdatedDERP(t *testing.T) { agentToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(agentToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index 9d9a80490146f..6ab541f078072 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -288,10 +288,10 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery) version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/coderd/workspaceapps/db_test.go b/coderd/workspaceapps/db_test.go index 1783fcec17cda..163247f6d4e4f 100644 --- a/coderd/workspaceapps/db_test.go +++ b/coderd/workspaceapps/db_test.go @@ -94,10 +94,10 @@ func Test_ResolveRequest(t *testing.T) { agentAuthToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index cd62f6a75046c..0ee810e3e3eda 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -376,12 +376,12 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }}, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -401,11 +401,12 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { require.Eventually(t, func() bool { var err error build, err = client.WorkspaceBuild(ctx, build.ID) + // job gets marked Failed when there is an Error; in practice we never get to Status = Canceled + // because provisioners report an Error when canceled. We check the Error string to ensure we don't mask + // other errors in this test. return assert.NoError(t, err) && - // The job will never actually cancel successfully because it will never send a - // provision complete response. - assert.Empty(t, build.Job.Error) && - build.Job.Status == codersdk.ProvisionerJobCanceling + build.Job.Error == "canceled" && + build.Job.Status == codersdk.ProvisionerJobFailed }, testutil.WaitShort, testutil.IntervalFast) }) t.Run("User is not allowed to cancel", func(t *testing.T) { @@ -415,12 +416,12 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { owner := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{}, }, }}, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) @@ -452,9 +453,9 @@ func TestWorkspaceBuildResources(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -494,16 +495,16 @@ func TestWorkspaceBuildLogs(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "example", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -548,10 +549,10 @@ func TestWorkspaceBuildState(t *testing.T) { wantState := []byte("some kinda state") version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ State: wantState, }, }, @@ -764,31 +765,31 @@ func TestWorkspaceBuildDebugMode(t *testing.T) { // Interact as template admin echoResponses := &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_DEBUG, Output: "want-it", }, }, }, { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_TRACE, Output: "dont-want-it", }, }, }, { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_DEBUG, Output: "done", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, } @@ -831,7 +832,10 @@ func TestWorkspaceBuildDebugMode(t *testing.T) { if !ok { break processingLogs } - + t.Logf("got log: %s -- %s | %s | %s", log.Level, log.Stage, log.Source, log.Output) + if log.Source != "provisioner" { + continue + } logsProcessed++ require.NotEqual(t, "dont-want-it", log.Output, "unexpected log message", "%s log message shouldn't be logged: %s") @@ -841,7 +845,6 @@ func TestWorkspaceBuildDebugMode(t *testing.T) { } } } - - require.Len(t, echoResponses.ProvisionApply, logsProcessed) + require.Equal(t, 2, logsProcessed) }) } diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index feadc62ddecb9..fdf1bd2335034 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -26,9 +26,9 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", @@ -71,9 +71,9 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", @@ -157,9 +157,9 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index ebedb8497deae..8da37158b1e3e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -174,9 +174,9 @@ func TestWorkspace(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -214,9 +214,9 @@ func TestWorkspace(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -258,9 +258,9 @@ func TestWorkspace(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -1248,7 +1248,7 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1276,7 +1276,7 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1316,10 +1316,10 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -1374,7 +1374,7 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1418,7 +1418,7 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1457,7 +1457,7 @@ func TestWorkspaceFilterManual(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -1575,7 +1575,7 @@ func TestPostWorkspaceBuild(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - ProvisionApply: []*proto.Provision_Response{{}}, + ProvisionApply: []*proto.Response{{}}, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) @@ -2138,10 +2138,10 @@ func TestWorkspaceWatcher(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -2231,10 +2231,10 @@ func TestWorkspaceWatcher(t *testing.T) { // Add a new version that will fail. badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Error: "test error", }, }, @@ -2299,9 +2299,9 @@ func TestWorkspaceResource(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "beta", Type: "example", @@ -2367,9 +2367,9 @@ func TestWorkspaceResource(t *testing.T) { } version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -2424,9 +2424,9 @@ func TestWorkspaceResource(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "some", Type: "example", @@ -2497,10 +2497,10 @@ func TestWorkspaceWithRichParameters(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: firstParameterName, @@ -2521,9 +2521,9 @@ func TestWorkspaceWithRichParameters(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, }) @@ -2590,10 +2590,10 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: firstParameterName, @@ -2612,9 +2612,9 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, }) @@ -2681,10 +2681,10 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{ + ProvisionPlan: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Parameters: []*proto.RichParameter{ { Name: firstParameterName, @@ -2706,9 +2706,9 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) { }, }, }, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }}, }) diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 82d853503ace4..e46756578d542 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -70,6 +70,11 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { return xerrors.Errorf("mkdir %q: %w", cacheDir, err) } + tempDir, err := os.MkdirTemp("", "provisionerd") + if err != nil { + return err + } + terraformClient, terraformServer := provisionersdk.MemTransportPipe() go func() { <-ctx.Done() @@ -84,10 +89,11 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { err := terraform.Serve(ctx, &terraform.ServeOptions{ ServeOptions: &provisionersdk.ServeOptions{ - Listener: terraformServer, + Listener: terraformServer, + Logger: logger.Named("terraform"), + WorkDirectory: tempDir, }, CachePath: cacheDir, - Logger: logger.Named("terraform"), }) if err != nil && !xerrors.Is(err, context.Canceled) { select { @@ -97,11 +103,6 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { } }() - tempDir, err := os.MkdirTemp("", "provisionerd") - if err != nil { - return err - } - logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags)) provisioners := provisionerd.Provisioners{ @@ -121,7 +122,6 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { JobPollJitter: pollJitter, UpdateInterval: 500 * time.Millisecond, Provisioners: provisioners, - WorkDirectory: tempDir, }) var exitErr error diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go index 2cf7259044e83..8ee2c071377d0 100644 --- a/enterprise/coderd/appearance_test.go +++ b/enterprise/coderd/appearance_test.go @@ -111,7 +111,7 @@ func TestServiceBanners(t *testing.T) { agentClient.SetSessionToken(authToken) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index d9b688fab8158..e190a3df90e20 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -139,9 +139,9 @@ func TestProvisionerDaemonServe(t *testing.T) { authToken := uuid.NewString() data, err := echo.Tar(&echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index 52e59858f5354..5b7ecbe1bcfd8 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -73,9 +73,9 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go index 3119168696a36..69c9a4bc3de88 100644 --- a/enterprise/coderd/workspacequota_test.go +++ b/enterprise/coderd/workspacequota_test.go @@ -89,9 +89,9 @@ func TestWorkspaceQuota(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -183,39 +183,15 @@ func TestWorkspaceQuota(t *testing.T) { require.NoError(t, err) verifyQuota(ctx, t, client, 0, 4) - stopResp := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - DailyCost: 1, - }}, - }, - }, - }} - - startResp := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - DailyCost: 2, - }}, - }, - }, - }} - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{ - proto.WorkspaceTransition_START: startResp, - proto.WorkspaceTransition_STOP: stopResp, + ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_START: planWithCost(2), + proto.WorkspaceTransition_STOP: planWithCost(1), }, - ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{ - proto.WorkspaceTransition_START: startResp, - proto.WorkspaceTransition_STOP: stopResp, + ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_START: applyWithCost(2), + proto.WorkspaceTransition_STOP: applyWithCost(1), }, }) @@ -258,3 +234,31 @@ func TestWorkspaceQuota(t *testing.T) { require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) }) } + +func planWithCost(cost int32) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + DailyCost: cost, + }}, + }, + }, + }} +} + +func applyWithCost(cost int32) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + DailyCost: cost, + }}, + }, + }, + }} +} diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 373b79c78d59f..db14ae96f1100 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -120,8 +120,8 @@ func TestWorkspaceAutobuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionFailed, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyFailed, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) @@ -166,8 +166,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionFailed, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyFailed, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds()) @@ -212,8 +212,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) // Create a template without setting a failure_ttl. template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) @@ -255,8 +255,8 @@ func TestWorkspaceAutobuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) @@ -311,8 +311,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) @@ -353,8 +353,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](autoDeleteTTL.Milliseconds()) @@ -395,8 +395,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) @@ -447,8 +447,8 @@ func TestWorkspaceAutobuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantMillis = ptr.Ref[int64](transitionTTL.Milliseconds()) @@ -516,8 +516,8 @@ func TestWorkspaceAutobuild(t *testing.T) { }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](dormantTTL.Milliseconds()) @@ -578,8 +578,8 @@ func TestWorkspaceAutobuild(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: echo.ProvisionComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ApplyComplete, }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index c057254704f3d..9e22157bc381b 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -5,25 +5,24 @@ import ( "bytes" "context" "fmt" + "os" "path/filepath" "strings" + "github.com/google/uuid" "golang.org/x/xerrors" protobuf "google.golang.org/protobuf/proto" - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" ) // ProvisionApplyWithAgent returns provision responses that will mock a fake // "aws_instance" resource with an agent that has the given auth token. -func ProvisionApplyWithAgent(authToken string) []*proto.Provision_Response { - return []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ +func ProvisionApplyWithAgent(authToken string) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -42,23 +41,36 @@ func ProvisionApplyWithAgent(authToken string) []*proto.Provision_Response { var ( // ParseComplete is a helper to indicate an empty parse completion. - ParseComplete = []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{}, + ParseComplete = []*proto.Response{{ + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{}, + }, + }} + // PlanComplete is a helper to indicate an empty provision completion. + PlanComplete = []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{}, }, }} - // ProvisionComplete is a helper to indicate an empty provision completion. - ProvisionComplete = []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, + // ApplyComplete is a helper to indicate an empty provision completion. + ApplyComplete = []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{}, }, }} - // ProvisionFailed is a helper to convey a failed provision - // operation. - ProvisionFailed = []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + // PlanFailed is a helper to convey a failed plan operation + PlanFailed = []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Error: "failed!", + }, + }, + }} + // ApplyFailed is a helper to convey a failed apply operation + ApplyFailed = []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Error: "failed!", }, }, @@ -66,120 +78,116 @@ var ( ) // Serve starts the echo provisioner. -func Serve(ctx context.Context, filesystem afero.Fs, options *provisionersdk.ServeOptions) error { - return provisionersdk.Serve(ctx, &echo{ - filesystem: filesystem, - }, options) +func Serve(ctx context.Context, options *provisionersdk.ServeOptions) error { + return provisionersdk.Serve(ctx, &echo{}, options) } // The echo provisioner serves as a dummy provisioner primarily // used for testing. It echos responses from JSON files in the // format %d.protobuf. It's used for testing. -type echo struct { - filesystem afero.Fs -} +type echo struct{} -// Parse reads requests from the provided directory to stream responses. -func (e *echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error { - for index := 0; ; index++ { - path := filepath.Join(request.Directory, fmt.Sprintf("%d.parse.protobuf", index)) - _, err := e.filesystem.Stat(path) - if err != nil { - if index == 0 { - // Error if nothing is around to enable failed states. - return xerrors.Errorf("no state: %w", err) - } - break - } - data, err := afero.ReadFile(e.filesystem, path) - if err != nil { - return xerrors.Errorf("read file %q: %w", path, err) - } - var response proto.Parse_Response - err = protobuf.Unmarshal(data, &response) - if err != nil { - return xerrors.Errorf("unmarshal: %w", err) - } - err = stream.Send(&response) - if err != nil { - return err - } - } - <-stream.Context().Done() - return stream.Context().Err() -} - -// Provision reads requests from the provided directory to stream responses. -func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error { - msg, err := stream.Recv() - if err != nil { - return err - } - - var config *proto.Provision_Config - switch { - case msg.GetPlan() != nil: - config = msg.GetPlan().GetConfig() - case msg.GetApply() != nil: - config = msg.GetApply().GetConfig() - default: - // Probably a cancel - return nil - } - -outer: +func readResponses(sess *provisionersdk.Session, trans string, suffix string) ([]*proto.Response, error) { + var responses []*proto.Response for i := 0; ; i++ { - var extension string - if msg.GetPlan() != nil { - extension = ".plan.protobuf" - } else { - extension = ".apply.protobuf" - } - var ( - path string - pathIndex int - ) - // Try more specific path first, then fallback to generic. paths := []string{ - filepath.Join(config.Directory, fmt.Sprintf("%d.%s.provision"+extension, i, strings.ToLower(config.GetMetadata().GetWorkspaceTransition().String()))), - filepath.Join(config.Directory, fmt.Sprintf("%d.provision"+extension, i)), + // Try more specific path first, then fallback to generic. + filepath.Join(sess.WorkDirectory, fmt.Sprintf("%d.%s.%s", i, trans, suffix)), + filepath.Join(sess.WorkDirectory, fmt.Sprintf("%d.%s", i, suffix)), } - for pathIndex, path = range paths { - _, err := e.filesystem.Stat(path) - if err != nil && pathIndex == len(paths)-1 { - // If there are zero messages, something is wrong. + for pathIndex, path := range paths { + _, err := os.Stat(path) + if err != nil && pathIndex == (len(paths)-1) { + // If there are zero messages, something is wrong if i == 0 { // Error if nothing is around to enable failed states. - return xerrors.New("no state") + return nil, xerrors.Errorf("no state: %w", err) } - // Otherwise, we're done with the entire provision. - break outer - } else if err != nil { + // Otherwise, we've read all responses + return responses, nil + } + if err != nil { + // try next path continue } + data, err := os.ReadFile(path) + if err != nil { + return nil, xerrors.Errorf("read file %q: %w", path, err) + } + response := new(proto.Response) + err = protobuf.Unmarshal(data, response) + if err != nil { + return nil, xerrors.Errorf("unmarshal: %w", err) + } + responses = append(responses, response) break } - data, err := afero.ReadFile(e.filesystem, path) - if err != nil { - return xerrors.Errorf("read file %q: %w", path, err) + } +} + +// Parse reads requests from the provided directory to stream responses. +func (*echo) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete { + responses, err := readResponses(sess, "unspecified", "parse.protobuf") + if err != nil { + return &proto.ParseComplete{Error: err.Error()} + } + for _, response := range responses { + if log := response.GetLog(); log != nil { + sess.ProvisionLog(log.Level, log.Output) } - var response proto.Provision_Response - err = protobuf.Unmarshal(data, &response) - if err != nil { - return xerrors.Errorf("unmarshal: %w", err) + if complete := response.GetParse(); complete != nil { + return complete + } + } + + // if we didn't get a complete from the filesystem, that's an error + return provisionersdk.ParseErrorf("complete response missing") +} + +// Plan reads requests from the provided directory to stream responses. +func (*echo) Plan(sess *provisionersdk.Session, req *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete { + responses, err := readResponses( + sess, + strings.ToLower(req.GetMetadata().GetWorkspaceTransition().String()), + "plan.protobuf") + if err != nil { + return &proto.PlanComplete{Error: err.Error()} + } + for _, response := range responses { + if log := response.GetLog(); log != nil { + sess.ProvisionLog(log.Level, log.Output) } - r, ok := filterLogResponses(config, &response) - if !ok { - continue + if complete := response.GetPlan(); complete != nil { + return complete } + } - err = stream.Send(r) - if err != nil { - return err + // some tests use Echo without a complete response to test cancel + <-canceledOrComplete + return provisionersdk.PlanErrorf("canceled") +} + +// Apply reads requests from the provided directory to stream responses. +func (*echo) Apply(sess *provisionersdk.Session, req *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete { + responses, err := readResponses( + sess, + strings.ToLower(req.GetMetadata().GetWorkspaceTransition().String()), + "apply.protobuf") + if err != nil { + return &proto.ApplyComplete{Error: err.Error()} + } + for _, response := range responses { + if log := response.GetLog(); log != nil { + sess.ProvisionLog(log.Level, log.Output) + } + if complete := response.GetApply(); complete != nil { + return complete } } - <-stream.Context().Done() - return stream.Context().Err() + + // some tests use Echo without a complete response to test cancel + <-canceledOrComplete + return provisionersdk.ApplyErrorf("canceled") } func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) { @@ -188,29 +196,42 @@ func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) { // Responses is a collection of mocked responses to Provision operations. type Responses struct { - Parse []*proto.Parse_Response + Parse []*proto.Response // ProvisionApply and ProvisionPlan are used to mock ALL responses of // Apply and Plan, regardless of transition. - ProvisionApply []*proto.Provision_Response - ProvisionPlan []*proto.Provision_Response + ProvisionApply []*proto.Response + ProvisionPlan []*proto.Response // ProvisionApplyMap and ProvisionPlanMap are used to mock specific // transition responses. They are prioritized over the generic responses. - ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Provision_Response - ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Provision_Response + ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response + ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response } // Tar returns a tar archive of responses to provisioner operations. func Tar(responses *Responses) ([]byte, error) { if responses == nil { responses = &Responses{ - ParseComplete, ProvisionComplete, ProvisionComplete, + ParseComplete, ApplyComplete, PlanComplete, nil, nil, } } if responses.ProvisionPlan == nil { - responses.ProvisionPlan = responses.ProvisionApply + for _, resp := range responses.ProvisionApply { + if resp.GetLog() != nil { + responses.ProvisionPlan = append(responses.ProvisionPlan, resp) + continue + } + responses.ProvisionPlan = append(responses.ProvisionPlan, &proto.Response{ + Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ + Error: resp.GetApply().GetError(), + Resources: resp.GetApply().GetResources(), + Parameters: resp.GetApply().GetParameters(), + GitAuthProviders: resp.GetApply().GetGitAuthProviders(), + }}, + }) + } } var buffer bytes.Buffer @@ -245,20 +266,20 @@ func Tar(responses *Responses) ([]byte, error) { } } for index, response := range responses.ProvisionApply { - err := writeProto(fmt.Sprintf("%d.provision.apply.protobuf", index), response) + err := writeProto(fmt.Sprintf("%d.apply.protobuf", index), response) if err != nil { return nil, err } } for index, response := range responses.ProvisionPlan { - err := writeProto(fmt.Sprintf("%d.provision.plan.protobuf", index), response) + err := writeProto(fmt.Sprintf("%d.plan.protobuf", index), response) if err != nil { return nil, err } } for trans, m := range responses.ProvisionApplyMap { for i, rs := range m { - err := writeProto(fmt.Sprintf("%d.%s.provision.apply.protobuf", i, strings.ToLower(trans.String())), rs) + err := writeProto(fmt.Sprintf("%d.%s.apply.protobuf", i, strings.ToLower(trans.String())), rs) if err != nil { return nil, err } @@ -266,7 +287,7 @@ func Tar(responses *Responses) ([]byte, error) { } for trans, m := range responses.ProvisionPlanMap { for i, rs := range m { - err := writeProto(fmt.Sprintf("%d.%s.provision.plan.protobuf", i, strings.ToLower(trans.String())), rs) + err := writeProto(fmt.Sprintf("%d.%s.plan.protobuf", i, strings.ToLower(trans.String())), rs) if err != nil { return nil, err } @@ -279,22 +300,14 @@ func Tar(responses *Responses) ([]byte, error) { return buffer.Bytes(), nil } -func filterLogResponses(config *proto.Provision_Config, response *proto.Provision_Response) (*proto.Provision_Response, bool) { - responseLog, ok := response.Type.(*proto.Provision_Response_Log) - if !ok { - // Pass all non-log responses - return response, true - } - - if config.ProvisionerLogLevel == "" { - // Don't change the default behavior of "echo" - return response, true - } - - provisionerLogLevel := proto.LogLevel_value[strings.ToUpper(config.ProvisionerLogLevel)] - if int32(responseLog.Log.Level) < provisionerLogLevel { - // Log level is not enabled - return nil, false +func WithResources(resources []*proto.Resource) *Responses { + return &Responses{ + Parse: ParseComplete, + ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{ + Resources: resources, + }}}}, + ProvisionPlan: []*proto.Response{{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ + Resources: resources, + }}}}, } - return response, true } diff --git a/provisioner/echo/serve_test.go b/provisioner/echo/serve_test.go index 01b283f8a55f5..6590f2ecafc54 100644 --- a/provisioner/echo/serve_test.go +++ b/provisioner/echo/serve_test.go @@ -1,27 +1,23 @@ package echo_test import ( - "archive/tar" - "bytes" "context" - "io" - "os" - "path/filepath" "testing" - "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" ) func TestEcho(t *testing.T) { t.Parallel() - fs := afero.NewMemMapFs() + workdir := t.TempDir() + // Create an in-memory provisioner to communicate with. client, server := provisionersdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) @@ -31,8 +27,9 @@ func TestEcho(t *testing.T) { cancelFunc() }) go func() { - err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{ - Listener: server, + err := echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: server, + WorkDirectory: workdir, }) assert.NoError(t, err) }() @@ -40,25 +37,39 @@ func TestEcho(t *testing.T) { t.Run("Parse", func(t *testing.T) { t.Parallel() + ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort) + defer cancel() - responses := []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Log{ - Log: &proto.Log{ - Output: "log-output", + responses := []*proto.Response{ + { + Type: &proto.Response_Log{ + Log: &proto.Log{ + Output: "log-output", + }, }, }, - }, { - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{}, + { + Type: &proto.Response_Parse{ + Parse: &proto.ParseComplete{}, + }, }, - }} + } data, err := echo.Tar(&echo.Responses{ Parse: responses, }) require.NoError(t, err) - client, err := api.Parse(ctx, &proto.Parse_Request{ - Directory: unpackTar(t, fs, data), - }) + client, err := api.Session(ctx) + require.NoError(t, err) + defer func() { + err := client.Close() + require.NoError(t, err) + }() + err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ + TemplateSourceArchive: data, + }}}) + require.NoError(t, err) + + err = client.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) require.NoError(t, err) log, err := client.Recv() require.NoError(t, err) @@ -70,95 +81,117 @@ func TestEcho(t *testing.T) { t.Run("Provision", func(t *testing.T) { t.Parallel() + ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort) + defer cancel() - responses := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ - Log: &proto.Log{ - Level: proto.LogLevel_INFO, - Output: "log-output", + planResponses := []*proto.Response{ + { + Type: &proto.Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "log-output", + }, }, }, - }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "resource", - }}, + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "resource", + }}, + }, }, }, - }} - data, err := echo.Tar(&echo.Responses{ - ProvisionApply: responses, - }) - require.NoError(t, err) - client, err := api.Provision(ctx) - require.NoError(t, err) - err = client.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: unpackTar(t, fs, data), + } + applyResponses := []*proto.Response{ + { + Type: &proto.Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "log-output", + }, + }, + }, + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: "resource", + }}, }, }, }, + } + data, err := echo.Tar(&echo.Responses{ + ProvisionPlan: planResponses, + ProvisionApply: applyResponses, }) require.NoError(t, err) + client, err := api.Session(ctx) + require.NoError(t, err) + defer func() { + err := client.Close() + require.NoError(t, err) + }() + err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ + TemplateSourceArchive: data, + }}}) + require.NoError(t, err) + + err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}}) + require.NoError(t, err) log, err := client.Recv() require.NoError(t, err) - require.Equal(t, responses[0].GetLog().Output, log.GetLog().Output) + require.Equal(t, planResponses[0].GetLog().Output, log.GetLog().Output) complete, err := client.Recv() require.NoError(t, err) - require.Equal(t, responses[1].GetComplete().Resources[0].Name, - complete.GetComplete().Resources[0].Name) + require.Equal(t, planResponses[1].GetPlan().Resources[0].Name, + complete.GetPlan().Resources[0].Name) + + err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}}) + require.NoError(t, err) + log, err = client.Recv() + require.NoError(t, err) + require.Equal(t, applyResponses[0].GetLog().Output, log.GetLog().Output) + complete, err = client.Recv() + require.NoError(t, err) + require.Equal(t, applyResponses[1].GetApply().Resources[0].Name, + complete.GetApply().Resources[0].Name) }) t.Run("ProvisionStop", func(t *testing.T) { t.Parallel() // Stop responses should be returned when the workspace is being stopped. - - defaultResponses := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "DEFAULT", - }}, - }, - }, - }} - stopResponses := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "STOP", - }}, - }, - }, - }} data, err := echo.Tar(&echo.Responses{ - ProvisionApply: defaultResponses, - ProvisionPlan: defaultResponses, - ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{ - proto.WorkspaceTransition_STOP: stopResponses, + ProvisionApply: applyCompleteResource("DEFAULT"), + ProvisionPlan: planCompleteResource("DEFAULT"), + ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_STOP: planCompleteResource("STOP"), }, - ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{ - proto.WorkspaceTransition_STOP: stopResponses, + ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{ + proto.WorkspaceTransition_STOP: applyCompleteResource("STOP"), }, }) require.NoError(t, err) - client, err := api.Provision(ctx) + client, err := api.Session(ctx) + require.NoError(t, err) + defer func() { + err := client.Close() + require.NoError(t, err) + }() + err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ + TemplateSourceArchive: data, + }}}) require.NoError(t, err) // Do stop. - err = client.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: unpackTar(t, fs, data), - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_STOP, - }, + err = client.Send(&proto.Request{ + Type: &proto.Request_Plan{ + Plan: &proto.PlanRequest{ + Metadata: &proto.Metadata{ + WorkspaceTransition: proto.WorkspaceTransition_STOP, }, }, }, @@ -168,22 +201,16 @@ func TestEcho(t *testing.T) { complete, err := client.Recv() require.NoError(t, err) require.Equal(t, - stopResponses[0].GetComplete().Resources[0].Name, - complete.GetComplete().Resources[0].Name, + "STOP", + complete.GetPlan().Resources[0].Name, ) // Do start. - client, err = api.Provision(ctx) - require.NoError(t, err) - - err = client.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: unpackTar(t, fs, data), - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_START, - }, + err = client.Send(&proto.Request{ + Type: &proto.Request_Plan{ + Plan: &proto.PlanRequest{ + Metadata: &proto.Metadata{ + WorkspaceTransition: proto.WorkspaceTransition_START, }, }, }, @@ -193,31 +220,33 @@ func TestEcho(t *testing.T) { complete, err = client.Recv() require.NoError(t, err) require.Equal(t, - defaultResponses[0].GetComplete().Resources[0].Name, - complete.GetComplete().Resources[0].Name, + "DEFAULT", + complete.GetPlan().Resources[0].Name, ) }) t.Run("ProvisionWithLogLevel", func(t *testing.T) { t.Parallel() + ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort) + defer cancel() - responses := []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Log{ + responses := []*proto.Response{{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_TRACE, Output: "log-output-trace", }, }, }, { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "log-output-info", }, }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "resource", }}, @@ -225,49 +254,62 @@ func TestEcho(t *testing.T) { }, }} data, err := echo.Tar(&echo.Responses{ + ProvisionPlan: echo.PlanComplete, ProvisionApply: responses, }) require.NoError(t, err) - client, err := api.Provision(ctx) + client, err := api.Session(ctx) require.NoError(t, err) - err = client.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: unpackTar(t, fs, data), - ProvisionerLogLevel: "debug", - }, - }, - }, - }) + defer func() { + err := client.Close() + require.NoError(t, err) + }() + err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{ + TemplateSourceArchive: data, + ProvisionerLogLevel: "debug", + }}}) + require.NoError(t, err) + + // Plan is required before apply + err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}}) + require.NoError(t, err) + complete, err := client.Recv() + require.NoError(t, err) + require.NotNil(t, complete.GetPlan()) + + err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}}) require.NoError(t, err) log, err := client.Recv() require.NoError(t, err) // Skip responses[0] as it's trace level require.Equal(t, responses[1].GetLog().Output, log.GetLog().Output) - complete, err := client.Recv() + complete, err = client.Recv() require.NoError(t, err) - require.Equal(t, responses[2].GetComplete().Resources[0].Name, - complete.GetComplete().Resources[0].Name) + require.Equal(t, responses[2].GetApply().Resources[0].Name, + complete.GetApply().Resources[0].Name) }) } -func unpackTar(t *testing.T, fs afero.Fs, data []byte) string { - directory := t.TempDir() - reader := tar.NewReader(bytes.NewReader(data)) - for { - header, err := reader.Next() - if err != nil { - break - } - // #nosec - path := filepath.Join(directory, header.Name) - file, err := fs.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) - require.NoError(t, err) - _, err = io.CopyN(file, reader, 1<<20) - require.ErrorIs(t, err, io.EOF) - err = file.Close() - require.NoError(t, err) - } - return directory +func planCompleteResource(name string) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: name, + }}, + }, + }, + }} +} + +func applyCompleteResource(name string) []*proto.Response { + return []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: name, + }}, + }, + }, + }} } diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index cd847910e4c0f..d523eeca198e5 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -25,6 +25,7 @@ import ( ) type executor struct { + logger slog.Logger server *server mut *sync.Mutex binaryPath string @@ -50,8 +51,10 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str ctx, span := e.server.startTrace(ctx, fmt.Sprintf("exec - terraform %s", args[0])) defer span.End() span.SetAttributes(attribute.StringSlice("args", args)) + e.logger.Debug(ctx, "starting command", slog.F("args", args)) defer func() { + e.logger.Debug(ctx, "closing writers", slog.Error(err)) closeErr := stdOutWriter.Close() if err == nil && closeErr != nil { err = closeErr @@ -62,6 +65,7 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str } }() if ctx.Err() != nil { + e.logger.Debug(ctx, "context canceled before command started", slog.F("args", args)) return ctx.Err() } @@ -90,11 +94,14 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str ) err = cmd.Start() if err != nil { + e.logger.Debug(ctx, "failed to start command", slog.F("args", args)) return err } - interruptCommandOnCancel(ctx, killCtx, cmd) + interruptCommandOnCancel(ctx, killCtx, e.logger, cmd) - return cmd.Wait() + err = cmd.Wait() + e.logger.Debug(ctx, "command done", slog.F("args", args), slog.Error(err)) + return err } // execParseJSON must only be called while the lock is held. @@ -120,7 +127,7 @@ func (e *executor) execParseJSON(ctx, killCtx context.Context, args, env []strin if err != nil { return err } - interruptCommandOnCancel(ctx, killCtx, cmd) + interruptCommandOnCancel(ctx, killCtx, e.logger, cmd) err = cmd.Wait() if err != nil { @@ -207,15 +214,23 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error { return e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), outWriter, errWriter) } +func getPlanFilePath(workdir string) string { + return filepath.Join(workdir, "terraform.tfplan") +} + +func getStateFilePath(workdir string) string { + return filepath.Join(workdir, "terraform.tfstate") +} + // revive:disable-next-line:flag-parameter -func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool) (*proto.Provision_Response, error) { +func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool) (*proto.PlanComplete, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) defer span.End() e.mut.Lock() defer e.mut.Unlock() - planfilePath := filepath.Join(e.workdir, "terraform.tfplan") + planfilePath := getPlanFilePath(e.workdir) args := []string{ "plan", "-no-color", @@ -248,19 +263,10 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l if err != nil { return nil, err } - planFileByt, err := os.ReadFile(planfilePath) - if err != nil { - return nil, err - } - return &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: state.Parameters, - Resources: state.Resources, - GitAuthProviders: state.GitAuthProviders, - Plan: planFileByt, - }, - }, + return &proto.PlanComplete{ + Parameters: state.Parameters, + Resources: state.Resources, + GitAuthProviders: state.GitAuthProviders, }, nil } @@ -346,7 +352,7 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) { if err != nil { return "", err } - interruptCommandOnCancel(ctx, killCtx, cmd) + interruptCommandOnCancel(ctx, killCtx, e.logger, cmd) err = cmd.Wait() if err != nil { @@ -357,33 +363,22 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) { func (e *executor) apply( ctx, killCtx context.Context, - plan []byte, env []string, logr logSink, -) (*proto.Provision_Response, error) { +) (*proto.ApplyComplete, error) { ctx, span := e.server.startTrace(ctx, tracing.FuncName()) defer span.End() e.mut.Lock() defer e.mut.Unlock() - planFile, err := os.CreateTemp("", "coder-terrafrom-plan") - if err != nil { - return nil, xerrors.Errorf("create plan file: %w", err) - } - _, err = planFile.Write(plan) - if err != nil { - return nil, xerrors.Errorf("write plan file: %w", err) - } - defer os.Remove(planFile.Name()) - args := []string{ "apply", "-no-color", "-auto-approve", "-input=false", "-json", - planFile.Name(), + getPlanFilePath(e.workdir), } outWriter, doneOut := provisionLogWriter(logr) @@ -395,7 +390,7 @@ func (e *executor) apply( <-doneErr }() - err = e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter) + err := e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter) if err != nil { return nil, xerrors.Errorf("terraform apply: %w", err) } @@ -408,15 +403,11 @@ func (e *executor) apply( if err != nil { return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err) } - return &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: state.Parameters, - Resources: state.Resources, - GitAuthProviders: state.GitAuthProviders, - State: stateContent, - }, - }, + return &proto.ApplyComplete{ + Parameters: state.Parameters, + Resources: state.Resources, + GitAuthProviders: state.GitAuthProviders, + State: stateContent, }, nil } @@ -461,48 +452,28 @@ func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) { return state, nil } -func interruptCommandOnCancel(ctx, killCtx context.Context, cmd *exec.Cmd) { +func interruptCommandOnCancel(ctx, killCtx context.Context, logger slog.Logger, cmd *exec.Cmd) { go func() { select { case <-ctx.Done(): + var err error switch runtime.GOOS { case "windows": // Interrupts aren't supported by Windows. - _ = cmd.Process.Kill() + err = cmd.Process.Kill() default: - _ = cmd.Process.Signal(os.Interrupt) + err = cmd.Process.Signal(os.Interrupt) } + logger.Debug(ctx, "interrupted command", slog.F("args", cmd.Args), slog.Error(err)) case <-killCtx.Done(): + logger.Debug(ctx, "kill context ended", slog.F("args", cmd.Args)) } }() } type logSink interface { - Log(*proto.Log) -} - -type streamLogSink struct { - // Any errors writing to the stream will be logged to logger. - logger slog.Logger - stream proto.DRPCProvisioner_ProvisionStream -} - -var _ logSink = streamLogSink{} - -func (s streamLogSink) Log(l *proto.Log) { - err := s.stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Log{ - Log: l, - }, - }) - if err != nil { - s.logger.Warn(context.Background(), "write log to stream", - slog.F("level", l.Level.String()), - slog.F("message", l.Output), - slog.Error(err), - ) - } + ProvisionLog(l proto.LogLevel, o string) } // logWriter creates a WriteCloser that will log each line of text at the given level. The WriteCloser must be closed @@ -526,7 +497,7 @@ func readAndLog(sink logSink, r io.Reader, done chan<- any, level proto.LogLevel continue } - sink.Log(&proto.Log{Level: level, Output: scanner.Text()}) + sink.ProvisionLog(level, scanner.Text()) continue } @@ -543,7 +514,7 @@ func readAndLog(sink logSink, r io.Reader, done chan<- any, level proto.LogLevel if logLevel == proto.LogLevel_INFO { logLevel = proto.LogLevel_DEBUG } - sink.Log(&proto.Log{Level: logLevel, Output: log.Message}) + sink.ProvisionLog(logLevel, log.Message) } } @@ -588,7 +559,7 @@ func provisionReadAndLog(sink logSink, r io.Reader, done chan<- any) { } logLevel := convertTerraformLogLevel(log.Level, sink) - sink.Log(&proto.Log{Level: logLevel, Output: log.Message}) + sink.ProvisionLog(logLevel, log.Message) // If the diagnostic is provided, let's provide a bit more info! if log.Diagnostic == nil { @@ -596,7 +567,7 @@ func provisionReadAndLog(sink logSink, r io.Reader, done chan<- any) { } logLevel = convertTerraformLogLevel(string(log.Diagnostic.Severity), sink) for _, diagLine := range strings.Split(FormatDiagnostic(log.Diagnostic), "\n") { - sink.Log(&proto.Log{Level: logLevel, Output: diagLine}) + sink.ProvisionLog(logLevel, diagLine) } } } @@ -614,10 +585,7 @@ func convertTerraformLogLevel(logLevel string, sink logSink) proto.LogLevel { case "error": return proto.LogLevel_ERROR default: - sink.Log(&proto.Log{ - Level: proto.LogLevel_WARN, - Output: fmt.Sprintf("unable to convert log level %s", logLevel), - }) + sink.ProvisionLog(proto.LogLevel_WARN, fmt.Sprintf("unable to convert log level %s", logLevel)) return proto.LogLevel_INFO } } diff --git a/provisioner/terraform/executor_internal_test.go b/provisioner/terraform/executor_internal_test.go index fd203c9b1ee5f..97cb5285372f2 100644 --- a/provisioner/terraform/executor_internal_test.go +++ b/provisioner/terraform/executor_internal_test.go @@ -16,8 +16,8 @@ type mockLogger struct { var _ logSink = &mockLogger{} -func (m *mockLogger) Log(l *proto.Log) { - m.logs = append(m.logs, l) +func (m *mockLogger) ProvisionLog(l proto.LogLevel, o string) { + m.logs = append(m.logs, &proto.Log{Level: l, Output: o}) } func TestLogWriter_Mainline(t *testing.T) { diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go index 6c52dc1313e1e..10ab7b801b071 100644 --- a/provisioner/terraform/parse.go +++ b/provisioner/terraform/parse.go @@ -12,18 +12,20 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/tracing" + "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" ) // Parse extracts Terraform variables from source-code. -func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error { - _, span := s.startTrace(stream.Context(), tracing.FuncName()) +func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete { + ctx := sess.Context() + _, span := s.startTrace(ctx, tracing.FuncName()) defer span.End() // Load the module and print any parse errors. - module, diags := tfconfig.LoadModule(request.Directory) + module, diags := tfconfig.LoadModule(sess.WorkDirectory) if diags.HasErrors() { - return xerrors.Errorf("load module: %s", formatDiagnostics(request.Directory, diags)) + return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags)) } // Sort variables by (filename, line) to make the ordering consistent @@ -40,17 +42,13 @@ func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisione for _, v := range variables { mv, err := convertTerraformVariable(v) if err != nil { - return xerrors.Errorf("can't convert the Terraform variable to a managed one: %w", err) + return provisionersdk.ParseErrorf("can't convert the Terraform variable to a managed one: %s", err) } templateVariables = append(templateVariables, mv) } - return stream.Send(&proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: templateVariables, - }, - }, - }) + return &proto.ParseComplete{ + TemplateVariables: templateVariables, + } } // Converts a Terraform variable to a template-wide variable, processed by Coder. diff --git a/provisioner/terraform/parse_test.go b/provisioner/terraform/parse_test.go index aa0e19984b224..c28532af25831 100644 --- a/provisioner/terraform/parse_test.go +++ b/provisioner/terraform/parse_test.go @@ -4,8 +4,6 @@ package terraform_test import ( "encoding/json" - "os" - "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -21,9 +19,8 @@ func TestParse(t *testing.T) { testCases := []struct { Name string Files map[string]string - Response *proto.Parse_Response - // If ErrorContains is not empty, then response.Recv() should return an - // error containing this string before a Complete response is returned. + Response *proto.ParseComplete + // If ErrorContains is not empty, then the ParseComplete should have an Error containing the given string ErrorContains string }{ { @@ -33,16 +30,12 @@ func TestParse(t *testing.T) { description = "Testing!" }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Description: "Testing!", - Required: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Description: "Testing!", + Required: true, }, }, }, @@ -54,15 +47,11 @@ func TestParse(t *testing.T) { default = "wow" }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - DefaultValue: "wow", - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + DefaultValue: "wow", }, }, }, @@ -76,15 +65,11 @@ func TestParse(t *testing.T) { } }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Required: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Required: true, }, }, }, @@ -104,27 +89,23 @@ func TestParse(t *testing.T) { "main2.tf": `variable "baz" { } variable "quux" { }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "foo", - Required: true, - }, - { - Name: "bar", - Required: true, - }, - { - Name: "baz", - Required: true, - }, - { - Name: "quux", - Required: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "foo", + Required: true, + }, + { + Name: "bar", + Required: true, + }, + { + Name: "baz", + Required: true, + }, + { + Name: "quux", + Required: true, }, }, }, @@ -139,19 +120,15 @@ func TestParse(t *testing.T) { sensitive = true }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Description: "Testing!", - Type: "bool", - DefaultValue: "true", - Required: false, - Sensitive: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Description: "Testing!", + Type: "bool", + DefaultValue: "true", + Required: false, + Sensitive: true, }, }, }, @@ -166,19 +143,15 @@ func TestParse(t *testing.T) { sensitive = true }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Description: "Testing!", - Type: "string", - DefaultValue: "abc", - Required: false, - Sensitive: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Description: "Testing!", + Type: "string", + DefaultValue: "abc", + Required: false, + Sensitive: true, }, }, }, @@ -193,19 +166,15 @@ func TestParse(t *testing.T) { sensitive = true }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Description: "Testing!", - Type: "string", - DefaultValue: "", - Required: false, - Sensitive: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Description: "Testing!", + Type: "string", + DefaultValue: "", + Required: false, + Sensitive: true, }, }, }, @@ -219,19 +188,15 @@ func TestParse(t *testing.T) { sensitive = true }`, }, - Response: &proto.Parse_Response{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - TemplateVariables: []*proto.TemplateVariable{ - { - Name: "A", - Description: "Testing!", - Type: "string", - DefaultValue: "", - Required: true, - Sensitive: true, - }, - }, + Response: &proto.ParseComplete{ + TemplateVariables: []*proto.TemplateVariable{ + { + Name: "A", + Description: "Testing!", + Type: "string", + DefaultValue: "", + Required: true, + Sensitive: true, }, }, }, @@ -243,40 +208,31 @@ func TestParse(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { t.Parallel() - // Write all files to the temporary test directory. - directory := t.TempDir() - for path, content := range testCase.Files { - err := os.WriteFile(filepath.Join(directory, path), []byte(content), 0o600) - require.NoError(t, err) - } - - response, err := api.Parse(ctx, &proto.Parse_Request{ - Directory: directory, + session := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, testCase.Files), }) + + err := session.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) require.NoError(t, err) for { - msg, err := response.Recv() - if err != nil { - if testCase.ErrorContains != "" { - require.ErrorContains(t, err, testCase.ErrorContains) - break - } + msg, err := session.Recv() + require.NoError(t, err) - require.NoError(t, err) + if testCase.ErrorContains != "" { + require.Contains(t, msg.GetParse().GetError(), testCase.ErrorContains) + break } - if msg.GetComplete() == nil { + // Ignore logs in this test + if msg.GetLog() != nil { continue } - if testCase.ErrorContains != "" { - t.Fatal("expected error but job completed successfully") - } // Ensure the want and got are equivalent! want, err := json.Marshal(testCase.Response) require.NoError(t, err) - got, err := json.Marshal(msg) + got, err := json.Marshal(msg.GetParse()) require.NoError(t, err) require.Equal(t, string(want), string(got)) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 504984520d3f2..ab832e4408683 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -4,11 +4,10 @@ import ( "context" "fmt" "os" - "path/filepath" "strings" "time" - "golang.org/x/xerrors" + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/provisionersdk" @@ -16,48 +15,23 @@ import ( "github.com/coder/terraform-provider-coder/provider" ) -// Provision executes `terraform apply` or `terraform plan` for dry runs. -func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error { - ctx, span := s.startTrace(stream.Context(), tracing.FuncName()) - defer span.End() - - request, err := stream.Recv() - if err != nil { - return err - } - if request.GetCancel() != nil { - return nil - } - - var ( - applyRequest = request.GetApply() - planRequest = request.GetPlan() - ) - - var config *proto.Provision_Config - if applyRequest == nil && planRequest == nil { - return nil - } else if applyRequest != nil { - config = applyRequest.Config - } else if planRequest != nil { - config = planRequest.Config - } - - // Create a context for graceful cancellation bound to the stream +func (s *server) setupContexts(parent context.Context, canceledOrComplete <-chan struct{}) ( + ctx context.Context, cancel func(), killCtx context.Context, kill func(), +) { + // Create a context for graceful cancellation bound to the session // context. This ensures that we will perform graceful cancellation // even on connection loss. - ctx, cancel := context.WithCancel(ctx) - defer cancel() + ctx, cancel = context.WithCancel(parent) // Create a separate context for forceful cancellation not tied to // the stream so that we can control when to terminate the process. - killCtx, kill := context.WithCancel(context.Background()) - defer kill() + killCtx, kill = context.WithCancel(context.Background()) // Ensure processes are eventually cleaned up on graceful // cancellation or disconnect. go func() { <-ctx.Done() + s.logger.Debug(ctx, "graceful context done") // TODO(mafredri): We should track this provision request as // part of graceful server shutdown procedure. Waiting on a @@ -66,134 +40,131 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error { defer t.Stop() select { case <-t.C: + s.logger.Debug(ctx, "exit timeout hit") kill() case <-killCtx.Done(): + s.logger.Debug(ctx, "kill context done") } }() + // Process cancel go func() { - for { - request, err := stream.Recv() - if err != nil { - return - } - if request.GetCancel() == nil { - // We only process cancellation requests here. - continue - } - cancel() - return - } + <-canceledOrComplete + s.logger.Debug(ctx, "canceledOrComplete closed") + cancel() }() + return ctx, cancel, killCtx, kill +} - sink := streamLogSink{ - logger: s.logger.Named("execution_logs"), - stream: stream, - } - - e := s.executor(config.Directory) - if err = e.checkMinVersion(ctx); err != nil { - return err - } - logTerraformEnvVars(sink) +func (s *server) Plan( + sess *provisionersdk.Session, request *proto.PlanRequest, canceledOrComplete <-chan struct{}, +) *proto.PlanComplete { + ctx, span := s.startTrace(sess.Context(), tracing.FuncName()) + defer span.End() + ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete) + defer cancel() + defer kill() - statefilePath := filepath.Join(config.Directory, "terraform.tfstate") - if len(config.State) > 0 { - err = os.WriteFile(statefilePath, config.State, 0o600) - if err != nil { - return xerrors.Errorf("write statefile %q: %w", statefilePath, err) - } + e := s.executor(sess.WorkDirectory) + if err := e.checkMinVersion(ctx); err != nil { + return provisionersdk.PlanErrorf(err.Error()) } + logTerraformEnvVars(sess) // If we're destroying, exit early if there's no state. This is necessary to // avoid any cases where a workspace is "locked out" of terraform due to // e.g. bad template param values and cannot be deleted. This is just for // contingency, in the future we will try harder to prevent workspaces being // broken this hard. - if config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY && len(config.State) == 0 { - _ = stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Log{ - Log: &proto.Log{ - Level: proto.LogLevel_INFO, - Output: "The terraform state does not exist, there is nothing to do", - }, - }, - }) + if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 { + sess.ProvisionLog(proto.LogLevel_INFO, "The terraform state does not exist, there is nothing to do") + return &proto.PlanComplete{} + } - return stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }) + statefilePath := getStateFilePath(sess.WorkDirectory) + if len(sess.Config.State) > 0 { + err := os.WriteFile(statefilePath, sess.Config.State, 0o600) + if err != nil { + return provisionersdk.PlanErrorf("write statefile %q: %s", statefilePath, err) + } } s.logger.Debug(ctx, "running initialization") - err = e.init(ctx, killCtx, sink) + err := e.init(ctx, killCtx, sess) if err != nil { - if ctx.Err() != nil { - return stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Error: err.Error(), - }, - }, - }) - } - return xerrors.Errorf("initialize terraform: %w", err) + s.logger.Debug(ctx, "init failed", slog.Error(err)) + return provisionersdk.PlanErrorf("initialize terraform: %s", err) } s.logger.Debug(ctx, "ran initialization") - env, err := provisionEnv(config, request.GetPlan().GetRichParameterValues(), request.GetPlan().GetGitAuthProviders()) + + env, err := provisionEnv(sess.Config, request.Metadata, request.RichParameterValues, request.GitAuthProviders) if err != nil { - return err + return provisionersdk.PlanErrorf("setup env: %s", err) } - var resp *proto.Provision_Response - if planRequest != nil { - vars, err := planVars(planRequest) - if err != nil { - return err - } + vars, err := planVars(request) + if err != nil { + return provisionersdk.PlanErrorf("plan vars: %s", err) + } - resp, err = e.plan( - ctx, killCtx, env, vars, sink, - config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY, - ) - if err != nil { - if ctx.Err() != nil { - return stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Error: err.Error(), - }, - }, - }) - } - return xerrors.Errorf("plan terraform: %w", err) - } - return stream.Send(resp) + resp, err := e.plan( + ctx, killCtx, env, vars, sess, + request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY, + ) + if err != nil { + return provisionersdk.PlanErrorf(err.Error()) } - // Must be apply - resp, err = e.apply( - ctx, killCtx, applyRequest.Plan, env, sink, + return resp +} + +func (s *server) Apply( + sess *provisionersdk.Session, request *proto.ApplyRequest, canceledOrComplete <-chan struct{}, +) *proto.ApplyComplete { + ctx, span := s.startTrace(sess.Context(), tracing.FuncName()) + defer span.End() + ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete) + defer cancel() + defer kill() + + e := s.executor(sess.WorkDirectory) + if err := e.checkMinVersion(ctx); err != nil { + return provisionersdk.ApplyErrorf(err.Error()) + } + logTerraformEnvVars(sess) + + // Exit early if there is no plan file. This is necessary to + // avoid any cases where a workspace is "locked out" of terraform due to + // e.g. bad template param values and cannot be deleted. This is just for + // contingency, in the future we will try harder to prevent workspaces being + // broken this hard. + if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 { + sess.ProvisionLog(proto.LogLevel_INFO, "The terraform plan does not exist, there is nothing to do") + return &proto.ApplyComplete{} + } + + // Earlier in the session, Plan() will have written the state file and the plan file. + statefilePath := getStateFilePath(sess.WorkDirectory) + env, err := provisionEnv(sess.Config, request.Metadata, nil, nil) + if err != nil { + return provisionersdk.ApplyErrorf("provision env: %s", err) + } + resp, err := e.apply( + ctx, killCtx, env, sess, ) if err != nil { errorMessage := err.Error() // Terraform can fail and apply and still need to store it's state. // In this case, we return Complete with an explicit error message. stateData, _ := os.ReadFile(statefilePath) - return stream.Send(&proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - State: stateData, - Error: errorMessage, - }, - }, - }) + return &proto.ApplyComplete{ + State: stateData, + Error: errorMessage, + } } - return stream.Send(resp) + return resp } -func planVars(plan *proto.Provision_Plan) ([]string, error) { +func planVars(plan *proto.PlanRequest) ([]string, error) { vars := []string{} for _, variable := range plan.VariableValues { vars = append(vars, fmt.Sprintf("%s=%s", variable.Name, variable.Value)) @@ -201,18 +172,21 @@ func planVars(plan *proto.Provision_Plan) ([]string, error) { return vars, nil } -func provisionEnv(config *proto.Provision_Config, richParams []*proto.RichParameterValue, gitAuth []*proto.GitAuthProvider) ([]string, error) { +func provisionEnv( + config *proto.Config, metadata *proto.Metadata, + richParams []*proto.RichParameterValue, gitAuth []*proto.GitAuthProvider, +) ([]string, error) { env := safeEnviron() env = append(env, - "CODER_AGENT_URL="+config.Metadata.CoderUrl, - "CODER_WORKSPACE_TRANSITION="+strings.ToLower(config.Metadata.WorkspaceTransition.String()), - "CODER_WORKSPACE_NAME="+config.Metadata.WorkspaceName, - "CODER_WORKSPACE_OWNER="+config.Metadata.WorkspaceOwner, - "CODER_WORKSPACE_OWNER_EMAIL="+config.Metadata.WorkspaceOwnerEmail, - "CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN="+config.Metadata.WorkspaceOwnerOidcAccessToken, - "CODER_WORKSPACE_ID="+config.Metadata.WorkspaceId, - "CODER_WORKSPACE_OWNER_ID="+config.Metadata.WorkspaceOwnerId, - "CODER_WORKSPACE_OWNER_SESSION_TOKEN="+config.Metadata.WorkspaceOwnerSessionToken, + "CODER_AGENT_URL="+metadata.GetCoderUrl(), + "CODER_WORKSPACE_TRANSITION="+strings.ToLower(metadata.GetWorkspaceTransition().String()), + "CODER_WORKSPACE_NAME="+metadata.GetWorkspaceName(), + "CODER_WORKSPACE_OWNER="+metadata.GetWorkspaceOwner(), + "CODER_WORKSPACE_OWNER_EMAIL="+metadata.GetWorkspaceOwnerEmail(), + "CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN="+metadata.GetWorkspaceOwnerOidcAccessToken(), + "CODER_WORKSPACE_ID="+metadata.GetWorkspaceId(), + "CODER_WORKSPACE_OWNER_ID="+metadata.GetWorkspaceOwnerId(), + "CODER_WORKSPACE_OWNER_SESSION_TOKEN="+metadata.GetWorkspaceOwnerSessionToken(), ) for key, value := range provisionersdk.AgentScriptEnv() { env = append(env, key+"="+value) @@ -258,10 +232,10 @@ func logTerraformEnvVars(sink logSink) { if !tfEnvSafeToPrint[parts[0]] { parts[1] = "" } - sink.Log(&proto.Log{ - Level: proto.LogLevel_WARN, - Output: fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]), - }) + sink.ProvisionLog( + proto.LogLevel_WARN, + fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]), + ) } } } diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 03fc70ed696c5..254ddec45b5e6 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -3,6 +3,8 @@ package terraform_test import ( + "archive/tar" + "bytes" "context" "encoding/json" "errors" @@ -35,6 +37,7 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont opts = &provisionerServeOptions{} } cachePath := t.TempDir() + workDir := t.TempDir() client, server := provisionersdk.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) serverErr := make(chan error, 1) @@ -50,40 +53,75 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont go func() { serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{ ServeOptions: &provisionersdk.ServeOptions{ - Listener: server, + Listener: server, + Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), + WorkDirectory: workDir, }, BinaryPath: opts.binaryPath, CachePath: cachePath, - Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), ExitTimeout: opts.exitTimeout, }) }() api := proto.NewDRPCProvisionerClient(client) + return ctx, api } -func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_ProvisionClient) ( - string, - *proto.Provision_Complete, -) { - var ( - logBuf strings.Builder - c *proto.Provision_Complete - ) +func makeTar(t *testing.T, files map[string]string) []byte { + t.Helper() + var buffer bytes.Buffer + writer := tar.NewWriter(&buffer) + for name, content := range files { + err := writer.WriteHeader(&tar.Header{ + Name: name, + Size: int64(len(content)), + Mode: 0o644, + }) + require.NoError(t, err) + _, err = writer.Write([]byte(content)) + require.NoError(t, err) + } + err := writer.Flush() + require.NoError(t, err) + return buffer.Bytes() +} + +func configure(ctx context.Context, t *testing.T, client proto.DRPCProvisionerClient, config *proto.Config) proto.DRPCProvisioner_SessionClient { + t.Helper() + sess, err := client.Session(ctx) + require.NoError(t, err) + err = sess.Send(&proto.Request{Type: &proto.Request_Config{Config: config}}) + require.NoError(t, err) + return sess +} + +func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_SessionClient) string { + var logBuf strings.Builder for { msg, err := response.Recv() require.NoError(t, err) if log := msg.GetLog(); log != nil { t.Log(log.Level.String(), log.Output) - _, _ = logBuf.WriteString(log.Output) - } - if c = msg.GetComplete(); c != nil { - require.Empty(t, c.Error) - break + _, err = logBuf.WriteString(log.Output) + require.NoError(t, err) + continue } + break } - return logBuf.String(), c + return logBuf.String() +} + +func sendPlan(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error { + return sess.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{ + Metadata: &proto.Metadata{WorkspaceTransition: transition}, + }}}) +} + +func sendApply(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error { + return sess.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{ + Metadata: &proto.Metadata{WorkspaceTransition: transition}, + }}}) } func TestProvision_Cancel(t *testing.T) { @@ -109,9 +147,10 @@ func TestProvision_Cancel(t *testing.T) { wantLog: []string{"interrupt", "exit"}, }, { - name: "Cancel apply", - mode: "apply", - startSequence: []string{"init", "apply_start"}, + // Provisioner requires a plan before an apply, so test cancel with plan. + name: "Cancel plan", + mode: "plan", + startSequence: []string{"init", "plan_start"}, wantLog: []string{"interrupt", "exit"}, }, } @@ -131,24 +170,16 @@ func TestProvision_Cancel(t *testing.T) { ctx, api := setupProvisioner(t, &provisionerServeOptions{ binaryPath: binPath, }) - - response, err := api.Provision(ctx) - require.NoError(t, err) - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Apply{ - Apply: &proto.Provision_Apply{ - Config: &proto.Provision_Config{ - Directory: dir, - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, + sess := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, nil), }) + + err = sendPlan(sess, proto.WorkspaceTransition_START) require.NoError(t, err) for _, line := range tt.startSequence { LoopStart: - msg, err := response.Recv() + msg, err := sess.Recv() require.NoError(t, err) t.Log(msg.Type) @@ -160,22 +191,22 @@ func TestProvision_Cancel(t *testing.T) { require.Equal(t, line, log.Output) } - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Cancel{ - Cancel: &proto.Provision_Cancel{}, + err = sess.Send(&proto.Request{ + Type: &proto.Request_Cancel{ + Cancel: &proto.CancelRequest{}, }, }) require.NoError(t, err) var gotLog []string for { - msg, err := response.Recv() + msg, err := sess.Recv() require.NoError(t, err) if log := msg.GetLog(); log != nil { gotLog = append(gotLog, log.Output) } - if c := msg.GetComplete(); c != nil { + if c := msg.GetPlan(); c != nil { require.Contains(t, c.Error, "exit status 1") break } @@ -208,23 +239,17 @@ func TestProvision_CancelTimeout(t *testing.T) { exitTimeout: time.Second, }) - response, err := api.Provision(ctx) - require.NoError(t, err) - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Apply{ - Apply: &proto.Provision_Apply{ - Config: &proto.Provision_Config{ - Directory: dir, - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, + sess := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, nil), }) + + // provisioner requires plan before apply, so test cancel with plan. + err = sendPlan(sess, proto.WorkspaceTransition_START) require.NoError(t, err) - for _, line := range []string{"init", "apply_start"} { + for _, line := range []string{"init", "plan_start"} { LoopStart: - msg, err := response.Recv() + msg, err := sess.Recv() require.NoError(t, err) t.Log(msg.Type) @@ -236,18 +261,14 @@ func TestProvision_CancelTimeout(t *testing.T) { require.Equal(t, line, log.Output) } - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Cancel{ - Cancel: &proto.Provision_Cancel{}, - }, - }) + err = sess.Send(&proto.Request{Type: &proto.Request_Cancel{Cancel: &proto.CancelRequest{}}}) require.NoError(t, err) for { - msg, err := response.Recv() + msg, err := sess.Recv() require.NoError(t, err) - if c := msg.GetComplete(); c != nil { + if c := msg.GetPlan(); c != nil { require.Contains(t, c.Error, "killed") break } @@ -258,17 +279,18 @@ func TestProvision(t *testing.T) { t.Parallel() testCases := []struct { - Name string - Files map[string]string - Request *proto.Provision_Plan + Name string + Files map[string]string + Metadata *proto.Metadata + Request *proto.PlanRequest // Response may be nil to not check the response. - Response *proto.Provision_Response - // If ErrorContains is not empty, then response.Recv() should return an - // error containing this string before a Complete response is returned. + Response *proto.PlanComplete + // If ErrorContains is not empty, PlanComplete should have an Error containing the given string ErrorContains string // If ExpectLogContains is not empty, then the logs should contain it. ExpectLogContains string - Apply bool + // If Apply is true, then send an Apply request and check we get the same Resources as in Response. + Apply bool }{ { Name: "missing-variable", @@ -293,15 +315,11 @@ func TestProvision(t *testing.T) { Files: map[string]string{ "main.tf": `resource "null_resource" "A" {}`, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - }}, - }, - }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + }}, }, }, { @@ -309,15 +327,11 @@ func TestProvision(t *testing.T) { Files: map[string]string{ "main.tf": `resource "null_resource" "A" {}`, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - }}, - }, - }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + }}, }, Apply: true, }, @@ -334,15 +348,11 @@ func TestProvision(t *testing.T) { } }`, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - }}, - }, - }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + }}, }, Apply: true, }, @@ -367,12 +377,8 @@ func TestProvision(t *testing.T) { Files: map[string]string{ "main.tf": `resource "null_resource" "A" {}`, }, - Request: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_DESTROY, - }, - }, + Metadata: &proto.Metadata{ + WorkspaceTransition: proto.WorkspaceTransition_DESTROY, }, ExpectLogContains: "nothing to do", }, @@ -406,7 +412,7 @@ func TestProvision(t *testing.T) { } }`, }, - Request: &proto.Provision_Plan{ + Request: &proto.PlanRequest{ RichParameterValues: []*proto.RichParameterValue{ { Name: "Example", @@ -418,27 +424,23 @@ func TestProvision(t *testing.T) { }, }, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: []*proto.RichParameter{ - { - Name: "Example", - Type: "string", - DefaultValue: "foobar", - }, - { - Name: "Sample", - Type: "string", - DefaultValue: "foobaz", - }, - }, - Resources: []*proto.Resource{{ - Name: "example", - Type: "null_resource", - }}, + Response: &proto.PlanComplete{ + Parameters: []*proto.RichParameter{ + { + Name: "Example", + Type: "string", + DefaultValue: "foobar", + }, + { + Name: "Sample", + Type: "string", + DefaultValue: "foobaz", }, }, + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + }}, }, }, { @@ -488,7 +490,7 @@ func TestProvision(t *testing.T) { ] }`, }, - Request: &proto.Provision_Plan{ + Request: &proto.PlanRequest{ RichParameterValues: []*proto.RichParameterValue{ { Name: "Example", @@ -500,27 +502,23 @@ func TestProvision(t *testing.T) { }, }, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Parameters: []*proto.RichParameter{ - { - Name: "Example", - Type: "string", - DefaultValue: "foobar", - }, - { - Name: "Sample", - Type: "string", - DefaultValue: "foobaz", - }, - }, - Resources: []*proto.Resource{{ - Name: "example", - Type: "null_resource", - }}, + Response: &proto.PlanComplete{ + Parameters: []*proto.RichParameter{ + { + Name: "Example", + Type: "string", + DefaultValue: "foobar", + }, + { + Name: "Sample", + Type: "string", + DefaultValue: "foobaz", }, }, + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + }}, }, }, { @@ -550,25 +548,21 @@ func TestProvision(t *testing.T) { } `, }, - Request: &proto.Provision_Plan{ + Request: &proto.PlanRequest{ GitAuthProviders: []*proto.GitAuthProvider{{ Id: "github", AccessToken: "some-value", }}, }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "null_resource", - Metadata: []*proto.Resource_Metadata{{ - Key: "token", - Value: "some-value", - }}, - }}, - }, - }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + Metadata: []*proto.Resource_Metadata{{ + Key: "token", + Value: "some-value", + }}, + }}, }, }, } @@ -579,50 +573,26 @@ func TestProvision(t *testing.T) { t.Parallel() ctx, api := setupProvisioner(t, nil) + sess := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, testCase.Files), + }) - directory := t.TempDir() - for path, content := range testCase.Files { - err := os.WriteFile(filepath.Join(directory, path), []byte(content), 0o600) - require.NoError(t, err) - } - - planRequest := &proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: directory, - }, - }, - }, - } + planRequest := &proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{ + Metadata: testCase.Metadata, + }}} if testCase.Request != nil { - if planRequest.GetPlan().GetConfig() == nil { - planRequest.GetPlan().Config = &proto.Provision_Config{} - } - planRequest.GetPlan().RichParameterValues = testCase.Request.RichParameterValues - planRequest.GetPlan().GitAuthProviders = testCase.Request.GitAuthProviders - if testCase.Request.Config != nil { - planRequest.GetPlan().Config.State = testCase.Request.Config.State - planRequest.GetPlan().Config.Metadata = testCase.Request.Config.Metadata - } - } - if planRequest.GetPlan().Config.Metadata == nil { - planRequest.GetPlan().Config.Metadata = &proto.Provision_Metadata{} + planRequest = &proto.Request{Type: &proto.Request_Plan{Plan: testCase.Request}} } gotExpectedLog := testCase.ExpectLogContains == "" - provision := func(req *proto.Provision_Request) *proto.Provision_Complete { - response, err := api.Provision(ctx) - require.NoError(t, err) - err = response.Send(req) + provision := func(req *proto.Request) *proto.Response { + err := sess.Send(req) require.NoError(t, err) - - var complete *proto.Provision_Complete - for { - msg, err := response.Recv() - if msg != nil && msg.GetLog() != nil { + msg, err := sess.Recv() + require.NoError(t, err) + if msg.GetLog() != nil { if testCase.ExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.ExpectLogContains) { gotExpectedLog = true } @@ -630,67 +600,51 @@ func TestProvision(t *testing.T) { t.Logf("log: [%s] %s", msg.GetLog().Level, msg.GetLog().Output) continue } - if testCase.ErrorContains != "" { - require.ErrorContains(t, err, testCase.ErrorContains) - break - } - require.NoError(t, err) - - if complete = msg.GetComplete(); complete == nil { - continue - } - - require.NoError(t, err) - - // Remove randomly generated data. - for _, resource := range msg.GetComplete().Resources { - sort.Slice(resource.Agents, func(i, j int) bool { - return resource.Agents[i].Name < resource.Agents[j].Name - }) - - for _, agent := range resource.Agents { - agent.Id = "" - if agent.GetToken() == "" { - continue - } - agent.Auth = &proto.Agent_Token{} - } - } + return msg + } + } - if testCase.Response != nil { - require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error) + resp := provision(planRequest) + planComplete := resp.GetPlan() + require.NotNil(t, planComplete) - resourcesGot, err := json.Marshal(msg.GetComplete().Resources) - require.NoError(t, err) - resourcesWant, err := json.Marshal(testCase.Response.GetComplete().Resources) - require.NoError(t, err) + if testCase.ErrorContains != "" { + require.Contains(t, planComplete.GetError(), testCase.ErrorContains) + } - require.Equal(t, string(resourcesWant), string(resourcesGot)) + if testCase.Response != nil { + require.Equal(t, testCase.Response.Error, planComplete.Error) - parametersGot, err := json.Marshal(msg.GetComplete().Parameters) - require.NoError(t, err) - parametersWant, err := json.Marshal(testCase.Response.GetComplete().Parameters) - require.NoError(t, err) - require.Equal(t, string(parametersWant), string(parametersGot)) - } - break - } + // Remove randomly generated data. + normalizeResources(planComplete.Resources) + resourcesGot, err := json.Marshal(planComplete.Resources) + require.NoError(t, err) + resourcesWant, err := json.Marshal(testCase.Response.Resources) + require.NoError(t, err) + require.Equal(t, string(resourcesWant), string(resourcesGot)) - return complete + parametersGot, err := json.Marshal(planComplete.Parameters) + require.NoError(t, err) + parametersWant, err := json.Marshal(testCase.Response.Parameters) + require.NoError(t, err) + require.Equal(t, string(parametersWant), string(parametersGot)) } - planComplete := provision(planRequest) - if testCase.Apply { - require.NotNil(t, planComplete.Plan) - provision(&proto.Provision_Request{ - Type: &proto.Provision_Request_Apply{ - Apply: &proto.Provision_Apply{ - Config: planRequest.GetPlan().GetConfig(), - Plan: planComplete.Plan, - }, - }, - }) + resp = provision(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{ + Metadata: &proto.Metadata{WorkspaceTransition: proto.WorkspaceTransition_START}, + }}}) + applyComplete := resp.GetApply() + require.NotNil(t, applyComplete) + + if testCase.Response != nil { + normalizeResources(applyComplete.Resources) + resourcesGot, err := json.Marshal(applyComplete.Resources) + require.NoError(t, err) + resourcesWant, err := json.Marshal(testCase.Response.Resources) + require.NoError(t, err) + require.Equal(t, string(resourcesWant), string(resourcesGot)) + } } if !gotExpectedLog { @@ -700,6 +654,22 @@ func TestProvision(t *testing.T) { } } +func normalizeResources(resources []*proto.Resource) { + for _, resource := range resources { + sort.Slice(resource.Agents, func(i, j int) bool { + return resource.Agents[i].Name < resource.Agents[j].Name + }) + + for _, agent := range resource.Agents { + agent.Id = "" + if agent.GetToken() == "" { + continue + } + agent.Auth = &proto.Agent_Token{} + } + } +} + // nolint:paralleltest func TestProvision_ExtraEnv(t *testing.T) { // #nosec @@ -708,31 +678,15 @@ func TestProvision_ExtraEnv(t *testing.T) { t.Setenv("TF_SUPERSECRET", secretValue) ctx, api := setupProvisioner(t, nil) + sess := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, map[string]string{"main.tf": `resource "null_resource" "A" {}`}), + }) - directory := t.TempDir() - path := filepath.Join(directory, "main.tf") - err := os.WriteFile(path, []byte(`resource "null_resource" "A" {}`), 0o600) - require.NoError(t, err) - - request := &proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: directory, - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_START, - }, - }, - }, - }, - } - response, err := api.Provision(ctx) - require.NoError(t, err) - err = response.Send(request) + err := sendPlan(sess, proto.WorkspaceTransition_START) require.NoError(t, err) found := false for { - msg, err := response.Recv() + msg, err := sess.Recv() require.NoError(t, err) if log := msg.GetLog(); log != nil { @@ -742,7 +696,7 @@ func TestProvision_ExtraEnv(t *testing.T) { } require.NotContains(t, log.Output, secretValue) } - if c := msg.GetComplete(); c != nil { + if c := msg.GetPlan(); c != nil { require.Empty(t, c.Error) break } @@ -774,48 +728,19 @@ func TestProvision_SafeEnv(t *testing.T) { ` ctx, api := setupProvisioner(t, nil) - - directory := t.TempDir() - path := filepath.Join(directory, "main.tf") - err := os.WriteFile(path, []byte(echoResource), 0o600) - require.NoError(t, err) - - response, err := api.Provision(ctx) - require.NoError(t, err) - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Plan{ - Plan: &proto.Provision_Plan{ - Config: &proto.Provision_Config{ - Directory: directory, - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_START, - }, - }, - }, - }, + sess := configure(ctx, t, api, &proto.Config{ + TemplateSourceArchive: makeTar(t, map[string]string{"main.tf": echoResource}), }) + + err := sendPlan(sess, proto.WorkspaceTransition_START) require.NoError(t, err) - _, complete := readProvisionLog(t, response) + _ = readProvisionLog(t, sess) - response, err = api.Provision(ctx) - require.NoError(t, err) - err = response.Send(&proto.Provision_Request{ - Type: &proto.Provision_Request_Apply{ - Apply: &proto.Provision_Apply{ - Config: &proto.Provision_Config{ - Directory: directory, - Metadata: &proto.Provision_Metadata{ - WorkspaceTransition: proto.WorkspaceTransition_START, - }, - }, - Plan: complete.GetPlan(), - }, - }, - }) + err = sendApply(sess, proto.WorkspaceTransition_START) require.NoError(t, err) - log, _ := readProvisionLog(t, response) + log := readProvisionLog(t, sess) require.Contains(t, log, passedValue) require.NotContains(t, log, secretValue) require.Contains(t, log, "CODER_") diff --git a/provisioner/terraform/serve.go b/provisioner/terraform/serve.go index 7a25d27dca598..0fc12ea870896 100644 --- a/provisioner/terraform/serve.go +++ b/provisioner/terraform/serve.go @@ -24,7 +24,6 @@ type ServeOptions struct { BinaryPath string // CachePath must not be used by multiple processes at once. CachePath string - Logger slog.Logger Tracer trace.Tracer // ExitTimeout defines how long we will wait for a running Terraform @@ -128,5 +127,6 @@ func (s *server) executor(workdir string) *executor { binaryPath: s.binaryPath, cachePath: s.cachePath, workdir: workdir, + logger: s.logger.Named("executor"), } } diff --git a/provisioner/terraform/testdata/fake_cancel.sh b/provisioner/terraform/testdata/fake_cancel.sh index cd2511facf938..2ea713379cce9 100755 --- a/provisioner/terraform/testdata/fake_cancel.sh +++ b/provisioner/terraform/testdata/fake_cancel.sh @@ -22,8 +22,9 @@ version) ;; init) case "$MODE" in - apply) + plan) echo "init" + exit 0 ;; init) sleep 10 & @@ -39,7 +40,7 @@ init) ;; esac ;; -apply) +plan) sleep 10 & sleep_pid=$! @@ -47,14 +48,14 @@ apply) trap 'json_print interrupt; exit 1' INT trap 'json_print terminate; exit 2' TERM - json_print apply_start + json_print plan_start wait - json_print apply_end + json_print plan_end ;; -plan) - echo "plan not supported" +apply) + echo "apply not supported" exit 1 ;; esac -exit 0 +exit 10 diff --git a/provisioner/terraform/testdata/fake_cancel_hang.sh b/provisioner/terraform/testdata/fake_cancel_hang.sh index c6d29c88c733f..e8db67f6837cd 100755 --- a/provisioner/terraform/testdata/fake_cancel_hang.sh +++ b/provisioner/terraform/testdata/fake_cancel_hang.sh @@ -23,19 +23,19 @@ init) echo "init" exit 0 ;; -apply) +plan) trap 'json_print interrupt' INT - json_print apply_start + json_print plan_start sleep 10 2>/dev/null >/dev/null - json_print apply_end + json_print plan_end exit 0 ;; -plan) - echo "plan not supported" +apply) + echo "apply not supported" exit 1 ;; esac -exit 0 +exit 10 diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 29a1e7dc505a9..018e0f25ac8e1 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -819,7 +819,7 @@ type AcquiredJob_WorkspaceBuild struct { RichParameterValues []*proto.RichParameterValue `protobuf:"bytes,4,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"` VariableValues []*proto.VariableValue `protobuf:"bytes,5,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"` GitAuthProviders []*proto.GitAuthProvider `protobuf:"bytes,6,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` - Metadata *proto.Provision_Metadata `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"` + Metadata *proto.Metadata `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"` State []byte `protobuf:"bytes,8,opt,name=state,proto3" json:"state,omitempty"` LogLevel string `protobuf:"bytes,9,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"` } @@ -891,7 +891,7 @@ func (x *AcquiredJob_WorkspaceBuild) GetGitAuthProviders() []*proto.GitAuthProvi return nil } -func (x *AcquiredJob_WorkspaceBuild) GetMetadata() *proto.Provision_Metadata { +func (x *AcquiredJob_WorkspaceBuild) GetMetadata() *proto.Metadata { if x != nil { return x.Metadata } @@ -917,8 +917,8 @@ type AcquiredJob_TemplateImport struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Metadata *proto.Provision_Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - UserVariableValues []*proto.VariableValue `protobuf:"bytes,2,rep,name=user_variable_values,json=userVariableValues,proto3" json:"user_variable_values,omitempty"` + Metadata *proto.Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + UserVariableValues []*proto.VariableValue `protobuf:"bytes,2,rep,name=user_variable_values,json=userVariableValues,proto3" json:"user_variable_values,omitempty"` } func (x *AcquiredJob_TemplateImport) Reset() { @@ -953,7 +953,7 @@ func (*AcquiredJob_TemplateImport) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{1, 1} } -func (x *AcquiredJob_TemplateImport) GetMetadata() *proto.Provision_Metadata { +func (x *AcquiredJob_TemplateImport) GetMetadata() *proto.Metadata { if x != nil { return x.Metadata } @@ -974,7 +974,7 @@ type AcquiredJob_TemplateDryRun struct { RichParameterValues []*proto.RichParameterValue `protobuf:"bytes,2,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"` VariableValues []*proto.VariableValue `protobuf:"bytes,3,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"` - Metadata *proto.Provision_Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + Metadata *proto.Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *AcquiredJob_TemplateDryRun) Reset() { @@ -1023,7 +1023,7 @@ func (x *AcquiredJob_TemplateDryRun) GetVariableValues() []*proto.VariableValue return nil } -func (x *AcquiredJob_TemplateDryRun) GetMetadata() *proto.Provision_Metadata { +func (x *AcquiredJob_TemplateDryRun) GetMetadata() *proto.Metadata { if x != nil { return x.Metadata } @@ -1335,7 +1335,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x1a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, - 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xab, 0x0b, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69, + 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x8d, 0x0b, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, @@ -1368,7 +1368,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xc1, 0x03, 0x0a, 0x0e, 0x57, 0x6f, 0x72, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xb7, 0x03, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, @@ -1389,193 +1389,191 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, - 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x1a, 0x9b, 0x01, 0x0a, - 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, - 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, 0x0a, 0x14, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xed, 0x01, 0x0a, 0x0e, 0x54, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x53, 0x0a, - 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, - 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, - 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, - 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x1a, 0x40, 0x0a, 0x12, 0x54, 0x72, - 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0xa5, 0x03, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, - 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, - 0x51, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, - 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x12, 0x51, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, - 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, - 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x52, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x5f, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, - 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x4a, 0x04, 0x08, 0x03, + 0x10, 0x04, 0x1a, 0x91, 0x01, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xe3, 0x01, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, + 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, + 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x1a, 0x40, 0x0a, 0x12, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa5, 0x03, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65, + 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x51, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, + 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x12, 0x51, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x52, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x5f, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x1a, 0x26, 0x0a, 0x0e, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd8, + 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, + 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x54, 0x0a, 0x0f, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, + 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x12, 0x55, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x64, + 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x1a, 0x26, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, - 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, - 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd8, 0x05, 0x0a, - 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, - 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, - 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, - 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, - 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x55, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x72, 0x79, - 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, - 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x5b, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x5b, 0x0a, 0x0e, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x81, 0x02, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x1a, 0x81, 0x02, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x70, 0x5f, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, + 0x70, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, 0x5f, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, + 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0e, 0x72, 0x69, + 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, + 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x45, 0x0a, 0x0e, 0x54, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, - 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0e, 0x72, 0x69, 0x63, 0x68, - 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, - 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x45, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, - 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, - 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, - 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, - 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x8a, 0x02, 0x0a, 0x10, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x4c, 0x0a, - 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, - 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, - 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, - 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4a, 0x04, 0x08, - 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x22, - 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, - 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, - 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f, - 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32, - 0xec, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, - 0x4a, 0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, - 0x74, 0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x03, 0x4c, 0x6f, + 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x8a, 0x02, 0x0a, + 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, + 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, + 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a, + 0x14, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, + 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, + 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x64, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, + 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4a, + 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, + 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, + 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, + 0x74, 0x22, 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64, + 0x69, 0x74, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, + 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c, + 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, + 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, + 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, + 0x01, 0x32, 0xec, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, + 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, + 0x75, 0x6f, 0x74, 0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, - 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, - 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, - 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2e, - 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, + 0x6f, 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, + 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1618,7 +1616,7 @@ var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ (*proto.VariableValue)(nil), // 22: provisioner.VariableValue (*proto.RichParameterValue)(nil), // 23: provisioner.RichParameterValue (*proto.GitAuthProvider)(nil), // 24: provisioner.GitAuthProvider - (*proto.Provision_Metadata)(nil), // 25: provisioner.Provision.Metadata + (*proto.Metadata)(nil), // 25: provisioner.Metadata (*proto.Resource)(nil), // 26: provisioner.Resource (*proto.RichParameter)(nil), // 27: provisioner.RichParameter } @@ -1642,12 +1640,12 @@ var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ 23, // 16: provisionerd.AcquiredJob.WorkspaceBuild.rich_parameter_values:type_name -> provisioner.RichParameterValue 22, // 17: provisionerd.AcquiredJob.WorkspaceBuild.variable_values:type_name -> provisioner.VariableValue 24, // 18: provisionerd.AcquiredJob.WorkspaceBuild.git_auth_providers:type_name -> provisioner.GitAuthProvider - 25, // 19: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Provision.Metadata - 25, // 20: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Provision.Metadata + 25, // 19: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Metadata + 25, // 20: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Metadata 22, // 21: provisionerd.AcquiredJob.TemplateImport.user_variable_values:type_name -> provisioner.VariableValue 23, // 22: provisionerd.AcquiredJob.TemplateDryRun.rich_parameter_values:type_name -> provisioner.RichParameterValue 22, // 23: provisionerd.AcquiredJob.TemplateDryRun.variable_values:type_name -> provisioner.VariableValue - 25, // 24: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Provision.Metadata + 25, // 24: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Metadata 26, // 25: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource 26, // 26: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource 26, // 27: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto index 2a417f48a0cc7..8d4fadffc6373 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -19,12 +19,12 @@ message AcquiredJob { repeated provisioner.RichParameterValue rich_parameter_values = 4; repeated provisioner.VariableValue variable_values = 5; repeated provisioner.GitAuthProvider git_auth_providers = 6; - provisioner.Provision.Metadata metadata = 7; + provisioner.Metadata metadata = 7; bytes state = 8; string log_level = 9; } message TemplateImport { - provisioner.Provision.Metadata metadata = 1; + provisioner.Metadata metadata = 1; repeated provisioner.VariableValue user_variable_values = 2; } message TemplateDryRun { @@ -32,7 +32,7 @@ message AcquiredJob { repeated provisioner.RichParameterValue rich_parameter_values = 2; repeated provisioner.VariableValue variable_values = 3; - provisioner.Provision.Metadata metadata = 4; + provisioner.Metadata metadata = 4; } string job_id = 1; @@ -45,9 +45,9 @@ message AcquiredJob { TemplateImport template_import = 7; TemplateDryRun template_dry_run = 8; } - // trace_metadata is currently used for tracing information only. It allows - // jobs to be tied to the request that created them. - map trace_metadata = 9; + // trace_metadata is currently used for tracing information only. It allows + // jobs to be tied to the request that created them. + map trace_metadata = 9; } message FailedJob { @@ -113,7 +113,7 @@ message UpdateJobRequest { string job_id = 1; repeated Log logs = 2; repeated provisioner.TemplateVariable template_variables = 4; - repeated provisioner.VariableValue user_variable_values = 5; + repeated provisioner.VariableValue user_variable_values = 5; bytes readme = 6; } @@ -121,7 +121,7 @@ message UpdateJobResponse { reserved 2; bool canceled = 1; - repeated provisioner.VariableValue variable_values = 3; + repeated provisioner.VariableValue variable_values = 3; } message CommitQuotaRequest { diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index f127ab7b584bd..a341bd5a3df85 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/yamux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/spf13/afero" "github.com/valyala/fasthttp/fasthttputil" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" @@ -44,7 +43,6 @@ type Provisioners map[string]sdkproto.DRPCProvisionerClient // Options provides customizations to the behavior of a provisioner daemon. type Options struct { - Filesystem afero.Fs Logger slog.Logger TracerProvider trace.TracerProvider Metrics *Metrics @@ -56,8 +54,6 @@ type Options struct { JobPollJitter time.Duration JobPollDebounce time.Duration Provisioners Provisioners - // WorkDirectory must not be used by multiple processes at once. - WorkDirectory string } // New creates and starts a provisioner daemon. @@ -80,9 +76,6 @@ func New(clientDialer Dialer, opts *Options) *Server { if opts.LogBufferInterval == 0 { opts.LogBufferInterval = 250 * time.Millisecond } - if opts.Filesystem == nil { - opts.Filesystem = afero.NewOsFs() - } if opts.TracerProvider == nil { opts.TracerProvider = trace.NewNoopTracerProvider() } @@ -405,8 +398,6 @@ func (p *Server) acquireJob(ctx context.Context) { Updater: p, QuotaCommitter: p, Logger: p.opts.Logger.Named("runner"), - Filesystem: p.opts.Filesystem, - WorkDirectory: p.opts.WorkDirectory, Provisioner: provisioner, UpdateInterval: p.opts.UpdateInterval, ForceCancelInterval: p.opts.ForceCancelInterval, diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index 90b9923996be9..ee379e0ab9929 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -25,7 +25,6 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/provisionerd" "github.com/coder/coder/v2/provisionerd/proto" - "github.com/coder/coder/v2/provisionerd/runner" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" @@ -129,7 +128,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -144,10 +143,15 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error { + parse: func(_ *provisionersdk.Session, _ *sdkproto.ParseRequest, _ <-chan struct{}) *sdkproto.ParseComplete { closerMutex.Lock() defer closerMutex.Unlock() - return closer.Close() + err := closer.Close() + c := &sdkproto.ParseComplete{} + if err != nil { + c.Error = err.Error() + } + return c }, }), }) @@ -180,7 +184,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -220,7 +224,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -235,9 +239,13 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error { - <-stream.Context().Done() - return nil + parse: func( + _ *provisionersdk.Session, + _ *sdkproto.ParseRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.ParseComplete { + <-cancelOrComplete + return &sdkproto.ParseComplete{} }, }), }) @@ -255,7 +263,6 @@ func TestProvisionerd(t *testing.T) { didComplete atomic.Bool didLog atomic.Bool didAcquireJob atomic.Bool - didDryRun = atomic.NewBool(true) didReadme atomic.Bool completeChan = make(chan struct{}) completeOnce sync.Once @@ -273,12 +280,12 @@ func TestProvisionerd(t *testing.T) { JobId: "test", Provisioner: "someprovisioner", TemplateSourceArchive: createTar(t, map[string]string{ - "test.txt": "content", - runner.ReadmeFile: "# A cool template 😎\n", + "test.txt": "content", + provisionersdk.ReadmeFile: "# A cool template 😎\n", }), Type: &proto.AcquiredJob_TemplateImport_{ TemplateImport: &proto.AcquiredJob_TemplateImport{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -299,54 +306,34 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error { - data, err := os.ReadFile(filepath.Join(request.Directory, "test.txt")) + parse: func( + s *provisionersdk.Session, + _ *sdkproto.ParseRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.ParseComplete { + data, err := os.ReadFile(filepath.Join(s.WorkDirectory, "test.txt")) require.NoError(t, err) require.Equal(t, "content", string(data)) - - err = stream.Send(&sdkproto.Parse_Response{ - Type: &sdkproto.Parse_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_INFO, - Output: "hello", - }, - }, - }) - require.NoError(t, err) - - err = stream.Send(&sdkproto.Parse_Response{ - Type: &sdkproto.Parse_Response_Complete{ - Complete: &sdkproto.Parse_Complete{}, - }, - }) - require.NoError(t, err) - return nil + s.ProvisionLog(sdkproto.LogLevel_INFO, "hello") + return &sdkproto.ParseComplete{} }, - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - request, err := stream.Recv() - require.NoError(t, err) - if request.GetApply() != nil { - didDryRun.Store(false) + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_INFO, "hello") + return &sdkproto.PlanComplete{ + Resources: []*sdkproto.Resource{}, } - err = stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_INFO, - Output: "hello", - }, - }, - }) - require.NoError(t, err) - - err = stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Resources: []*sdkproto.Resource{}, - }, - }, - }) - require.NoError(t, err) - return nil + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("dry run should not apply") + return &sdkproto.ApplyComplete{} }, }), }) @@ -355,7 +342,6 @@ func TestProvisionerd(t *testing.T) { require.NoError(t, closer.Close()) assert.True(t, didLog.Load(), "should log some updates") assert.True(t, didComplete.Load(), "should complete the job") - assert.True(t, didDryRun.Load(), "should be a dry run") }) t.Run("TemplateDryRun", func(t *testing.T) { @@ -371,7 +357,7 @@ func TestProvisionerd(t *testing.T) { completeChan = make(chan struct{}) completeOnce sync.Once - metadata = &sdkproto.Provision_Metadata{} + metadata = &sdkproto.Metadata{} ) closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { @@ -414,16 +400,22 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Resources: []*sdkproto.Resource{}, - }, - }, - }) - require.NoError(t, err) - return nil + plan: func( + _ *provisionersdk.Session, + _ *sdkproto.PlanRequest, + _ <-chan struct{}, + ) *sdkproto.PlanComplete { + return &sdkproto.PlanComplete{ + Resources: []*sdkproto.Resource{}, + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("dry run should not apply") + return &sdkproto.ApplyComplete{} }, }), }) @@ -464,7 +456,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -482,24 +474,20 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_DEBUG, - Output: "wow", - }, - }, - }) - require.NoError(t, err) - - err = stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{}, - }, - }) - require.NoError(t, err) - return nil + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow") + return &sdkproto.PlanComplete{} + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + return &sdkproto.ApplyComplete{} }, }), }) @@ -540,7 +528,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -567,40 +555,46 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_DEBUG, - Output: "wow", + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow") + return &sdkproto.PlanComplete{ + Resources: []*sdkproto.Resource{ + { + DailyCost: 10, + }, + { + DailyCost: 15, }, }, - }) - require.NoError(t, err) - - err = stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Resources: []*sdkproto.Resource{ - { - DailyCost: 10, - }, - { - DailyCost: 15, - }, - }, + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("should not apply when resources exceed quota") + return &sdkproto.ApplyComplete{ + Resources: []*sdkproto.Resource{ + { + DailyCost: 10, + }, + { + DailyCost: 15, }, }, - }) - require.NoError(t, err) - return nil + } }, }), }) require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) require.NoError(t, closer.Close()) assert.True(t, didLog.Load(), "should log some updates") - assert.False(t, didComplete.Load(), "should complete the job") + assert.False(t, didComplete.Load(), "should not complete the job") assert.True(t, didFail.Load(), "should fail the job") }) @@ -633,7 +627,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -646,14 +640,24 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - return stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Error: "some error", - }, - }, - }) + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + cancelOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + return &sdkproto.PlanComplete{ + Error: "some error", + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("should not apply when plan errors") + return &sdkproto.ApplyComplete{ + Error: "some error", + } }, }), }) @@ -683,7 +687,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -712,31 +716,24 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - // Ignore the first provision message! - _, _ = stream.Recv() - - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_DEBUG, - Output: "in progress", - }, - }, - }) - require.NoError(t, err) - - msg, err := stream.Recv() - require.NoError(t, err) - require.NotNil(t, msg.GetCancel()) - - return stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Error: "some error", - }, - }, - }) + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + canceledOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "in progress") + <-canceledOrComplete + return &sdkproto.PlanComplete{ + Error: "some error", + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("should not apply when shut down during plan") + return &sdkproto.ApplyComplete{} }, }), }) @@ -768,7 +765,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -805,31 +802,24 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - // Ignore the first provision message! - _, _ = stream.Recv() - - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_DEBUG, - Output: "in progress", - }, - }, - }) - require.NoError(t, err) - - msg, err := stream.Recv() - require.NoError(t, err) - require.NotNil(t, msg.GetCancel()) - - return stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Error: "some error", - }, - }, - }) + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + canceledOrComplete <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "in progress") + <-canceledOrComplete + return &sdkproto.PlanComplete{ + Error: "some error", + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("should not apply when shut down during plan") + return &sdkproto.ApplyComplete{} }, }), }) @@ -867,7 +857,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -898,16 +888,22 @@ func TestProvisionerd(t *testing.T) { return client, nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - // Ignore the first provision message! - _, _ = stream.Recv() - return stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{ - Error: "some error", - }, - }, - }) + plan: func( + _ *provisionersdk.Session, + _ *sdkproto.PlanRequest, + _ <-chan struct{}, + ) *sdkproto.PlanComplete { + return &sdkproto.PlanComplete{ + Error: "some error", + } + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + t.Error("should not apply when error during plan") + return &sdkproto.ApplyComplete{} }, }), }) @@ -945,7 +941,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -977,14 +973,19 @@ func TestProvisionerd(t *testing.T) { return client, nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - // Ignore the first provision message! - _, _ = stream.Recv() - return stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{}, - }, - }) + plan: func( + _ *provisionersdk.Session, + _ *sdkproto.PlanRequest, + _ <-chan struct{}, + ) *sdkproto.PlanComplete { + return &sdkproto.PlanComplete{} + }, + apply: func( + _ *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + return &sdkproto.ApplyComplete{} }, }), }) @@ -1023,7 +1024,7 @@ func TestProvisionerd(t *testing.T) { }), Type: &proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - Metadata: &sdkproto.Provision_Metadata{}, + Metadata: &sdkproto.Metadata{}, }, }, }, nil @@ -1056,24 +1057,21 @@ func TestProvisionerd(t *testing.T) { }), nil }, provisionerd.Provisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{ - provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - err := stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Log{ - Log: &sdkproto.Log{ - Level: sdkproto.LogLevel_DEBUG, - Output: "wow", - }, - }, - }) - require.NoError(t, err) - - err = stream.Send(&sdkproto.Provision_Response{ - Type: &sdkproto.Provision_Response_Complete{ - Complete: &sdkproto.Provision_Complete{}, - }, - }) - require.NoError(t, err) - return nil + plan: func( + s *provisionersdk.Session, + _ *sdkproto.PlanRequest, + _ <-chan struct{}, + ) *sdkproto.PlanComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow") + return &sdkproto.PlanComplete{} + }, + apply: func( + s *provisionersdk.Session, + _ *sdkproto.ApplyRequest, + _ <-chan struct{}, + ) *sdkproto.ApplyComplete { + s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow") + return &sdkproto.ApplyComplete{} }, }), }) @@ -1111,7 +1109,6 @@ func createProvisionerd(t *testing.T, dialer provisionerd.Dialer, provisioners p JobPollInterval: 50 * time.Millisecond, UpdateInterval: 50 * time.Millisecond, Provisioners: provisioners, - WorkDirectory: t.TempDir(), }) t.Cleanup(func() { _ = server.Close() @@ -1172,15 +1169,15 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio _ = clientPipe.Close() _ = serverPipe.Close() }) - mux := drpcmux.New() - err := sdkproto.DRPCRegisterProvisioner(mux, &server) - require.NoError(t, err) - srv := drpcserver.New(mux) ctx, cancelFunc := context.WithCancel(context.Background()) closed := make(chan struct{}) go func() { defer close(closed) - _ = srv.Serve(ctx, serverPipe) + _ = provisionersdk.Serve(ctx, &server, &provisionersdk.ServeOptions{ + Listener: serverPipe, + Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug).Named("test-provisioner"), + WorkDirectory: t.TempDir(), + }) }() t.Cleanup(func() { cancelFunc() @@ -1200,16 +1197,21 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio } type provisionerTestServer struct { - parse func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error - provision func(stream sdkproto.DRPCProvisioner_ProvisionStream) error + parse func(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete + plan func(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete + apply func(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete +} + +func (p *provisionerTestServer) Parse(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete { + return p.parse(s, r, canceledOrComplete) } -func (p *provisionerTestServer) Parse(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error { - return p.parse(request, stream) +func (p *provisionerTestServer) Plan(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete { + return p.plan(s, r, canceledOrComplete) } -func (p *provisionerTestServer) Provision(stream sdkproto.DRPCProvisioner_ProvisionStream) error { - return p.provision(stream) +func (p *provisionerTestServer) Apply(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete { + return p.apply(s, r, canceledOrComplete) } // Fulfills the protobuf interface for a ProvisionerDaemon with diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go index 5911004f98e2e..7afa7a0999627 100644 --- a/provisionerd/runner/runner.go +++ b/provisionerd/runner/runner.go @@ -1,15 +1,9 @@ package runner import ( - "archive/tar" - "bytes" "context" "errors" "fmt" - "io" - "os" - "path" - "path/filepath" "reflect" "strings" "sync" @@ -18,7 +12,6 @@ import ( "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/afero" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.14.0" @@ -54,14 +47,14 @@ type Runner struct { sender JobUpdater quotaCommitter QuotaCommitter logger slog.Logger - filesystem afero.Fs - workDirectory string provisioner sdkproto.DRPCProvisionerClient lastUpdate atomic.Pointer[time.Time] updateInterval time.Duration forceCancelInterval time.Duration logBufferInterval time.Duration + // session is the provisioning session with the (possibly remote) provisioner + session sdkproto.DRPCProvisioner_SessionClient // closed when the Runner is finished sending any updates/failed/complete. done chan struct{} // active as long as we are not canceled @@ -108,8 +101,6 @@ type Options struct { Updater JobUpdater QuotaCommitter QuotaCommitter Logger slog.Logger - Filesystem afero.Fs - WorkDirectory string Provisioner sdkproto.DRPCProvisionerClient UpdateInterval time.Duration ForceCancelInterval time.Duration @@ -149,8 +140,6 @@ func New( sender: opts.Updater, quotaCommitter: opts.QuotaCommitter, logger: logger, - filesystem: opts.Filesystem, - workDirectory: opts.WorkDirectory, provisioner: opts.Provisioner, updateInterval: opts.UpdateInterval, forceCancelInterval: opts.ForceCancelInterval, @@ -386,6 +375,14 @@ func (r *Runner) doCleanFinish(ctx context.Context) { r.setComplete(completedJob) }() + var err error + r.session, err = r.provisioner.Session(ctx) + if err != nil { + failedJob = r.failedJobf("open session: %s", err) + return + } + defer r.session.Close() + defer func() { ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() @@ -396,23 +393,6 @@ func (r *Runner) doCleanFinish(ctx context.Context) { Stage: "Cleaning Up", CreatedAt: time.Now().UnixMilli(), }) - - // Cleanup the work directory after execution. - for attempt := 0; attempt < 5; attempt++ { - err := r.filesystem.RemoveAll(r.workDirectory) - if err != nil { - // On Windows, open files cannot be removed. - // When the provisioner daemon is shutting down, - // it may take a few milliseconds for processes to exit. - // See: https://github.com/golang/go/issues/50510 - r.logger.Debug(ctx, "failed to clean work directory; trying again", slog.Error(err)) - time.Sleep(250 * time.Millisecond) - continue - } - r.logger.Debug(ctx, "cleaned up work directory") - break - } - r.flushQueuedLogs(ctx) }() @@ -424,85 +404,19 @@ func (r *Runner) do(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() - err := r.filesystem.MkdirAll(r.workDirectory, 0o700) - if err != nil { - return nil, r.failedJobf("create work directory %q: %s", r.workDirectory, err) - } - r.queueLog(ctx, &proto.Log{ Source: proto.LogSource_PROVISIONER_DAEMON, Level: sdkproto.LogLevel_INFO, Stage: "Setting up", CreatedAt: time.Now().UnixMilli(), }) - if err != nil { - return nil, r.failedJobf("write log: %s", err) - } - - r.logger.Info(ctx, "unpacking template source archive", - slog.F("size_bytes", len(r.job.TemplateSourceArchive)), - ) - reader := tar.NewReader(bytes.NewBuffer(r.job.TemplateSourceArchive)) - for { - header, err := reader.Next() - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, r.failedJobf("read template source archive: %s", err) - } - // #nosec - headerPath := filepath.Join(r.workDirectory, header.Name) - if !strings.HasPrefix(headerPath, filepath.Clean(r.workDirectory)) { - return nil, r.failedJobf("tar attempts to target relative upper directory") - } - mode := header.FileInfo().Mode() - if mode == 0 { - mode = 0o600 - } - switch header.Typeflag { - case tar.TypeDir: - err = r.filesystem.MkdirAll(headerPath, mode) - if err != nil { - return nil, r.failedJobf("mkdir %q: %s", headerPath, err) - } - r.logger.Debug(context.Background(), "extracted directory", slog.F("path", headerPath)) - case tar.TypeReg: - file, err := r.filesystem.OpenFile(headerPath, os.O_CREATE|os.O_RDWR, mode) - if err != nil { - return nil, r.failedJobf("create file %q (mode %s): %s", headerPath, mode, err) - } - // Max file size of 10MiB. - size, err := io.CopyN(file, reader, 10<<20) - if errors.Is(err, io.EOF) { - err = nil - } - if err != nil { - _ = file.Close() - return nil, r.failedJobf("copy file %q: %s", headerPath, err) - } - err = file.Close() - if err != nil { - return nil, r.failedJobf("close file %q: %s", headerPath, err) - } - r.logger.Debug(context.Background(), "extracted file", - slog.F("size_bytes", size), - slog.F("path", headerPath), - slog.F("mode", mode), - ) - } - } switch jobType := r.job.Type.(type) { case *proto.AcquiredJob_TemplateImport_: r.logger.Debug(context.Background(), "acquired job is template import", slog.F("user_variable_values", redactVariableValues(jobType.TemplateImport.UserVariableValues)), ) - failedJob := r.runReadmeParse(ctx) - if failedJob != nil { - return nil, failedJob - } return r.runTemplateImport(ctx) case *proto.AcquiredJob_TemplateDryRun_: r.logger.Debug(context.Background(), "acquired job is template dry-run", @@ -525,6 +439,14 @@ func (r *Runner) do(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) } } +func (r *Runner) configure(config *sdkproto.Config) *proto.FailedJob { + err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Config{Config: config}}) + if err != nil { + return r.failedJobf("send config: %s", err) + } + return nil +} + // heartbeatRoutine periodically sends updates on the job, which keeps coder server // from assuming the job is stalled, and allows the runner to learn if the job // has been canceled by the user. @@ -577,44 +499,16 @@ func (r *Runner) heartbeatRoutine(ctx context.Context) { } } -// ReadmeFile is the location we look for to extract documentation from template -// versions. -const ReadmeFile = "README.md" - -func (r *Runner) runReadmeParse(ctx context.Context) *proto.FailedJob { +func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) { ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() - fi, err := afero.ReadFile(r.filesystem, path.Join(r.workDirectory, ReadmeFile)) - if err != nil { - r.queueLog(ctx, &proto.Log{ - Source: proto.LogSource_PROVISIONER_DAEMON, - Level: sdkproto.LogLevel_DEBUG, - Stage: "No README.md provided", - CreatedAt: time.Now().UnixMilli(), - }) - return nil - } - - _, err = r.update(ctx, &proto.UpdateJobRequest{ - JobId: r.job.JobId, - Logs: []*proto.Log{{ - Source: proto.LogSource_PROVISIONER_DAEMON, - Level: sdkproto.LogLevel_INFO, - Stage: "Adding README.md...", - CreatedAt: time.Now().UnixMilli(), - }}, - Readme: fi, + failedJob := r.configure(&sdkproto.Config{ + TemplateSourceArchive: r.job.GetTemplateSourceArchive(), }) - if err != nil { - return r.failedJobf("write log: %s", err) + if failedJob != nil { + return nil, failedJob } - return nil -} - -func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) { - ctx, span := r.startTrace(ctx, tracing.FuncName()) - defer span.End() // Parse parameters and update the job with the parameter specs r.queueLog(ctx, &proto.Log{ @@ -623,7 +517,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p Stage: "Parsing template parameters", CreatedAt: time.Now().UnixMilli(), }) - templateVariables, err := r.runTemplateImportParse(ctx) + templateVariables, readme, err := r.runTemplateImportParse(ctx) if err != nil { return nil, r.failedJobf("run parse: %s", err) } @@ -634,6 +528,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p JobId: r.job.JobId, TemplateVariables: templateVariables, UserVariableValues: r.job.GetTemplateImport().GetUserVariableValues(), + Readme: readme, }) if err != nil { return nil, r.failedJobf("update job: %s", err) @@ -646,7 +541,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p Stage: "Detecting persistent resources", CreatedAt: time.Now().UnixMilli(), }) - startProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Provision_Metadata{ + startProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Metadata{ CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl, WorkspaceTransition: sdkproto.WorkspaceTransition_START, }) @@ -661,7 +556,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p Stage: "Detecting ephemeral resources", CreatedAt: time.Now().UnixMilli(), }) - stopProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Provision_Metadata{ + stopProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Metadata{ CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl, WorkspaceTransition: sdkproto.WorkspaceTransition_STOP, }) @@ -682,25 +577,24 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p }, nil } -// Parses template variables and parameter schemas from source. -func (r *Runner) runTemplateImportParse(ctx context.Context) ([]*sdkproto.TemplateVariable, error) { +// Parses template variables and README from source. +func (r *Runner) runTemplateImportParse(ctx context.Context) ( + vars []*sdkproto.TemplateVariable, readme []byte, err error, +) { ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() - stream, err := r.provisioner.Parse(ctx, &sdkproto.Parse_Request{ - Directory: r.workDirectory, - }) + err = r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Parse{Parse: &sdkproto.ParseRequest{}}}) if err != nil { - return nil, xerrors.Errorf("parse source: %w", err) + return nil, nil, xerrors.Errorf("parse source: %w", err) } - defer stream.Close() for { - msg, err := stream.Recv() + msg, err := r.session.Recv() if err != nil { - return nil, xerrors.Errorf("recv parse source: %w", err) + return nil, nil, xerrors.Errorf("recv parse source: %w", err) } switch msgType := msg.Type.(type) { - case *sdkproto.Parse_Response_Log: + case *sdkproto.Response_Log: r.logger.Debug(context.Background(), "parse job logged", slog.F("level", msgType.Log.Level), slog.F("output", msgType.Log.Output), @@ -713,14 +607,20 @@ func (r *Runner) runTemplateImportParse(ctx context.Context) ([]*sdkproto.Templa Output: msgType.Log.Output, Stage: "Parse parameters", }) - case *sdkproto.Parse_Response_Complete: + case *sdkproto.Response_Parse: + pc := msgType.Parse r.logger.Debug(context.Background(), "parse complete", - slog.F("template_variables", msgType.Complete.TemplateVariables), + slog.F("template_variables", pc.TemplateVariables), + slog.F("readme_len", len(pc.Readme)), + slog.F("error", pc.Error), ) + if pc.Error != "" { + return nil, nil, xerrors.Errorf("parse error: %s", pc.Error) + } - return msgType.Complete.TemplateVariables, nil + return msgType.Parse.TemplateVariables, msgType.Parse.Readme, nil default: - return nil, xerrors.Errorf("invalid message type %q received from provisioner", + return nil, nil, xerrors.Errorf("invalid message type %q received from provisioner", reflect.TypeOf(msg.Type).String()) } } @@ -735,13 +635,18 @@ type templateImportProvision struct { // Performs a dry-run provision when importing a template. // This is used to detect resources that would be provisioned for a workspace in various states. // It doesn't define values for rich parameters as they're unknown during template import. -func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) { +func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Metadata) (*templateImportProvision, error) { return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata) } // Performs a dry-run provision with provided rich parameters. // This is used to detect resources that would be provisioned for a workspace in various states. -func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Context, variableValues []*sdkproto.VariableValue, richParameterValues []*sdkproto.RichParameterValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) { +func (r *Runner) runTemplateImportProvisionWithRichParameters( + ctx context.Context, + variableValues []*sdkproto.VariableValue, + richParameterValues []*sdkproto.RichParameterValue, + metadata *sdkproto.Metadata, +) (*templateImportProvision, error) { ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() @@ -754,46 +659,38 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex } // use the notStopped so that if we attempt to gracefully cancel, the stream will still be available for us // to send the cancel to the provisioner - stream, err := r.provisioner.Provision(ctx) + err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Plan{Plan: &sdkproto.PlanRequest{ + Metadata: metadata, + RichParameterValues: richParameterValues, + VariableValues: variableValues, + }}}) if err != nil { - return nil, xerrors.Errorf("provision: %w", err) + return nil, xerrors.Errorf("start provision: %w", err) } - defer stream.Close() + nevermind := make(chan struct{}) + defer close(nevermind) go func() { select { + case <-nevermind: + return case <-r.notStopped.Done(): return case <-r.notCanceled.Done(): - _ = stream.Send(&sdkproto.Provision_Request{ - Type: &sdkproto.Provision_Request_Cancel{ - Cancel: &sdkproto.Provision_Cancel{}, + _ = r.session.Send(&sdkproto.Request{ + Type: &sdkproto.Request_Cancel{ + Cancel: &sdkproto.CancelRequest{}, }, }) } }() - err = stream.Send(&sdkproto.Provision_Request{ - Type: &sdkproto.Provision_Request_Plan{ - Plan: &sdkproto.Provision_Plan{ - Config: &sdkproto.Provision_Config{ - Directory: r.workDirectory, - Metadata: metadata, - }, - RichParameterValues: richParameterValues, - VariableValues: variableValues, - }, - }, - }) - if err != nil { - return nil, xerrors.Errorf("start provision: %w", err) - } for { - msg, err := stream.Recv() + msg, err := r.session.Recv() if err != nil { return nil, xerrors.Errorf("recv import provision: %w", err) } switch msgType := msg.Type.(type) { - case *sdkproto.Provision_Response_Log: + case *sdkproto.Response_Log: r.logger.Debug(context.Background(), "template import provision job logged", slog.F("level", msgType.Log.Level), slog.F("output", msgType.Log.Output), @@ -805,25 +702,25 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex Output: msgType.Log.Output, Stage: stage, }) - case *sdkproto.Provision_Response_Complete: - if msgType.Complete.Error != "" { + case *sdkproto.Response_Plan: + c := msgType.Plan + if c.Error != "" { r.logger.Info(context.Background(), "dry-run provision failure", - slog.F("error", msgType.Complete.Error), + slog.F("error", c.Error), ) - return nil, xerrors.New(msgType.Complete.Error) + return nil, xerrors.New(c.Error) } r.logger.Info(context.Background(), "parse dry-run provision successful", - slog.F("resource_count", len(msgType.Complete.Resources)), - slog.F("resources", msgType.Complete.Resources), - slog.F("state_length", len(msgType.Complete.State)), + slog.F("resource_count", len(c.Resources)), + slog.F("resources", c.Resources), ) return &templateImportProvision{ - Resources: msgType.Complete.Resources, - Parameters: msgType.Complete.Parameters, - GitAuthProviders: msgType.Complete.GitAuthProviders, + Resources: c.Resources, + Parameters: c.Parameters, + GitAuthProviders: c.GitAuthProviders, }, nil default: return nil, xerrors.Errorf("invalid message type %q received from provisioner", @@ -864,6 +761,13 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p metadata.WorkspaceOwnerId = id.String() } + failedJob := r.configure(&sdkproto.Config{ + TemplateSourceArchive: r.job.GetTemplateSourceArchive(), + }) + if failedJob != nil { + return nil, failedJob + } + // Run the template import provision task since it's already a dry run. provision, err := r.runTemplateImportProvisionWithRichParameters(ctx, r.job.GetTemplateDryRun().GetVariableValues(), @@ -884,41 +788,39 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p }, nil } -func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Provision_Request) ( - *sdkproto.Provision_Complete, *proto.FailedJob, +func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Request) ( + *sdkproto.Response, *proto.FailedJob, ) { // use the notStopped so that if we attempt to gracefully cancel, the stream // will still be available for us to send the cancel to the provisioner - stream, err := r.provisioner.Provision(ctx) + err := r.session.Send(req) if err != nil { - return nil, r.failedWorkspaceBuildf("provision: %s", err) + return nil, r.failedWorkspaceBuildf("start provision: %s", err) } - defer stream.Close() + nevermind := make(chan struct{}) + defer close(nevermind) go func() { select { + case <-nevermind: + return case <-r.notStopped.Done(): return case <-r.notCanceled.Done(): - _ = stream.Send(&sdkproto.Provision_Request{ - Type: &sdkproto.Provision_Request_Cancel{ - Cancel: &sdkproto.Provision_Cancel{}, + _ = r.session.Send(&sdkproto.Request{ + Type: &sdkproto.Request_Cancel{ + Cancel: &sdkproto.CancelRequest{}, }, }) } }() - err = stream.Send(req) - if err != nil { - return nil, r.failedWorkspaceBuildf("start provision: %s", err) - } - for { - msg, err := stream.Recv() + msg, err := r.session.Recv() if err != nil { return nil, r.failedWorkspaceBuildf("recv workspace provision: %s", err) } switch msgType := msg.Type.(type) { - case *sdkproto.Provision_Response_Log: + case *sdkproto.Response_Log: r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "workspace provisioner job logged", slog.F("level", msgType.Log.Level), slog.F("output", msgType.Log.Output), @@ -932,33 +834,9 @@ func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto Output: msgType.Log.Output, Stage: stage, }) - case *sdkproto.Provision_Response_Complete: - if msgType.Complete.Error != "" { - r.logger.Warn(context.Background(), "provision failed; updating state", - slog.F("state_length", len(msgType.Complete.State)), - slog.F("error", msgType.Complete.Error), - ) - - return nil, &proto.FailedJob{ - JobId: r.job.JobId, - Error: msgType.Complete.Error, - Type: &proto.FailedJob_WorkspaceBuild_{ - WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ - State: msgType.Complete.State, - }, - }, - } - } - - r.logger.Info(context.Background(), "provision successful", - slog.F("resource_count", len(msgType.Complete.Resources)), - slog.F("resources", msgType.Complete.Resources), - slog.F("state_length", len(msgType.Complete.State)), - ) - // Stop looping! - return msgType.Complete, nil default: - return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", msg.Type) + // Stop looping! + return msg, nil } } } @@ -1035,18 +913,19 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p applyStage = "Destroying workspace" } - config := &sdkproto.Provision_Config{ - Directory: r.workDirectory, - Metadata: r.job.GetWorkspaceBuild().Metadata, - State: r.job.GetWorkspaceBuild().State, - - ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel, + failedJob := r.configure(&sdkproto.Config{ + TemplateSourceArchive: r.job.GetTemplateSourceArchive(), + State: r.job.GetWorkspaceBuild().State, + ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel, + }) + if failedJob != nil { + return nil, failedJob } - completedPlan, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Provision_Request{ - Type: &sdkproto.Provision_Request_Plan{ - Plan: &sdkproto.Provision_Plan{ - Config: config, + resp, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Request{ + Type: &sdkproto.Request_Plan{ + Plan: &sdkproto.PlanRequest{ + Metadata: r.job.GetWorkspaceBuild().Metadata, RichParameterValues: r.job.GetWorkspaceBuild().RichParameterValues, VariableValues: r.job.GetWorkspaceBuild().VariableValues, GitAuthProviders: r.job.GetWorkspaceBuild().GitAuthProviders, @@ -1056,9 +935,31 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p if failed != nil { return nil, failed } + planComplete := resp.GetPlan() + if planComplete == nil { + return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type) + } + if planComplete.Error != "" { + r.logger.Warn(context.Background(), "plan request failed", + slog.F("error", planComplete.Error), + ) + + return nil, &proto.FailedJob{ + JobId: r.job.JobId, + Error: planComplete.Error, + Type: &proto.FailedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{}, + }, + } + } + + r.logger.Info(context.Background(), "plan request successful", + slog.F("resource_count", len(planComplete.Resources)), + slog.F("resources", planComplete.Resources), + ) r.flushQueuedLogs(ctx) if commitQuota { - failed = r.commitQuota(ctx, completedPlan.GetResources()) + failed = r.commitQuota(ctx, planComplete.Resources) r.flushQueuedLogs(ctx) if failed != nil { return nil, failed @@ -1072,25 +973,50 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p CreatedAt: time.Now().UnixMilli(), }) - completedApply, failed := r.buildWorkspace(ctx, applyStage, &sdkproto.Provision_Request{ - Type: &sdkproto.Provision_Request_Apply{ - Apply: &sdkproto.Provision_Apply{ - Config: config, - Plan: completedPlan.GetPlan(), + resp, failed = r.buildWorkspace(ctx, applyStage, &sdkproto.Request{ + Type: &sdkproto.Request_Apply{ + Apply: &sdkproto.ApplyRequest{ + Metadata: r.job.GetWorkspaceBuild().Metadata, }, }, }) if failed != nil { return nil, failed } + applyComplete := resp.GetApply() + if applyComplete == nil { + return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type) + } + if applyComplete.Error != "" { + r.logger.Warn(context.Background(), "apply failed; updating state", + slog.F("error", applyComplete.Error), + slog.F("state_len", len(applyComplete.State)), + ) + + return nil, &proto.FailedJob{ + JobId: r.job.JobId, + Error: applyComplete.Error, + Type: &proto.FailedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ + State: applyComplete.State, + }, + }, + } + } + + r.logger.Info(context.Background(), "apply successful", + slog.F("resource_count", len(applyComplete.Resources)), + slog.F("resources", applyComplete.Resources), + slog.F("state_len", len(applyComplete.State)), + ) r.flushQueuedLogs(ctx) return &proto.CompletedJob{ JobId: r.job.JobId, Type: &proto.CompletedJob_WorkspaceBuild_{ WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{ - State: completedApply.GetState(), - Resources: completedApply.GetResources(), + State: applyComplete.State, + Resources: applyComplete.Resources, }, }, }, nil diff --git a/provisionersdk/errors.go b/provisionersdk/errors.go new file mode 100644 index 0000000000000..0dc66e6e6b301 --- /dev/null +++ b/provisionersdk/errors.go @@ -0,0 +1,19 @@ +package provisionersdk + +import ( + "fmt" + + "github.com/coder/coder/v2/provisionersdk/proto" +) + +func ParseErrorf(format string, args ...any) *proto.ParseComplete { + return &proto.ParseComplete{Error: fmt.Sprintf(format, args...)} +} + +func PlanErrorf(format string, args ...any) *proto.PlanComplete { + return &proto.PlanComplete{Error: fmt.Sprintf(format, args...)} +} + +func ApplyErrorf(format string, args ...any) *proto.ApplyComplete { + return &proto.ApplyComplete{Error: fmt.Sprintf(format, args...)} +} diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index f39e9731e6101..c0ea0be327953 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -125,6 +125,7 @@ func (AppSharingLevel) EnumDescriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1} } +// WorkspaceTransition is the desired outcome of a build type WorkspaceTransition int32 const ( @@ -1313,15 +1314,27 @@ func (x *Resource) GetDailyCost() int32 { return 0 } -// Parse consumes source-code from a directory to produce inputs. -type Parse struct { +// Metadata is information about a workspace used in the execution of a build +type Metadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` + WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` + WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` + WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` + WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` + WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` + WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` + TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` + TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` + WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` + WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` } -func (x *Parse) Reset() { - *x = Parse{} +func (x *Metadata) Reset() { + *x = Metadata{} if protoimpl.UnsafeEnabled { mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1329,13 +1342,13 @@ func (x *Parse) Reset() { } } -func (x *Parse) String() string { +func (x *Metadata) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Parse) ProtoMessage() {} +func (*Metadata) ProtoMessage() {} -func (x *Parse) ProtoReflect() protoreflect.Message { +func (x *Metadata) ProtoReflect() protoreflect.Message { mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1347,158 +1360,118 @@ func (x *Parse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Parse.ProtoReflect.Descriptor instead. -func (*Parse) Descriptor() ([]byte, []int) { +// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13} } -// Provision consumes source-code from a directory to produce resources. -// Exactly one of Plan or Apply must be provided in a single session. -type Provision struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *Provision) Reset() { - *x = Provision{} - if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *Metadata) GetCoderUrl() string { + if x != nil { + return x.CoderUrl } + return "" } -func (x *Provision) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Provision) ProtoMessage() {} - -func (x *Provision) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms +func (x *Metadata) GetWorkspaceTransition() WorkspaceTransition { + if x != nil { + return x.WorkspaceTransition } - return mi.MessageOf(x) -} - -// Deprecated: Use Provision.ProtoReflect.Descriptor instead. -func (*Provision) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} -} - -type Agent_Metadata struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"` - Interval int64 `protobuf:"varint,4,opt,name=interval,proto3" json:"interval,omitempty"` - Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"` + return WorkspaceTransition_START } -func (x *Agent_Metadata) Reset() { - *x = Agent_Metadata{} - if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *Metadata) GetWorkspaceName() string { + if x != nil { + return x.WorkspaceName } + return "" } -func (x *Agent_Metadata) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *Metadata) GetWorkspaceOwner() string { + if x != nil { + return x.WorkspaceOwner + } + return "" } -func (*Agent_Metadata) ProtoMessage() {} - -func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms +func (x *Metadata) GetWorkspaceId() string { + if x != nil { + return x.WorkspaceId } - return mi.MessageOf(x) + return "" } -// Deprecated: Use Agent_Metadata.ProtoReflect.Descriptor instead. -func (*Agent_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0} +func (x *Metadata) GetWorkspaceOwnerId() string { + if x != nil { + return x.WorkspaceOwnerId + } + return "" } -func (x *Agent_Metadata) GetKey() string { +func (x *Metadata) GetWorkspaceOwnerEmail() string { if x != nil { - return x.Key + return x.WorkspaceOwnerEmail } return "" } -func (x *Agent_Metadata) GetDisplayName() string { +func (x *Metadata) GetTemplateName() string { if x != nil { - return x.DisplayName + return x.TemplateName } return "" } -func (x *Agent_Metadata) GetScript() string { +func (x *Metadata) GetTemplateVersion() string { if x != nil { - return x.Script + return x.TemplateVersion } return "" } -func (x *Agent_Metadata) GetInterval() int64 { +func (x *Metadata) GetWorkspaceOwnerOidcAccessToken() string { if x != nil { - return x.Interval + return x.WorkspaceOwnerOidcAccessToken } - return 0 + return "" } -func (x *Agent_Metadata) GetTimeout() int64 { +func (x *Metadata) GetWorkspaceOwnerSessionToken() string { if x != nil { - return x.Timeout + return x.WorkspaceOwnerSessionToken } - return 0 + return "" } -type Resource_Metadata struct { +// Config represents execution configuration shared by all subsequent requests in the Session +type Config struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"` - IsNull bool `protobuf:"varint,4,opt,name=is_null,json=isNull,proto3" json:"is_null,omitempty"` + // template_source_archive is a tar of the template source files + TemplateSourceArchive []byte `protobuf:"bytes,1,opt,name=template_source_archive,json=templateSourceArchive,proto3" json:"template_source_archive,omitempty"` + // state is the provisioner state (if any) + State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + ProvisionerLogLevel string `protobuf:"bytes,3,opt,name=provisioner_log_level,json=provisionerLogLevel,proto3" json:"provisioner_log_level,omitempty"` } -func (x *Resource_Metadata) Reset() { - *x = Resource_Metadata{} +func (x *Config) Reset() { + *x = Config{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Resource_Metadata) String() string { +func (x *Config) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Resource_Metadata) ProtoMessage() {} +func (*Config) ProtoMessage() {} -func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1509,64 +1482,56 @@ func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead. -func (*Resource_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0} -} - -func (x *Resource_Metadata) GetKey() string { - if x != nil { - return x.Key - } - return "" +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} } -func (x *Resource_Metadata) GetValue() string { +func (x *Config) GetTemplateSourceArchive() []byte { if x != nil { - return x.Value + return x.TemplateSourceArchive } - return "" + return nil } -func (x *Resource_Metadata) GetSensitive() bool { +func (x *Config) GetState() []byte { if x != nil { - return x.Sensitive + return x.State } - return false + return nil } -func (x *Resource_Metadata) GetIsNull() bool { +func (x *Config) GetProvisionerLogLevel() string { if x != nil { - return x.IsNull + return x.ProvisionerLogLevel } - return false + return "" } -type Parse_Request struct { +// ParseRequest consumes source-code to produce inputs. +type ParseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"` } -func (x *Parse_Request) Reset() { - *x = Parse_Request{} +func (x *ParseRequest) Reset() { + *x = ParseRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Parse_Request) String() string { +func (x *ParseRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Parse_Request) ProtoMessage() {} +func (*ParseRequest) ProtoMessage() {} -func (x *Parse_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] +func (x *ParseRequest) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1577,43 +1542,39 @@ func (x *Parse_Request) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Parse_Request.ProtoReflect.Descriptor instead. -func (*Parse_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 0} +// Deprecated: Use ParseRequest.ProtoReflect.Descriptor instead. +func (*ParseRequest) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} } -func (x *Parse_Request) GetDirectory() string { - if x != nil { - return x.Directory - } - return "" -} - -type Parse_Complete struct { +// ParseComplete indicates a request to parse completed. +type ParseComplete struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - TemplateVariables []*TemplateVariable `protobuf:"bytes,1,rep,name=template_variables,json=templateVariables,proto3" json:"template_variables,omitempty"` + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + TemplateVariables []*TemplateVariable `protobuf:"bytes,2,rep,name=template_variables,json=templateVariables,proto3" json:"template_variables,omitempty"` + Readme []byte `protobuf:"bytes,3,opt,name=readme,proto3" json:"readme,omitempty"` } -func (x *Parse_Complete) Reset() { - *x = Parse_Complete{} +func (x *ParseComplete) Reset() { + *x = ParseComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Parse_Complete) String() string { +func (x *ParseComplete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Parse_Complete) ProtoMessage() {} +func (*ParseComplete) ProtoMessage() {} -func (x *Parse_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] +func (x *ParseComplete) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1624,47 +1585,61 @@ func (x *Parse_Complete) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Parse_Complete.ProtoReflect.Descriptor instead. -func (*Parse_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 1} +// Deprecated: Use ParseComplete.ProtoReflect.Descriptor instead. +func (*ParseComplete) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} } -func (x *Parse_Complete) GetTemplateVariables() []*TemplateVariable { +func (x *ParseComplete) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *ParseComplete) GetTemplateVariables() []*TemplateVariable { if x != nil { return x.TemplateVariables } return nil } -type Parse_Response struct { +func (x *ParseComplete) GetReadme() []byte { + if x != nil { + return x.Readme + } + return nil +} + +// PlanRequest asks the provisioner to plan what resources & parameters it will create +type PlanRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Types that are assignable to Type: - // - // *Parse_Response_Log - // *Parse_Response_Complete - Type isParse_Response_Type `protobuf_oneof:"type"` + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + RichParameterValues []*RichParameterValue `protobuf:"bytes,2,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"` + VariableValues []*VariableValue `protobuf:"bytes,3,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"` + GitAuthProviders []*GitAuthProvider `protobuf:"bytes,4,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` } -func (x *Parse_Response) Reset() { - *x = Parse_Response{} +func (x *PlanRequest) Reset() { + *x = PlanRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Parse_Response) String() string { +func (x *PlanRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Parse_Response) ProtoMessage() {} +func (*PlanRequest) ProtoMessage() {} -func (x *Parse_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] +func (x *PlanRequest) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1675,83 +1650,68 @@ func (x *Parse_Response) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Parse_Response.ProtoReflect.Descriptor instead. -func (*Parse_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 2} +// Deprecated: Use PlanRequest.ProtoReflect.Descriptor instead. +func (*PlanRequest) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17} } -func (m *Parse_Response) GetType() isParse_Response_Type { - if m != nil { - return m.Type +func (x *PlanRequest) GetMetadata() *Metadata { + if x != nil { + return x.Metadata } return nil } -func (x *Parse_Response) GetLog() *Log { - if x, ok := x.GetType().(*Parse_Response_Log); ok { - return x.Log +func (x *PlanRequest) GetRichParameterValues() []*RichParameterValue { + if x != nil { + return x.RichParameterValues } return nil } -func (x *Parse_Response) GetComplete() *Parse_Complete { - if x, ok := x.GetType().(*Parse_Response_Complete); ok { - return x.Complete +func (x *PlanRequest) GetVariableValues() []*VariableValue { + if x != nil { + return x.VariableValues } return nil } -type isParse_Response_Type interface { - isParse_Response_Type() -} - -type Parse_Response_Log struct { - Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"` -} - -type Parse_Response_Complete struct { - Complete *Parse_Complete `protobuf:"bytes,2,opt,name=complete,proto3,oneof"` +func (x *PlanRequest) GetGitAuthProviders() []*GitAuthProvider { + if x != nil { + return x.GitAuthProviders + } + return nil } -func (*Parse_Response_Log) isParse_Response_Type() {} - -func (*Parse_Response_Complete) isParse_Response_Type() {} - -type Provision_Metadata struct { +// PlanComplete indicates a request to plan completed. +type PlanComplete struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` - WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"` - WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` - WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"` - WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` - WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"` - WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"` - TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"` - TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"` - WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"` - WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"` + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Resources []*Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"` + Parameters []*RichParameter `protobuf:"bytes,3,rep,name=parameters,proto3" json:"parameters,omitempty"` + GitAuthProviders []string `protobuf:"bytes,4,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` } -func (x *Provision_Metadata) Reset() { - *x = Provision_Metadata{} +func (x *PlanComplete) Reset() { + *x = PlanComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Metadata) String() string { +func (x *PlanComplete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Metadata) ProtoMessage() {} +func (*PlanComplete) ProtoMessage() {} -func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] +func (x *PlanComplete) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1762,118 +1722,118 @@ func (x *Provision_Metadata) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Metadata.ProtoReflect.Descriptor instead. -func (*Provision_Metadata) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 0} +// Deprecated: Use PlanComplete.ProtoReflect.Descriptor instead. +func (*PlanComplete) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} } -func (x *Provision_Metadata) GetCoderUrl() string { +func (x *PlanComplete) GetError() string { if x != nil { - return x.CoderUrl + return x.Error } return "" } -func (x *Provision_Metadata) GetWorkspaceTransition() WorkspaceTransition { +func (x *PlanComplete) GetResources() []*Resource { if x != nil { - return x.WorkspaceTransition + return x.Resources } - return WorkspaceTransition_START + return nil } -func (x *Provision_Metadata) GetWorkspaceName() string { +func (x *PlanComplete) GetParameters() []*RichParameter { if x != nil { - return x.WorkspaceName + return x.Parameters } - return "" + return nil } -func (x *Provision_Metadata) GetWorkspaceOwner() string { +func (x *PlanComplete) GetGitAuthProviders() []string { if x != nil { - return x.WorkspaceOwner + return x.GitAuthProviders } - return "" + return nil } -func (x *Provision_Metadata) GetWorkspaceId() string { - if x != nil { - return x.WorkspaceId - } - return "" -} +// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response +// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. +type ApplyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func (x *Provision_Metadata) GetWorkspaceOwnerId() string { - if x != nil { - return x.WorkspaceOwnerId - } - return "" + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` } -func (x *Provision_Metadata) GetWorkspaceOwnerEmail() string { - if x != nil { - return x.WorkspaceOwnerEmail +func (x *ApplyRequest) Reset() { + *x = ApplyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - return "" } -func (x *Provision_Metadata) GetTemplateName() string { - if x != nil { - return x.TemplateName - } - return "" +func (x *ApplyRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Provision_Metadata) GetTemplateVersion() string { - if x != nil { - return x.TemplateVersion +func (*ApplyRequest) ProtoMessage() {} + +func (x *ApplyRequest) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *Provision_Metadata) GetWorkspaceOwnerOidcAccessToken() string { - if x != nil { - return x.WorkspaceOwnerOidcAccessToken - } - return "" +// Deprecated: Use ApplyRequest.ProtoReflect.Descriptor instead. +func (*ApplyRequest) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19} } -func (x *Provision_Metadata) GetWorkspaceOwnerSessionToken() string { +func (x *ApplyRequest) GetMetadata() *Metadata { if x != nil { - return x.WorkspaceOwnerSessionToken + return x.Metadata } - return "" + return nil } -// Config represents execution configuration shared by both Plan and -// Apply commands. -type Provision_Config struct { +// ApplyComplete indicates a request to apply completed. +type ApplyComplete struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"` - State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - Metadata *Provision_Metadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` - ProvisionerLogLevel string `protobuf:"bytes,4,opt,name=provisioner_log_level,json=provisionerLogLevel,proto3" json:"provisioner_log_level,omitempty"` + State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + Resources []*Resource `protobuf:"bytes,3,rep,name=resources,proto3" json:"resources,omitempty"` + Parameters []*RichParameter `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"` + GitAuthProviders []string `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` } -func (x *Provision_Config) Reset() { - *x = Provision_Config{} +func (x *ApplyComplete) Reset() { + *x = ApplyComplete{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Config) String() string { +func (x *ApplyComplete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Config) ProtoMessage() {} +func (*ApplyComplete) ProtoMessage() {} -func (x *Provision_Config) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] +func (x *ApplyComplete) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1884,67 +1844,70 @@ func (x *Provision_Config) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Config.ProtoReflect.Descriptor instead. -func (*Provision_Config) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 1} +// Deprecated: Use ApplyComplete.ProtoReflect.Descriptor instead. +func (*ApplyComplete) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} } -func (x *Provision_Config) GetDirectory() string { +func (x *ApplyComplete) GetState() []byte { if x != nil { - return x.Directory + return x.State + } + return nil +} + +func (x *ApplyComplete) GetError() string { + if x != nil { + return x.Error } return "" } -func (x *Provision_Config) GetState() []byte { +func (x *ApplyComplete) GetResources() []*Resource { if x != nil { - return x.State + return x.Resources } return nil } -func (x *Provision_Config) GetMetadata() *Provision_Metadata { +func (x *ApplyComplete) GetParameters() []*RichParameter { if x != nil { - return x.Metadata + return x.Parameters } return nil } -func (x *Provision_Config) GetProvisionerLogLevel() string { +func (x *ApplyComplete) GetGitAuthProviders() []string { if x != nil { - return x.ProvisionerLogLevel + return x.GitAuthProviders } - return "" + return nil } -type Provision_Plan struct { +// CancelRequest requests that the previous request be canceled gracefully. +type CancelRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - Config *Provision_Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - RichParameterValues []*RichParameterValue `protobuf:"bytes,3,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"` - VariableValues []*VariableValue `protobuf:"bytes,4,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"` - GitAuthProviders []*GitAuthProvider `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` } -func (x *Provision_Plan) Reset() { - *x = Provision_Plan{} +func (x *CancelRequest) Reset() { + *x = CancelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Plan) String() string { +func (x *CancelRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Plan) ProtoMessage() {} +func (*CancelRequest) ProtoMessage() {} -func (x *Provision_Plan) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] +func (x *CancelRequest) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1955,65 +1918,43 @@ func (x *Provision_Plan) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Plan.ProtoReflect.Descriptor instead. -func (*Provision_Plan) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 2} -} - -func (x *Provision_Plan) GetConfig() *Provision_Config { - if x != nil { - return x.Config - } - return nil -} - -func (x *Provision_Plan) GetRichParameterValues() []*RichParameterValue { - if x != nil { - return x.RichParameterValues - } - return nil -} - -func (x *Provision_Plan) GetVariableValues() []*VariableValue { - if x != nil { - return x.VariableValues - } - return nil -} - -func (x *Provision_Plan) GetGitAuthProviders() []*GitAuthProvider { - if x != nil { - return x.GitAuthProviders - } - return nil +// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead. +func (*CancelRequest) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21} } -type Provision_Apply struct { +type Request struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Config *Provision_Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - Plan []byte `protobuf:"bytes,2,opt,name=plan,proto3" json:"plan,omitempty"` + // Types that are assignable to Type: + // + // *Request_Config + // *Request_Parse + // *Request_Plan + // *Request_Apply + // *Request_Cancel + Type isRequest_Type `protobuf_oneof:"type"` } -func (x *Provision_Apply) Reset() { - *x = Provision_Apply{} +func (x *Request) Reset() { + *x = Request{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Apply) String() string { +func (x *Request) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Apply) ProtoMessage() {} +func (*Request) ProtoMessage() {} -func (x *Provision_Apply) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2024,93 +1965,118 @@ func (x *Provision_Apply) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Apply.ProtoReflect.Descriptor instead. -func (*Provision_Apply) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 3} +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22} } -func (x *Provision_Apply) GetConfig() *Provision_Config { - if x != nil { +func (m *Request) GetType() isRequest_Type { + if m != nil { + return m.Type + } + return nil +} + +func (x *Request) GetConfig() *Config { + if x, ok := x.GetType().(*Request_Config); ok { return x.Config } return nil } -func (x *Provision_Apply) GetPlan() []byte { - if x != nil { +func (x *Request) GetParse() *ParseRequest { + if x, ok := x.GetType().(*Request_Parse); ok { + return x.Parse + } + return nil +} + +func (x *Request) GetPlan() *PlanRequest { + if x, ok := x.GetType().(*Request_Plan); ok { return x.Plan } return nil } -type Provision_Cancel struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Request) GetApply() *ApplyRequest { + if x, ok := x.GetType().(*Request_Apply); ok { + return x.Apply + } + return nil } -func (x *Provision_Cancel) Reset() { - *x = Provision_Cancel{} - if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *Request) GetCancel() *CancelRequest { + if x, ok := x.GetType().(*Request_Cancel); ok { + return x.Cancel } + return nil } -func (x *Provision_Cancel) String() string { - return protoimpl.X.MessageStringOf(x) +type isRequest_Type interface { + isRequest_Type() +} + +type Request_Config struct { + Config *Config `protobuf:"bytes,1,opt,name=config,proto3,oneof"` } -func (*Provision_Cancel) ProtoMessage() {} +type Request_Parse struct { + Parse *ParseRequest `protobuf:"bytes,2,opt,name=parse,proto3,oneof"` +} -func (x *Provision_Cancel) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) +type Request_Plan struct { + Plan *PlanRequest `protobuf:"bytes,3,opt,name=plan,proto3,oneof"` +} + +type Request_Apply struct { + Apply *ApplyRequest `protobuf:"bytes,4,opt,name=apply,proto3,oneof"` } -// Deprecated: Use Provision_Cancel.ProtoReflect.Descriptor instead. -func (*Provision_Cancel) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 4} +type Request_Cancel struct { + Cancel *CancelRequest `protobuf:"bytes,5,opt,name=cancel,proto3,oneof"` } -type Provision_Request struct { +func (*Request_Config) isRequest_Type() {} + +func (*Request_Parse) isRequest_Type() {} + +func (*Request_Plan) isRequest_Type() {} + +func (*Request_Apply) isRequest_Type() {} + +func (*Request_Cancel) isRequest_Type() {} + +type Response struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to Type: // - // *Provision_Request_Plan - // *Provision_Request_Apply - // *Provision_Request_Cancel - Type isProvision_Request_Type `protobuf_oneof:"type"` + // *Response_Log + // *Response_Parse + // *Response_Plan + // *Response_Apply + Type isResponse_Type `protobuf_oneof:"type"` } -func (x *Provision_Request) Reset() { - *x = Provision_Request{} +func (x *Response) Reset() { + *x = Response{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Request) String() string { +func (x *Response) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Request) ProtoMessage() {} +func (*Response) ProtoMessage() {} -func (x *Provision_Request) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2121,91 +2087,103 @@ func (x *Provision_Request) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Request.ProtoReflect.Descriptor instead. -func (*Provision_Request) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 5} +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23} } -func (m *Provision_Request) GetType() isProvision_Request_Type { +func (m *Response) GetType() isResponse_Type { if m != nil { return m.Type } return nil } -func (x *Provision_Request) GetPlan() *Provision_Plan { - if x, ok := x.GetType().(*Provision_Request_Plan); ok { - return x.Plan +func (x *Response) GetLog() *Log { + if x, ok := x.GetType().(*Response_Log); ok { + return x.Log } return nil } -func (x *Provision_Request) GetApply() *Provision_Apply { - if x, ok := x.GetType().(*Provision_Request_Apply); ok { - return x.Apply +func (x *Response) GetParse() *ParseComplete { + if x, ok := x.GetType().(*Response_Parse); ok { + return x.Parse } return nil } -func (x *Provision_Request) GetCancel() *Provision_Cancel { - if x, ok := x.GetType().(*Provision_Request_Cancel); ok { - return x.Cancel +func (x *Response) GetPlan() *PlanComplete { + if x, ok := x.GetType().(*Response_Plan); ok { + return x.Plan + } + return nil +} + +func (x *Response) GetApply() *ApplyComplete { + if x, ok := x.GetType().(*Response_Apply); ok { + return x.Apply } return nil } -type isProvision_Request_Type interface { - isProvision_Request_Type() +type isResponse_Type interface { + isResponse_Type() +} + +type Response_Log struct { + Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"` } -type Provision_Request_Plan struct { - Plan *Provision_Plan `protobuf:"bytes,1,opt,name=plan,proto3,oneof"` +type Response_Parse struct { + Parse *ParseComplete `protobuf:"bytes,2,opt,name=parse,proto3,oneof"` } -type Provision_Request_Apply struct { - Apply *Provision_Apply `protobuf:"bytes,2,opt,name=apply,proto3,oneof"` +type Response_Plan struct { + Plan *PlanComplete `protobuf:"bytes,3,opt,name=plan,proto3,oneof"` } -type Provision_Request_Cancel struct { - Cancel *Provision_Cancel `protobuf:"bytes,3,opt,name=cancel,proto3,oneof"` +type Response_Apply struct { + Apply *ApplyComplete `protobuf:"bytes,4,opt,name=apply,proto3,oneof"` } -func (*Provision_Request_Plan) isProvision_Request_Type() {} +func (*Response_Log) isResponse_Type() {} -func (*Provision_Request_Apply) isProvision_Request_Type() {} +func (*Response_Parse) isResponse_Type() {} -func (*Provision_Request_Cancel) isProvision_Request_Type() {} +func (*Response_Plan) isResponse_Type() {} -type Provision_Complete struct { +func (*Response_Apply) isResponse_Type() {} + +type Agent_Metadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` - Resources []*Resource `protobuf:"bytes,3,rep,name=resources,proto3" json:"resources,omitempty"` - Parameters []*RichParameter `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"` - GitAuthProviders []string `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"` - Plan []byte `protobuf:"bytes,6,opt,name=plan,proto3" json:"plan,omitempty"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"` + Interval int64 `protobuf:"varint,4,opt,name=interval,proto3" json:"interval,omitempty"` + Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"` } -func (x *Provision_Complete) Reset() { - *x = Provision_Complete{} +func (x *Agent_Metadata) Reset() { + *x = Agent_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Complete) String() string { +func (x *Agent_Metadata) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Complete) ProtoMessage() {} +func (*Agent_Metadata) ProtoMessage() {} -func (x *Provision_Complete) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27] +func (x *Agent_Metadata) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2216,82 +2194,74 @@ func (x *Provision_Complete) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Complete.ProtoReflect.Descriptor instead. -func (*Provision_Complete) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 6} -} - -func (x *Provision_Complete) GetState() []byte { - if x != nil { - return x.State - } - return nil +// Deprecated: Use Agent_Metadata.ProtoReflect.Descriptor instead. +func (*Agent_Metadata) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0} } -func (x *Provision_Complete) GetError() string { +func (x *Agent_Metadata) GetKey() string { if x != nil { - return x.Error + return x.Key } return "" } -func (x *Provision_Complete) GetResources() []*Resource { +func (x *Agent_Metadata) GetDisplayName() string { if x != nil { - return x.Resources + return x.DisplayName } - return nil + return "" } -func (x *Provision_Complete) GetParameters() []*RichParameter { +func (x *Agent_Metadata) GetScript() string { if x != nil { - return x.Parameters + return x.Script } - return nil + return "" } -func (x *Provision_Complete) GetGitAuthProviders() []string { +func (x *Agent_Metadata) GetInterval() int64 { if x != nil { - return x.GitAuthProviders + return x.Interval } - return nil + return 0 } -func (x *Provision_Complete) GetPlan() []byte { +func (x *Agent_Metadata) GetTimeout() int64 { if x != nil { - return x.Plan + return x.Timeout } - return nil + return 0 } -type Provision_Response struct { +type Resource_Metadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Types that are assignable to Type: - // - // *Provision_Response_Log - // *Provision_Response_Complete - Type isProvision_Response_Type `protobuf_oneof:"type"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + IsNull bool `protobuf:"varint,4,opt,name=is_null,json=isNull,proto3" json:"is_null,omitempty"` } -func (x *Provision_Response) Reset() { - *x = Provision_Response{} +func (x *Resource_Metadata) Reset() { + *x = Resource_Metadata{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *Provision_Response) String() string { +func (x *Resource_Metadata) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Provision_Response) ProtoMessage() {} +func (*Resource_Metadata) ProtoMessage() {} -func (x *Provision_Response) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28] +func (x *Resource_Metadata) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2302,48 +2272,39 @@ func (x *Provision_Response) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Provision_Response.ProtoReflect.Descriptor instead. -func (*Provision_Response) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 7} +// Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead. +func (*Resource_Metadata) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0} } -func (m *Provision_Response) GetType() isProvision_Response_Type { - if m != nil { - return m.Type +func (x *Resource_Metadata) GetKey() string { + if x != nil { + return x.Key } - return nil + return "" } -func (x *Provision_Response) GetLog() *Log { - if x, ok := x.GetType().(*Provision_Response_Log); ok { - return x.Log +func (x *Resource_Metadata) GetValue() string { + if x != nil { + return x.Value } - return nil + return "" } -func (x *Provision_Response) GetComplete() *Provision_Complete { - if x, ok := x.GetType().(*Provision_Response_Complete); ok { - return x.Complete +func (x *Resource_Metadata) GetSensitive() bool { + if x != nil { + return x.Sensitive } - return nil -} - -type isProvision_Response_Type interface { - isProvision_Response_Type() -} - -type Provision_Response_Log struct { - Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"` + return false } -type Provision_Response_Complete struct { - Complete *Provision_Complete `protobuf:"bytes,2,opt,name=complete,proto3,oneof"` +func (x *Resource_Metadata) GetIsNull() bool { + if x != nil { + return x.IsNull + } + return false } -func (*Provision_Response_Log) isProvision_Response_Type() {} - -func (*Provision_Response_Complete) isProvision_Response_Type() {} - var File_provisionersdk_proto_provisioner_proto protoreflect.FileDescriptor var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ @@ -2543,154 +2504,161 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0x85, 0x02, - 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, - 0x1a, 0x5e, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x12, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, - 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, - 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, - 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x91, 0x0d, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x1a, 0xae, 0x04, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, - 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0xad, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, - 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x1a, 0xa9, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x35, 0x0a, - 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, - 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xae, 0x04, + 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, + 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, + 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, + 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8a, + 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8b, 0x01, 0x0a, 0x0d, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, + 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x22, 0xa6, 0x02, 0x0a, 0x0b, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15, + 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, + 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x22, 0xc3, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, + 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xda, 0x01, 0x0a, 0x0d, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, - 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a, - 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, - 0x1a, 0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0xb3, - 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, 0x6c, - 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, 0x0a, - 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, - 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x1a, 0xe9, 0x01, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, - 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, - 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, - 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, - 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, - 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, - 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, - 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, - 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, - 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, - 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, - 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, - 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, - 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, - 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, - 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, - 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, + 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, + 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, + 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, + 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, + 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, + 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, + 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, + 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, + 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, + 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, + 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, + 0x59, 0x10, 0x02, 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, + 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2706,7 +2674,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: provisioner.LogLevel (AppSharingLevel)(0), // 1: provisioner.AppSharingLevel @@ -2724,59 +2692,58 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*App)(nil), // 13: provisioner.App (*Healthcheck)(nil), // 14: provisioner.Healthcheck (*Resource)(nil), // 15: provisioner.Resource - (*Parse)(nil), // 16: provisioner.Parse - (*Provision)(nil), // 17: provisioner.Provision - (*Agent_Metadata)(nil), // 18: provisioner.Agent.Metadata - nil, // 19: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 20: provisioner.Resource.Metadata - (*Parse_Request)(nil), // 21: provisioner.Parse.Request - (*Parse_Complete)(nil), // 22: provisioner.Parse.Complete - (*Parse_Response)(nil), // 23: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 24: provisioner.Provision.Metadata - (*Provision_Config)(nil), // 25: provisioner.Provision.Config - (*Provision_Plan)(nil), // 26: provisioner.Provision.Plan - (*Provision_Apply)(nil), // 27: provisioner.Provision.Apply - (*Provision_Cancel)(nil), // 28: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 29: provisioner.Provision.Request - (*Provision_Complete)(nil), // 30: provisioner.Provision.Complete - (*Provision_Response)(nil), // 31: provisioner.Provision.Response + (*Metadata)(nil), // 16: provisioner.Metadata + (*Config)(nil), // 17: provisioner.Config + (*ParseRequest)(nil), // 18: provisioner.ParseRequest + (*ParseComplete)(nil), // 19: provisioner.ParseComplete + (*PlanRequest)(nil), // 20: provisioner.PlanRequest + (*PlanComplete)(nil), // 21: provisioner.PlanComplete + (*ApplyRequest)(nil), // 22: provisioner.ApplyRequest + (*ApplyComplete)(nil), // 23: provisioner.ApplyComplete + (*CancelRequest)(nil), // 24: provisioner.CancelRequest + (*Request)(nil), // 25: provisioner.Request + (*Response)(nil), // 26: provisioner.Response + (*Agent_Metadata)(nil), // 27: provisioner.Agent.Metadata + nil, // 28: provisioner.Agent.EnvEntry + (*Resource_Metadata)(nil), // 29: provisioner.Resource.Metadata } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 5, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption 0, // 1: provisioner.Log.level:type_name -> provisioner.LogLevel - 19, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry + 28, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry 13, // 3: provisioner.Agent.apps:type_name -> provisioner.App - 18, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata + 27, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata 14, // 5: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck 1, // 6: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel 12, // 7: provisioner.Resource.agents:type_name -> provisioner.Agent - 20, // 8: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 4, // 9: provisioner.Parse.Complete.template_variables:type_name -> provisioner.TemplateVariable - 9, // 10: provisioner.Parse.Response.log:type_name -> provisioner.Log - 22, // 11: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete - 2, // 12: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 24, // 13: provisioner.Provision.Config.metadata:type_name -> provisioner.Provision.Metadata - 25, // 14: provisioner.Provision.Plan.config:type_name -> provisioner.Provision.Config - 7, // 15: provisioner.Provision.Plan.rich_parameter_values:type_name -> provisioner.RichParameterValue - 8, // 16: provisioner.Provision.Plan.variable_values:type_name -> provisioner.VariableValue - 11, // 17: provisioner.Provision.Plan.git_auth_providers:type_name -> provisioner.GitAuthProvider - 25, // 18: provisioner.Provision.Apply.config:type_name -> provisioner.Provision.Config - 26, // 19: provisioner.Provision.Request.plan:type_name -> provisioner.Provision.Plan - 27, // 20: provisioner.Provision.Request.apply:type_name -> provisioner.Provision.Apply - 28, // 21: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel - 15, // 22: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource - 6, // 23: provisioner.Provision.Complete.parameters:type_name -> provisioner.RichParameter - 9, // 24: provisioner.Provision.Response.log:type_name -> provisioner.Log - 30, // 25: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete - 21, // 26: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request - 29, // 27: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request - 23, // 28: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response - 31, // 29: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response - 28, // [28:30] is the sub-list for method output_type - 26, // [26:28] is the sub-list for method input_type - 26, // [26:26] is the sub-list for extension type_name - 26, // [26:26] is the sub-list for extension extendee - 0, // [0:26] is the sub-list for field type_name + 29, // 8: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata + 2, // 9: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition + 4, // 10: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable + 16, // 11: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata + 7, // 12: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue + 8, // 13: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue + 11, // 14: provisioner.PlanRequest.git_auth_providers:type_name -> provisioner.GitAuthProvider + 15, // 15: provisioner.PlanComplete.resources:type_name -> provisioner.Resource + 6, // 16: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter + 16, // 17: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata + 15, // 18: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource + 6, // 19: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter + 17, // 20: provisioner.Request.config:type_name -> provisioner.Config + 18, // 21: provisioner.Request.parse:type_name -> provisioner.ParseRequest + 20, // 22: provisioner.Request.plan:type_name -> provisioner.PlanRequest + 22, // 23: provisioner.Request.apply:type_name -> provisioner.ApplyRequest + 24, // 24: provisioner.Request.cancel:type_name -> provisioner.CancelRequest + 9, // 25: provisioner.Response.log:type_name -> provisioner.Log + 19, // 26: provisioner.Response.parse:type_name -> provisioner.ParseComplete + 21, // 27: provisioner.Response.plan:type_name -> provisioner.PlanComplete + 23, // 28: provisioner.Response.apply:type_name -> provisioner.ApplyComplete + 25, // 29: provisioner.Provisioner.Session:input_type -> provisioner.Request + 26, // 30: provisioner.Provisioner.Session:output_type -> provisioner.Response + 30, // [30:31] is the sub-list for method output_type + 29, // [29:30] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -2942,7 +2909,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -2954,7 +2921,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -2966,7 +2933,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Agent_Metadata); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -2977,8 +2944,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Resource_Metadata); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -2989,8 +2956,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Request); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -3001,8 +2968,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Complete); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -3013,8 +2980,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Parse_Response); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -3025,8 +2992,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Metadata); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApplyComplete); i { case 0: return &v.state case 1: @@ -3037,8 +3004,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Config); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CancelRequest); i { case 0: return &v.state case 1: @@ -3049,8 +3016,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Plan); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Request); i { case 0: return &v.state case 1: @@ -3061,8 +3028,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Apply); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); i { case 0: return &v.state case 1: @@ -3073,8 +3040,8 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Cancel); i { + file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Agent_Metadata); i { case 0: return &v.state case 1: @@ -3086,31 +3053,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_provisionersdk_proto_provisioner_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Complete); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_provisionersdk_proto_provisioner_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Provision_Response); i { + switch v := v.(*Resource_Metadata); i { case 0: return &v.state case 1: @@ -3127,18 +3070,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[20].OneofWrappers = []interface{}{ - (*Parse_Response_Log)(nil), - (*Parse_Response_Complete)(nil), - } - file_provisionersdk_proto_provisioner_proto_msgTypes[26].OneofWrappers = []interface{}{ - (*Provision_Request_Plan)(nil), - (*Provision_Request_Apply)(nil), - (*Provision_Request_Cancel)(nil), + file_provisionersdk_proto_provisioner_proto_msgTypes[22].OneofWrappers = []interface{}{ + (*Request_Config)(nil), + (*Request_Parse)(nil), + (*Request_Plan)(nil), + (*Request_Apply)(nil), + (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[28].OneofWrappers = []interface{}{ - (*Provision_Response_Log)(nil), - (*Provision_Response_Complete)(nil), + file_provisionersdk_proto_provisioner_proto_msgTypes[23].OneofWrappers = []interface{}{ + (*Response_Log)(nil), + (*Response_Parse)(nil), + (*Response_Plan)(nil), + (*Response_Apply)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -3146,7 +3089,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 3, - NumMessages: 29, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 789735782f37a..5670fbde2675d 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -82,8 +82,8 @@ message InstanceIdentityAuth { } message GitAuthProvider { - string id = 1; - string access_token = 2; + string id = 1; + string access_token = 2; } // Agent represents a running agent on the workspace. @@ -110,15 +110,15 @@ message Agent { string token = 9; string instance_id = 10; } - int32 connection_timeout_seconds = 11; - string troubleshooting_url = 12; - string motd_file = 13; - // Field 14 was bool login_before_ready = 14, now removed. - int32 startup_script_timeout_seconds = 15; - string shutdown_script = 16; - int32 shutdown_script_timeout_seconds = 17; + int32 connection_timeout_seconds = 11; + string troubleshooting_url = 12; + string motd_file = 13; + // Field 14 was bool login_before_ready = 14, now removed. + int32 startup_script_timeout_seconds = 15; + string shutdown_script = 16; + int32 shutdown_script_timeout_seconds = 17; repeated Metadata metadata = 18; - string startup_script_behavior = 19; + string startup_script_behavior = 19; } enum AppSharingLevel { @@ -168,96 +168,111 @@ message Resource { int32 daily_cost = 8; } -// Parse consumes source-code from a directory to produce inputs. -message Parse { - message Request { - string directory = 1; - } - message Complete { - reserved 2; - - repeated TemplateVariable template_variables = 1; - } - message Response { - oneof type { - Log log = 1; - Complete complete = 2; - } - } -} - +// WorkspaceTransition is the desired outcome of a build enum WorkspaceTransition { START = 0; STOP = 1; DESTROY = 2; } -// Provision consumes source-code from a directory to produce resources. -// Exactly one of Plan or Apply must be provided in a single session. -message Provision { - message Metadata { - string coder_url = 1; - WorkspaceTransition workspace_transition = 2; - string workspace_name = 3; - string workspace_owner = 4; - string workspace_id = 5; - string workspace_owner_id = 6; - string workspace_owner_email = 7; - string template_name = 8; - string template_version = 9; - string workspace_owner_oidc_access_token = 10; - string workspace_owner_session_token = 11; - } +// Metadata is information about a workspace used in the execution of a build +message Metadata { + string coder_url = 1; + WorkspaceTransition workspace_transition = 2; + string workspace_name = 3; + string workspace_owner = 4; + string workspace_id = 5; + string workspace_owner_id = 6; + string workspace_owner_email = 7; + string template_name = 8; + string template_version = 9; + string workspace_owner_oidc_access_token = 10; + string workspace_owner_session_token = 11; +} - // Config represents execution configuration shared by both Plan and - // Apply commands. - message Config { - string directory = 1; - bytes state = 2; - Metadata metadata = 3; +// Config represents execution configuration shared by all subsequent requests in the Session +message Config { + // template_source_archive is a tar of the template source files + bytes template_source_archive = 1; + // state is the provisioner state (if any) + bytes state = 2; + string provisioner_log_level = 3; +} - string provisioner_log_level = 4; - } +// ParseRequest consumes source-code to produce inputs. +message ParseRequest { +} - message Plan { - reserved 2; +// ParseComplete indicates a request to parse completed. +message ParseComplete { + string error = 1; + repeated TemplateVariable template_variables = 2; + bytes readme = 3; +} - Config config = 1; - repeated RichParameterValue rich_parameter_values = 3; - repeated VariableValue variable_values = 4; - repeated GitAuthProvider git_auth_providers = 5; - } +// PlanRequest asks the provisioner to plan what resources & parameters it will create +message PlanRequest { + Metadata metadata = 1; + repeated RichParameterValue rich_parameter_values = 2; + repeated VariableValue variable_values = 3; + repeated GitAuthProvider git_auth_providers = 4; +} + +// PlanComplete indicates a request to plan completed. +message PlanComplete { + string error = 1; + repeated Resource resources = 2; + repeated RichParameter parameters = 3; + repeated string git_auth_providers = 4; +} + +// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response +// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. +message ApplyRequest { + Metadata metadata = 1; +} - message Apply { +// ApplyComplete indicates a request to apply completed. +message ApplyComplete { + bytes state = 1; + string error = 2; + repeated Resource resources = 3; + repeated RichParameter parameters = 4; + repeated string git_auth_providers = 5; +} + +// CancelRequest requests that the previous request be canceled gracefully. +message CancelRequest {} + +message Request { + oneof type { Config config = 1; - bytes plan = 2; + ParseRequest parse = 2; + PlanRequest plan = 3; + ApplyRequest apply = 4; + CancelRequest cancel = 5; } +} - message Cancel {} - message Request { - oneof type { - Plan plan = 1; - Apply apply = 2; - Cancel cancel = 3; - } - } - message Complete { - bytes state = 1; - string error = 2; - repeated Resource resources = 3; - repeated RichParameter parameters = 4; - repeated string git_auth_providers = 5; - bytes plan = 6; - } - message Response { - oneof type { - Log log = 1; - Complete complete = 2; - } +message Response { + oneof type { + Log log = 1; + ParseComplete parse = 2; + PlanComplete plan = 3; + ApplyComplete apply = 4; } } service Provisioner { - rpc Parse(Parse.Request) returns (stream Parse.Response); - rpc Provision(stream Provision.Request) returns (stream Provision.Response); + // Session represents provisioning a single template import or workspace. The daemon always sends Config followed + // by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream + // of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete, + // ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan, + // and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may + // request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded. + // + // The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest, + // PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request + // that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest. + rpc Session(stream Request) returns (stream Response); } diff --git a/provisionersdk/proto/provisioner_drpc.pb.go b/provisionersdk/proto/provisioner_drpc.pb.go index d8b40060cd376..de310e779dcaa 100644 --- a/provisionersdk/proto/provisioner_drpc.pb.go +++ b/provisionersdk/proto/provisioner_drpc.pb.go @@ -38,8 +38,7 @@ func (drpcEncoding_File_provisionersdk_proto_provisioner_proto) JSONUnmarshal(bu type DRPCProvisionerClient interface { DRPCConn() drpc.Conn - Parse(ctx context.Context, in *Parse_Request) (DRPCProvisioner_ParseClient, error) - Provision(ctx context.Context) (DRPCProvisioner_ProvisionClient, error) + Session(ctx context.Context) (DRPCProvisioner_SessionClient, error) } type drpcProvisionerClient struct { @@ -52,123 +51,69 @@ func NewDRPCProvisionerClient(cc drpc.Conn) DRPCProvisionerClient { func (c *drpcProvisionerClient) DRPCConn() drpc.Conn { return c.cc } -func (c *drpcProvisionerClient) Parse(ctx context.Context, in *Parse_Request) (DRPCProvisioner_ParseClient, error) { - stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Parse", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) +func (c *drpcProvisionerClient) Session(ctx context.Context) (DRPCProvisioner_SessionClient, error) { + stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Session", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) if err != nil { return nil, err } - x := &drpcProvisioner_ParseClient{stream} - if err := x.MsgSend(in, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil { - return nil, err - } - if err := x.CloseSend(); err != nil { - return nil, err - } + x := &drpcProvisioner_SessionClient{stream} return x, nil } -type DRPCProvisioner_ParseClient interface { +type DRPCProvisioner_SessionClient interface { drpc.Stream - Recv() (*Parse_Response, error) + Send(*Request) error + Recv() (*Response, error) } -type drpcProvisioner_ParseClient struct { +type drpcProvisioner_SessionClient struct { drpc.Stream } -func (x *drpcProvisioner_ParseClient) GetStream() drpc.Stream { +func (x *drpcProvisioner_SessionClient) GetStream() drpc.Stream { return x.Stream } -func (x *drpcProvisioner_ParseClient) Recv() (*Parse_Response, error) { - m := new(Parse_Response) - if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil { - return nil, err - } - return m, nil -} - -func (x *drpcProvisioner_ParseClient) RecvMsg(m *Parse_Response) error { - return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) -} - -func (c *drpcProvisionerClient) Provision(ctx context.Context) (DRPCProvisioner_ProvisionClient, error) { - stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Provision", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) - if err != nil { - return nil, err - } - x := &drpcProvisioner_ProvisionClient{stream} - return x, nil -} - -type DRPCProvisioner_ProvisionClient interface { - drpc.Stream - Send(*Provision_Request) error - Recv() (*Provision_Response, error) -} - -type drpcProvisioner_ProvisionClient struct { - drpc.Stream -} - -func (x *drpcProvisioner_ProvisionClient) GetStream() drpc.Stream { - return x.Stream -} - -func (x *drpcProvisioner_ProvisionClient) Send(m *Provision_Request) error { +func (x *drpcProvisioner_SessionClient) Send(m *Request) error { return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) } -func (x *drpcProvisioner_ProvisionClient) Recv() (*Provision_Response, error) { - m := new(Provision_Response) +func (x *drpcProvisioner_SessionClient) Recv() (*Response, error) { + m := new(Response) if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil { return nil, err } return m, nil } -func (x *drpcProvisioner_ProvisionClient) RecvMsg(m *Provision_Response) error { +func (x *drpcProvisioner_SessionClient) RecvMsg(m *Response) error { return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) } type DRPCProvisionerServer interface { - Parse(*Parse_Request, DRPCProvisioner_ParseStream) error - Provision(DRPCProvisioner_ProvisionStream) error + Session(DRPCProvisioner_SessionStream) error } type DRPCProvisionerUnimplementedServer struct{} -func (s *DRPCProvisionerUnimplementedServer) Parse(*Parse_Request, DRPCProvisioner_ParseStream) error { - return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) -} - -func (s *DRPCProvisionerUnimplementedServer) Provision(DRPCProvisioner_ProvisionStream) error { +func (s *DRPCProvisionerUnimplementedServer) Session(DRPCProvisioner_SessionStream) error { return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } type DRPCProvisionerDescription struct{} -func (DRPCProvisionerDescription) NumMethods() int { return 2 } +func (DRPCProvisionerDescription) NumMethods() int { return 1 } func (DRPCProvisionerDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: - return "/provisioner.Provisioner/Parse", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}, + return "/provisioner.Provisioner/Session", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return nil, srv.(DRPCProvisionerServer). - Parse( - in1.(*Parse_Request), - &drpcProvisioner_ParseStream{in2.(drpc.Stream)}, + Session( + &drpcProvisioner_SessionStream{in1.(drpc.Stream)}, ) - }, DRPCProvisionerServer.Parse, true - case 1: - return "/provisioner.Provisioner/Provision", drpcEncoding_File_provisionersdk_proto_provisioner_proto{}, - func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { - return nil, srv.(DRPCProvisionerServer). - Provision( - &drpcProvisioner_ProvisionStream{in1.(drpc.Stream)}, - ) - }, DRPCProvisionerServer.Provision, true + }, DRPCProvisionerServer.Session, true default: return "", nil, nil, nil, false } @@ -178,41 +123,28 @@ func DRPCRegisterProvisioner(mux drpc.Mux, impl DRPCProvisionerServer) error { return mux.Register(impl, DRPCProvisionerDescription{}) } -type DRPCProvisioner_ParseStream interface { - drpc.Stream - Send(*Parse_Response) error -} - -type drpcProvisioner_ParseStream struct { - drpc.Stream -} - -func (x *drpcProvisioner_ParseStream) Send(m *Parse_Response) error { - return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) -} - -type DRPCProvisioner_ProvisionStream interface { +type DRPCProvisioner_SessionStream interface { drpc.Stream - Send(*Provision_Response) error - Recv() (*Provision_Request, error) + Send(*Response) error + Recv() (*Request, error) } -type drpcProvisioner_ProvisionStream struct { +type drpcProvisioner_SessionStream struct { drpc.Stream } -func (x *drpcProvisioner_ProvisionStream) Send(m *Provision_Response) error { +func (x *drpcProvisioner_SessionStream) Send(m *Response) error { return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) } -func (x *drpcProvisioner_ProvisionStream) Recv() (*Provision_Request, error) { - m := new(Provision_Request) +func (x *drpcProvisioner_SessionStream) Recv() (*Request, error) { + m := new(Request) if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil { return nil, err } return m, nil } -func (x *drpcProvisioner_ProvisionStream) RecvMsg(m *Provision_Request) error { +func (x *drpcProvisioner_SessionStream) RecvMsg(m *Request) error { return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}) } diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go index ea3364b6054fe..924c7ad013982 100644 --- a/provisionersdk/serve.go +++ b/provisionersdk/serve.go @@ -13,6 +13,8 @@ import ( "storj.io/drpc/drpcmux" "storj.io/drpc/drpcserver" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/provisionersdk/proto" ) @@ -20,11 +22,19 @@ import ( // ServeOptions are configurations to serve a provisioner. type ServeOptions struct { // Conn specifies a custom transport to serve the dRPC connection. - Listener net.Listener + Listener net.Listener + Logger slog.Logger + WorkDirectory string +} + +type Server interface { + Parse(s *Session, r *proto.ParseRequest, canceledOrComplete <-chan struct{}) *proto.ParseComplete + Plan(s *Session, r *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete + Apply(s *Session, r *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete } // Serve starts a dRPC connection for the provisioner and transport provided. -func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *ServeOptions) error { +func Serve(ctx context.Context, server Server, options *ServeOptions) error { if options == nil { options = &ServeOptions{} } @@ -45,11 +55,22 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser }() options.Listener = stdio } + if options.WorkDirectory == "" { + var err error + options.WorkDirectory, err = os.MkdirTemp("", "coderprovisioner") + if err != nil { + return xerrors.Errorf("failed to init temp work dir: %w", err) + } + } // dRPC is a drop-in replacement for gRPC with less generated code, and faster transports. // See: https://www.storj.io/blog/introducing-drpc-our-replacement-for-grpc mux := drpcmux.New() - err := proto.DRPCRegisterProvisioner(mux, server) + ps := &protoServer{ + server: server, + opts: *options, + } + err := proto.DRPCRegisterProvisioner(mux, ps) if err != nil { return xerrors.Errorf("register provisioner: %w", err) } diff --git a/provisionersdk/serve_test.go b/provisionersdk/serve_test.go index dedf891889d8a..baa5d2ba62b28 100644 --- a/provisionersdk/serve_test.go +++ b/provisionersdk/serve_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" - "storj.io/drpc/drpcerr" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" @@ -28,17 +27,37 @@ func TestProvisionerSDK(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() go func() { - err := provisionersdk.Serve(ctx, &proto.DRPCProvisionerUnimplementedServer{}, &provisionersdk.ServeOptions{ - Listener: server, + err := provisionersdk.Serve(ctx, unimplementedServer{}, &provisionersdk.ServeOptions{ + Listener: server, + WorkDirectory: t.TempDir(), }) assert.NoError(t, err) }() api := proto.NewDRPCProvisionerClient(client) - stream, err := api.Parse(context.Background(), &proto.Parse_Request{}) + s, err := api.Session(ctx) require.NoError(t, err) - _, err = stream.Recv() - require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err))) + err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}}) + require.NoError(t, err) + + err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}}) + require.NoError(t, err) + msg, err := s.Recv() + require.NoError(t, err) + require.Equal(t, "unimplemented", msg.GetParse().GetError()) + + err = s.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}}) + require.NoError(t, err) + msg, err = s.Recv() + require.NoError(t, err) + // Plan has no error so that we're allowed to run Apply + require.Equal(t, "", msg.GetPlan().GetError()) + + err = s.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}}) + require.NoError(t, err) + msg, err = s.Recv() + require.NoError(t, err) + require.Equal(t, "unimplemented", msg.GetApply().GetError()) }) t.Run("ServeClosedPipe", func(t *testing.T) { @@ -47,9 +66,24 @@ func TestProvisionerSDK(t *testing.T) { _ = client.Close() _ = server.Close() - err := provisionersdk.Serve(context.Background(), &proto.DRPCProvisionerUnimplementedServer{}, &provisionersdk.ServeOptions{ - Listener: server, + err := provisionersdk.Serve(context.Background(), unimplementedServer{}, &provisionersdk.ServeOptions{ + Listener: server, + WorkDirectory: t.TempDir(), }) require.NoError(t, err) }) } + +type unimplementedServer struct{} + +func (unimplementedServer) Parse(_ *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete { + return &proto.ParseComplete{Error: "unimplemented"} +} + +func (unimplementedServer) Plan(_ *provisionersdk.Session, _ *proto.PlanRequest, _ <-chan struct{}) *proto.PlanComplete { + return &proto.PlanComplete{} +} + +func (unimplementedServer) Apply(_ *provisionersdk.Session, _ *proto.ApplyRequest, _ <-chan struct{}) *proto.ApplyComplete { + return &proto.ApplyComplete{Error: "unimplemented"} +} diff --git a/provisionersdk/session.go b/provisionersdk/session.go new file mode 100644 index 0000000000000..dfcd981ce77f5 --- /dev/null +++ b/provisionersdk/session.go @@ -0,0 +1,318 @@ +package provisionersdk + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/provisionersdk/proto" +) + +// ReadmeFile is the location we look for to extract documentation from template +// versions. +const ReadmeFile = "README.md" + +// protoServer is a wrapper that translates the dRPC protocol into a Session with method calls into the Server. +type protoServer struct { + server Server + opts ServeOptions +} + +func (p *protoServer) Session(stream proto.DRPCProvisioner_SessionStream) error { + sessID := uuid.New().String() + s := &Session{ + Logger: p.opts.Logger.With(slog.F("session_id", sessID)), + stream: stream, + server: p.server, + } + sessDir := fmt.Sprintf("Session%s", sessID) + s.WorkDirectory = filepath.Join(p.opts.WorkDirectory, sessDir) + err := os.MkdirAll(s.WorkDirectory, 0o700) + if err != nil { + return xerrors.Errorf("create work directory %q: %w", s.WorkDirectory, err) + } + defer func() { + var err error + // Cleanup the work directory after execution. + for attempt := 0; attempt < 5; attempt++ { + err = os.RemoveAll(s.WorkDirectory) + if err != nil { + // On Windows, open files cannot be removed. + // When the provisioner daemon is shutting down, + // it may take a few milliseconds for processes to exit. + // See: https://github.com/golang/go/issues/50510 + s.Logger.Debug(s.Context(), "failed to clean work directory; trying again", slog.Error(err)) + time.Sleep(250 * time.Millisecond) + continue + } + s.Logger.Debug(s.Context(), "cleaned up work directory") + return + } + s.Logger.Error(s.Context(), "failed to clean up work directory after multiple attempts", + slog.F("path", s.WorkDirectory), slog.Error(err)) + }() + req, err := stream.Recv() + if err != nil { + return xerrors.Errorf("receive config: %w", err) + } + config := req.GetConfig() + if config == nil { + return xerrors.New("first request must be Config") + } + s.Config = config + if s.Config.ProvisionerLogLevel != "" { + s.logLevel = proto.LogLevel_value[strings.ToUpper(s.Config.ProvisionerLogLevel)] + } + + err = s.extractArchive() + if err != nil { + return xerrors.Errorf("extract archive: %w", err) + } + return s.handleRequests() +} + +func (s *Session) requestReader(done <-chan struct{}) <-chan *proto.Request { + ch := make(chan *proto.Request) + go func() { + defer close(ch) + for { + req, err := s.stream.Recv() + if err != nil { + s.Logger.Info(s.Context(), "recv done on Session", slog.Error(err)) + return + } + select { + case ch <- req: + continue + case <-done: + return + } + } + }() + return ch +} + +func (s *Session) handleRequests() error { + done := make(chan struct{}) + defer close(done) + requests := s.requestReader(done) + planned := false + for req := range requests { + if req.GetCancel() != nil { + s.Logger.Warn(s.Context(), "ignoring cancel before request or after complete") + continue + } + resp := &proto.Response{} + if parse := req.GetParse(); parse != nil { + r := &request[*proto.ParseRequest, *proto.ParseComplete]{ + req: parse, + session: s, + serverFn: s.server.Parse, + cancels: requests, + } + complete, err := r.do() + if err != nil { + return err + } + // Handle README centrally, so that individual provisioners don't need to mess with it. + readme, err := os.ReadFile(filepath.Join(s.WorkDirectory, ReadmeFile)) + if err == nil { + complete.Readme = readme + } else { + s.Logger.Debug(s.Context(), "failed to parse readme (missing ok)", slog.Error(err)) + } + resp.Type = &proto.Response_Parse{Parse: complete} + } + if plan := req.GetPlan(); plan != nil { + r := &request[*proto.PlanRequest, *proto.PlanComplete]{ + req: plan, + session: s, + serverFn: s.server.Plan, + cancels: requests, + } + complete, err := r.do() + if err != nil { + return err + } + resp.Type = &proto.Response_Plan{Plan: complete} + if complete.Error == "" { + planned = true + } + } + if apply := req.GetApply(); apply != nil { + if !planned { + return xerrors.New("cannot apply before successful plan") + } + r := &request[*proto.ApplyRequest, *proto.ApplyComplete]{ + req: apply, + session: s, + serverFn: s.server.Apply, + cancels: requests, + } + complete, err := r.do() + if err != nil { + return err + } + resp.Type = &proto.Response_Apply{Apply: complete} + } + err := s.stream.Send(resp) + if err != nil { + return xerrors.Errorf("send response: %w", err) + } + } + return nil +} + +type Session struct { + Logger slog.Logger + WorkDirectory string + Config *proto.Config + + server Server + stream proto.DRPCProvisioner_SessionStream + logLevel int32 +} + +func (s *Session) Context() context.Context { + return s.stream.Context() +} + +func (s *Session) extractArchive() error { + ctx := s.Context() + + s.Logger.Info(ctx, "unpacking template source archive", + slog.F("size_bytes", len(s.Config.TemplateSourceArchive)), + ) + + reader := tar.NewReader(bytes.NewBuffer(s.Config.TemplateSourceArchive)) + // for safety, nil out the reference on Config, since the reader now owns it. + s.Config.TemplateSourceArchive = nil + for { + header, err := reader.Next() + if err != nil { + if xerrors.Is(err, io.EOF) { + break + } + return xerrors.Errorf("read template source archive: %w", err) + } + // Security: don't untar absolute or relative paths, as this can allow a malicious tar to overwrite + // files outside the workdir. + if !filepath.IsLocal(header.Name) { + return xerrors.Errorf("refusing to extract to non-local path") + } + // nolint: gosec + headerPath := filepath.Join(s.WorkDirectory, header.Name) + if !strings.HasPrefix(headerPath, filepath.Clean(s.WorkDirectory)) { + return xerrors.New("tar attempts to target relative upper directory") + } + mode := header.FileInfo().Mode() + if mode == 0 { + mode = 0o600 + } + switch header.Typeflag { + case tar.TypeDir: + err = os.MkdirAll(headerPath, mode) + if err != nil { + return xerrors.Errorf("mkdir %q: %w", headerPath, err) + } + s.Logger.Debug(context.Background(), "extracted directory", + slog.F("path", headerPath), + slog.F("mode", fmt.Sprintf("%O", mode))) + case tar.TypeReg: + file, err := os.OpenFile(headerPath, os.O_CREATE|os.O_RDWR, mode) + if err != nil { + return xerrors.Errorf("create file %q (mode %s): %w", headerPath, mode, err) + } + // Max file size of 10MiB. + size, err := io.CopyN(file, reader, 10<<20) + if xerrors.Is(err, io.EOF) { + err = nil + } + if err != nil { + _ = file.Close() + return xerrors.Errorf("copy file %q: %w", headerPath, err) + } + err = file.Close() + if err != nil { + return xerrors.Errorf("close file %q: %s", headerPath, err) + } + s.Logger.Debug(context.Background(), "extracted file", + slog.F("size_bytes", size), + slog.F("path", headerPath), + slog.F("mode", mode), + ) + } + } + return nil +} + +func (s *Session) ProvisionLog(level proto.LogLevel, output string) { + if int32(level) < s.logLevel { + return + } + + err := s.stream.Send(&proto.Response{Type: &proto.Response_Log{Log: &proto.Log{ + Level: level, + Output: output, + }}}) + if err != nil { + s.Logger.Error(s.Context(), "failed to transmit log", + slog.F("level", level), slog.F("output", output)) + } +} + +type pRequest interface { + *proto.ParseRequest | *proto.PlanRequest | *proto.ApplyRequest +} + +type pComplete interface { + *proto.ParseComplete | *proto.PlanComplete | *proto.ApplyComplete +} + +// request processes a single request call to the Server and returns its complete result, while also processing cancel +// requests from the daemon. Provisioner implementations read from canceledOrComplete to be asynchronously informed +// of cancel. +type request[R pRequest, C pComplete] struct { + req R + session *Session + cancels <-chan *proto.Request + serverFn func(*Session, R, <-chan struct{}) C +} + +func (r *request[R, C]) do() (C, error) { + canceledOrComplete := make(chan struct{}) + result := make(chan C) + go func() { + c := r.serverFn(r.session, r.req, canceledOrComplete) + result <- c + }() + select { + case req := <-r.cancels: + close(canceledOrComplete) + // wait for server to complete the request, even though we have canceled, + // so that we can't start a new request, and so that if the job was close + // to completion and the cancel was ignored, we return to complete. + c := <-result + // verify we got a cancel instead of another request or closed channel --- which is an error! + if req.GetCancel() != nil { + return c, nil + } + if req == nil { + return c, xerrors.New("got nil while old request still processing") + } + return c, xerrors.Errorf("got new request %T while old request still processing", req.Type) + case c := <-result: + close(canceledOrComplete) + return c, nil + } +} diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go index 19730d3bf0530..f5df895d64eaa 100644 --- a/provisionersdk/transport.go +++ b/provisionersdk/transport.go @@ -19,7 +19,7 @@ const ( MaxMessageSize = 4 << 20 ) -// MultiplexedConn returns a multiplexed dRPC connection from a yamux session. +// MultiplexedConn returns a multiplexed dRPC connection from a yamux Session. func MultiplexedConn(session *yamux.Session) drpc.Conn { return &multiplexedDRPC{session} } diff --git a/scaletest/agentconn/run_test.go b/scaletest/agentconn/run_test.go index e9697ddedeb7a..4d3ffb8d0da2d 100644 --- a/scaletest/agentconn/run_test.go +++ b/scaletest/agentconn/run_test.go @@ -231,10 +231,10 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/scaletest/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go index b1d871cefd743..b69297f622676 100644 --- a/scaletest/createworkspaces/run_test.go +++ b/scaletest/createworkspaces/run_test.go @@ -48,10 +48,10 @@ func Test_Runner(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "hello from logs", @@ -59,8 +59,8 @@ func Test_Runner(t *testing.T) { }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{ { Name: "example", @@ -170,10 +170,10 @@ func Test_Runner(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{Log: &proto.Log{}}, + Type: &proto.Response_Log{Log: &proto.Log{}}, }, }, }) @@ -282,10 +282,10 @@ func Test_Runner(t *testing.T) { authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "hello from logs", @@ -293,8 +293,8 @@ func Test_Runner(t *testing.T) { }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{ { Name: "example", @@ -407,11 +407,11 @@ func Test_Runner(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Error: "test error", }, }, diff --git a/scaletest/reconnectingpty/run_test.go b/scaletest/reconnectingpty/run_test.go index 8b45f9e01587b..81de3dcfb9da8 100644 --- a/scaletest/reconnectingpty/run_test.go +++ b/scaletest/reconnectingpty/run_test.go @@ -252,10 +252,10 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/scaletest/workspacebuild/run_test.go b/scaletest/workspacebuild/run_test.go index 2b50c95bdad5f..c07b10f8095b9 100644 --- a/scaletest/workspacebuild/run_test.go +++ b/scaletest/workspacebuild/run_test.go @@ -45,10 +45,10 @@ func Test_Runner(t *testing.T) { authToken3 := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Log{ + Type: &proto.Response_Log{ Log: &proto.Log{ Level: proto.LogLevel_INFO, Output: "hello from logs", @@ -56,8 +56,8 @@ func Test_Runner(t *testing.T) { }, }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{ { Name: "example1", @@ -199,11 +199,11 @@ func Test_Runner(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{ { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Error: "test error", }, }, diff --git a/scaletest/workspacetraffic/run_test.go b/scaletest/workspacetraffic/run_test.go index 4077a266aa124..308630910427d 100644 --- a/scaletest/workspacetraffic/run_test.go +++ b/scaletest/workspacetraffic/run_test.go @@ -41,10 +41,10 @@ func TestRun(t *testing.T) { agentName = "agent" version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", @@ -154,10 +154,10 @@ func TestRun(t *testing.T) { agentName = "agent" version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, - ProvisionPlan: echo.ProvisionComplete, - ProvisionApply: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ Resources: []*proto.Resource{{ Name: "example", Type: "aws_instance", diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 6525fa3b01f9f..aa0e2e32eb9ab 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -8,10 +8,10 @@ import { Agent, App, AppSharingLevel, - Parse_Complete, - Parse_Response, - Provision_Complete, - Provision_Response, + Response, + ParseComplete, + PlanComplete, + ApplyComplete, Resource, RichParameter, } from "./provisionerGenerated" @@ -337,11 +337,11 @@ type RecursivePartial = { interface EchoProvisionerResponses { // parse is for observing any Terraform variables - parse?: RecursivePartial[] + parse?: RecursivePartial[] // plan occurs when the template is imported - plan?: RecursivePartial[] + plan?: RecursivePartial[] // apply occurs when the workspace is built - apply?: RecursivePartial[] + apply?: RecursivePartial[] } // createTemplateVersionTar consumes a series of echo provisioner protobufs and @@ -353,109 +353,133 @@ const createTemplateVersionTar = async ( responses = {} } if (!responses.parse) { - responses.parse = [{}] + responses.parse = [ + { + parse: {}, + }, + ] } if (!responses.apply) { - responses.apply = [{}] + responses.apply = [ + { + apply: {}, + }, + ] } if (!responses.plan) { - responses.plan = responses.apply + responses.plan = responses.apply.map((response) => { + if (response.log) { + return response + } + return { + plan: { + error: response.apply?.error ?? "", + resources: response.apply?.resources ?? [], + parameters: response.apply?.parameters ?? [], + gitAuthProviders: response.apply?.gitAuthProviders ?? [], + }, + } + }) } const tar = new TarWriter() responses.parse.forEach((response, index) => { - response.complete = { + response.parse = { templateVariables: [], - ...response.complete, - } as Parse_Complete + error: "", + readme: new Uint8Array(), + ...response.parse, + } as ParseComplete tar.addFile( `${index}.parse.protobuf`, - Parse_Response.encode(response as Parse_Response).finish(), + Response.encode(response as Response).finish(), ) }) - const fillProvisionResponse = ( - response: RecursivePartial, - ) => { - response.complete = { + const fillResource = (resource: RecursivePartial) => { + if (resource.agents) { + resource.agents = resource.agents?.map( + (agent: RecursivePartial) => { + if (agent.apps) { + agent.apps = agent.apps?.map((app: RecursivePartial) => { + return { + command: "", + displayName: "example", + external: false, + icon: "", + sharingLevel: AppSharingLevel.PUBLIC, + slug: "example", + subdomain: false, + url: "", + ...app, + } as App + }) + } + return { + apps: [], + architecture: "amd64", + connectionTimeoutSeconds: 300, + directory: "", + env: {}, + id: randomUUID(), + metadata: [], + motdFile: "", + name: "dev", + operatingSystem: "linux", + shutdownScript: "", + shutdownScriptTimeoutSeconds: 0, + startupScript: "", + startupScriptBehavior: "", + startupScriptTimeoutSeconds: 300, + troubleshootingUrl: "", + token: randomUUID(), + ...agent, + } as Agent + }, + ) + } + return { + agents: [], + dailyCost: 0, + hide: false, + icon: "", + instanceType: "", + metadata: [], + name: "dev", + type: "echo", + ...resource, + } as Resource + } + + responses.apply.forEach((response, index) => { + response.apply = { error: "", state: new Uint8Array(), resources: [], parameters: [], gitAuthProviders: [], - plan: new Uint8Array(), - ...response.complete, - } as Provision_Complete - response.complete.resources = response.complete.resources?.map( - (resource) => { - if (resource.agents) { - resource.agents = resource.agents?.map((agent) => { - if (agent.apps) { - agent.apps = agent.apps?.map((app) => { - return { - command: "", - displayName: "example", - external: false, - icon: "", - sharingLevel: AppSharingLevel.PUBLIC, - slug: "example", - subdomain: false, - url: "", - ...app, - } as App - }) - } - return { - apps: [], - architecture: "amd64", - connectionTimeoutSeconds: 300, - directory: "", - env: {}, - id: randomUUID(), - metadata: [], - motdFile: "", - name: "dev", - operatingSystem: "linux", - shutdownScript: "", - shutdownScriptTimeoutSeconds: 0, - startupScript: "", - startupScriptBehavior: "", - startupScriptTimeoutSeconds: 300, - troubleshootingUrl: "", - token: randomUUID(), - ...agent, - } as Agent - }) - } - return { - agents: [], - dailyCost: 0, - hide: false, - icon: "", - instanceType: "", - metadata: [], - name: "dev", - type: "echo", - ...resource, - } as Resource - }, - ) - } - - responses.apply.forEach((response, index) => { - fillProvisionResponse(response) + ...response.apply, + } as ApplyComplete + response.apply.resources = response.apply.resources?.map(fillResource) tar.addFile( - `${index}.provision.apply.protobuf`, - Provision_Response.encode(response as Provision_Response).finish(), + `${index}.apply.protobuf`, + Response.encode(response as Response).finish(), ) }) responses.plan.forEach((response, index) => { - fillProvisionResponse(response) + response.plan = { + error: "", + resources: [], + parameters: [], + gitAuthProviders: [], + ...response.plan, + } as PlanComplete + response.plan.resources = response.plan.resources?.map(fillResource) tar.addFile( - `${index}.provision.plan.protobuf`, - Provision_Response.encode(response as Provision_Response).finish(), + `${index}.plan.protobuf`, + Response.encode(response as Response).finish(), ) }) const tarFile = await tar.write() @@ -512,16 +536,21 @@ export const echoResponsesWithParameters = ( richParameters: RichParameter[], ): EchoProvisionerResponses => { return { + parse: [ + { + parse: {}, + }, + ], plan: [ { - complete: { + plan: { parameters: richParameters, }, }, ], apply: [ { - complete: { + apply: { resources: [ { name: "example", diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 66b6c222c9ac7..d79d5b422282c 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -21,6 +21,7 @@ export enum AppSharingLevel { UNRECOGNIZED = -1, } +/** WorkspaceTransition is the desired outcome of a build */ export enum WorkspaceTransition { START = 0, STOP = 1, @@ -177,29 +178,8 @@ export interface Resource_Metadata { isNull: boolean } -/** Parse consumes source-code from a directory to produce inputs. */ -export interface Parse {} - -export interface Parse_Request { - directory: string -} - -export interface Parse_Complete { - templateVariables: TemplateVariable[] -} - -export interface Parse_Response { - log?: Log | undefined - complete?: Parse_Complete | undefined -} - -/** - * Provision consumes source-code from a directory to produce resources. - * Exactly one of Plan or Apply must be provided in a single session. - */ -export interface Provision {} - -export interface Provision_Metadata { +/** Metadata is information about a workspace used in the execution of a build */ +export interface Metadata { coderUrl: string workspaceTransition: WorkspaceTransition workspaceName: string @@ -213,49 +193,74 @@ export interface Provision_Metadata { workspaceOwnerSessionToken: string } -/** - * Config represents execution configuration shared by both Plan and - * Apply commands. - */ -export interface Provision_Config { - directory: string +/** Config represents execution configuration shared by all subsequent requests in the Session */ +export interface Config { + /** template_source_archive is a tar of the template source files */ + templateSourceArchive: Uint8Array + /** state is the provisioner state (if any) */ state: Uint8Array - metadata: Provision_Metadata | undefined provisionerLogLevel: string } -export interface Provision_Plan { - config: Provision_Config | undefined +/** ParseRequest consumes source-code to produce inputs. */ +export interface ParseRequest {} + +/** ParseComplete indicates a request to parse completed. */ +export interface ParseComplete { + error: string + templateVariables: TemplateVariable[] + readme: Uint8Array +} + +/** PlanRequest asks the provisioner to plan what resources & parameters it will create */ +export interface PlanRequest { + metadata: Metadata | undefined richParameterValues: RichParameterValue[] variableValues: VariableValue[] gitAuthProviders: GitAuthProvider[] } -export interface Provision_Apply { - config: Provision_Config | undefined - plan: Uint8Array +/** PlanComplete indicates a request to plan completed. */ +export interface PlanComplete { + error: string + resources: Resource[] + parameters: RichParameter[] + gitAuthProviders: string[] } -export interface Provision_Cancel {} - -export interface Provision_Request { - plan?: Provision_Plan | undefined - apply?: Provision_Apply | undefined - cancel?: Provision_Cancel | undefined +/** + * ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response + * in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. + */ +export interface ApplyRequest { + metadata: Metadata | undefined } -export interface Provision_Complete { +/** ApplyComplete indicates a request to apply completed. */ +export interface ApplyComplete { state: Uint8Array error: string resources: Resource[] parameters: RichParameter[] gitAuthProviders: string[] - plan: Uint8Array } -export interface Provision_Response { +/** CancelRequest requests that the previous request be canceled gracefully. */ +export interface CancelRequest {} + +export interface Request { + config?: Config | undefined + parse?: ParseRequest | undefined + plan?: PlanRequest | undefined + apply?: ApplyRequest | undefined + cancel?: CancelRequest | undefined +} + +export interface Response { log?: Log | undefined - complete?: Provision_Complete | undefined + parse?: ParseComplete | undefined + plan?: PlanComplete | undefined + apply?: ApplyComplete | undefined } export const Empty = { @@ -648,60 +653,9 @@ export const Resource_Metadata = { }, } -export const Parse = { - encode(_: Parse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - return writer - }, -} - -export const Parse_Request = { +export const Metadata = { encode( - message: Parse_Request, - writer: _m0.Writer = _m0.Writer.create(), - ): _m0.Writer { - if (message.directory !== "") { - writer.uint32(10).string(message.directory) - } - return writer - }, -} - -export const Parse_Complete = { - encode( - message: Parse_Complete, - writer: _m0.Writer = _m0.Writer.create(), - ): _m0.Writer { - for (const v of message.templateVariables) { - TemplateVariable.encode(v!, writer.uint32(10).fork()).ldelim() - } - return writer - }, -} - -export const Parse_Response = { - encode( - message: Parse_Response, - writer: _m0.Writer = _m0.Writer.create(), - ): _m0.Writer { - if (message.log !== undefined) { - Log.encode(message.log, writer.uint32(10).fork()).ldelim() - } - if (message.complete !== undefined) { - Parse_Complete.encode(message.complete, writer.uint32(18).fork()).ldelim() - } - return writer - }, -} - -export const Provision = { - encode(_: Provision, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - return writer - }, -} - -export const Provision_Metadata = { - encode( - message: Provision_Metadata, + message: Metadata, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.coderUrl !== "") { @@ -741,96 +695,108 @@ export const Provision_Metadata = { }, } -export const Provision_Config = { +export const Config = { encode( - message: Provision_Config, + message: Config, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - if (message.directory !== "") { - writer.uint32(10).string(message.directory) + if (message.templateSourceArchive.length !== 0) { + writer.uint32(10).bytes(message.templateSourceArchive) } if (message.state.length !== 0) { writer.uint32(18).bytes(message.state) } - if (message.metadata !== undefined) { - Provision_Metadata.encode( - message.metadata, - writer.uint32(26).fork(), - ).ldelim() - } if (message.provisionerLogLevel !== "") { - writer.uint32(34).string(message.provisionerLogLevel) + writer.uint32(26).string(message.provisionerLogLevel) } return writer }, } -export const Provision_Plan = { +export const ParseRequest = { encode( - message: Provision_Plan, + _: ParseRequest, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - if (message.config !== undefined) { - Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim() - } - for (const v of message.richParameterValues) { - RichParameterValue.encode(v!, writer.uint32(26).fork()).ldelim() + return writer + }, +} + +export const ParseComplete = { + encode( + message: ParseComplete, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.error !== "") { + writer.uint32(10).string(message.error) } - for (const v of message.variableValues) { - VariableValue.encode(v!, writer.uint32(34).fork()).ldelim() + for (const v of message.templateVariables) { + TemplateVariable.encode(v!, writer.uint32(18).fork()).ldelim() } - for (const v of message.gitAuthProviders) { - GitAuthProvider.encode(v!, writer.uint32(42).fork()).ldelim() + if (message.readme.length !== 0) { + writer.uint32(26).bytes(message.readme) } return writer }, } -export const Provision_Apply = { +export const PlanRequest = { encode( - message: Provision_Apply, + message: PlanRequest, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - if (message.config !== undefined) { - Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim() + if (message.metadata !== undefined) { + Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim() } - if (message.plan.length !== 0) { - writer.uint32(18).bytes(message.plan) + for (const v of message.richParameterValues) { + RichParameterValue.encode(v!, writer.uint32(18).fork()).ldelim() + } + for (const v of message.variableValues) { + VariableValue.encode(v!, writer.uint32(26).fork()).ldelim() + } + for (const v of message.gitAuthProviders) { + GitAuthProvider.encode(v!, writer.uint32(34).fork()).ldelim() } return writer }, } -export const Provision_Cancel = { +export const PlanComplete = { encode( - _: Provision_Cancel, + message: PlanComplete, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { + if (message.error !== "") { + writer.uint32(10).string(message.error) + } + for (const v of message.resources) { + Resource.encode(v!, writer.uint32(18).fork()).ldelim() + } + for (const v of message.parameters) { + RichParameter.encode(v!, writer.uint32(26).fork()).ldelim() + } + for (const v of message.gitAuthProviders) { + writer.uint32(34).string(v!) + } return writer }, } -export const Provision_Request = { +export const ApplyRequest = { encode( - message: Provision_Request, + message: ApplyRequest, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - if (message.plan !== undefined) { - Provision_Plan.encode(message.plan, writer.uint32(10).fork()).ldelim() - } - if (message.apply !== undefined) { - Provision_Apply.encode(message.apply, writer.uint32(18).fork()).ldelim() - } - if (message.cancel !== undefined) { - Provision_Cancel.encode(message.cancel, writer.uint32(26).fork()).ldelim() + if (message.metadata !== undefined) { + Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim() } return writer }, } -export const Provision_Complete = { +export const ApplyComplete = { encode( - message: Provision_Complete, + message: ApplyComplete, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.state.length !== 0) { @@ -848,34 +814,76 @@ export const Provision_Complete = { for (const v of message.gitAuthProviders) { writer.uint32(42).string(v!) } - if (message.plan.length !== 0) { - writer.uint32(50).bytes(message.plan) + return writer + }, +} + +export const CancelRequest = { + encode( + _: CancelRequest, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + return writer + }, +} + +export const Request = { + encode( + message: Request, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.config !== undefined) { + Config.encode(message.config, writer.uint32(10).fork()).ldelim() + } + if (message.parse !== undefined) { + ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim() + } + if (message.plan !== undefined) { + PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim() + } + if (message.apply !== undefined) { + ApplyRequest.encode(message.apply, writer.uint32(34).fork()).ldelim() + } + if (message.cancel !== undefined) { + CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim() } return writer }, } -export const Provision_Response = { +export const Response = { encode( - message: Provision_Response, + message: Response, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.log !== undefined) { Log.encode(message.log, writer.uint32(10).fork()).ldelim() } - if (message.complete !== undefined) { - Provision_Complete.encode( - message.complete, - writer.uint32(18).fork(), - ).ldelim() + if (message.parse !== undefined) { + ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim() + } + if (message.plan !== undefined) { + PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim() + } + if (message.apply !== undefined) { + ApplyComplete.encode(message.apply, writer.uint32(34).fork()).ldelim() } return writer }, } export interface Provisioner { - Parse(request: Parse_Request): Observable - Provision( - request: Observable, - ): Observable + /** + * Session represents provisioning a single template import or workspace. The daemon always sends Config followed + * by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream + * of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete, + * ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan, + * and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may + * request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded. + * + * The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest, + * PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request + * that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest. + */ + Session(request: Observable): Observable } diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts index b3646fbac1caa..aa69475dc8184 100644 --- a/site/e2e/tests/app.spec.ts +++ b/site/e2e/tests/app.spec.ts @@ -20,7 +20,7 @@ test("app", async ({ context, page }) => { const template = await createTemplate(page, { apply: [ { - complete: { + apply: { resources: [ { agents: [ diff --git a/site/e2e/tests/createWorkspace.spec.ts b/site/e2e/tests/createWorkspace.spec.ts index 10630e2f46ab3..1effc01976651 100644 --- a/site/e2e/tests/createWorkspace.spec.ts +++ b/site/e2e/tests/createWorkspace.spec.ts @@ -21,7 +21,7 @@ test("create workspace", async ({ page }) => { const template = await createTemplate(page, { apply: [ { - complete: { + apply: { resources: [ { name: "example", diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts index 2b88ea71110df..e10c3f6edb290 100644 --- a/site/e2e/tests/outdatedAgent.spec.ts +++ b/site/e2e/tests/outdatedAgent.spec.ts @@ -15,7 +15,7 @@ test("ssh with agent " + agentVersion, async ({ page }) => { const template = await createTemplate(page, { apply: [ { - complete: { + apply: { resources: [ { agents: [ diff --git a/site/e2e/tests/outdatedCLI.spec.ts b/site/e2e/tests/outdatedCLI.spec.ts index ab143bad27c34..1b09fccf5e13f 100644 --- a/site/e2e/tests/outdatedCLI.spec.ts +++ b/site/e2e/tests/outdatedCLI.spec.ts @@ -15,7 +15,7 @@ test("ssh with client " + clientVersion, async ({ page }) => { const template = await createTemplate(page, { apply: [ { - complete: { + apply: { resources: [ { agents: [ diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts index 492634b293a3e..8869d352d3fc8 100644 --- a/site/e2e/tests/webTerminal.spec.ts +++ b/site/e2e/tests/webTerminal.spec.ts @@ -7,7 +7,7 @@ test("web terminal", async ({ context, page }) => { const template = await createTemplate(page, { apply: [ { - complete: { + apply: { resources: [ { agents: [ From 058fb2ecf07604ed4ddfa6f7f78e1829abe48210 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 25 Aug 2023 07:28:18 -0300 Subject: [PATCH 06/40] fix(site): fix default ephemeral parameter value on parameters page (#9314) --- .../WorkspaceParametersPage/WorkspaceParametersForm.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx index 15c38499b8e1c..8ebf0ee609d05 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx @@ -132,10 +132,7 @@ export const WorkspaceParametersForm: FC<{ }) }} parameter={parameter} - initialValue={workspaceBuildParameterValue( - buildParameters, - parameter, - )} + initialValue={form.values.rich_parameter_values[index]?.value} /> ) : null, )} From e7a231e44fc3179094b5761f70199e38c6ae0d79 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 25 Aug 2023 14:48:42 +0400 Subject: [PATCH 07/40] fix: fix test flake introduced by #9264 (#9330) * Fix test flake introduced by #9264 Signed-off-by: Spike Curtis * change check to match suffix Signed-off-by: Spike Curtis --------- Signed-off-by: Spike Curtis --- coderd/templateversions_test.go | 10 ++++++---- coderd/workspacebuilds_test.go | 7 ++++++- scaletest/createworkspaces/run_test.go | 13 ++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index d06e68fabb368..2ca934e2d4085 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -308,11 +308,13 @@ func TestPatchCancelTemplateVersion(t *testing.T) { require.Eventually(t, func() bool { var err error version, err = client.TemplateVersion(ctx, version.ID) + // job gets marked Failed when there is an Error; in practice we never get to Status = Canceled + // because provisioners report an Error when canceled. We check the Error string to ensure we don't mask + // other errors in this test. + t.Logf("got version %s | %s", version.Job.Error, version.Job.Status) return assert.NoError(t, err) && - // The job will never actually cancel successfully because it will never send a - // provision complete response. - assert.Empty(t, version.Job.Error) && - version.Job.Status == codersdk.ProvisionerJobCanceling + strings.HasSuffix(version.Job.Error, "canceled") && + version.Job.Status == codersdk.ProvisionerJobFailed }, testutil.WaitShort, testutil.IntervalFast) }) } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 0ee810e3e3eda..2c32f9ac39b7a 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/xerrors" + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -412,7 +414,10 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { t.Run("User is not allowed to cancel", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + // need to include our own logger because the provisioner (rightly) drops error logs when we shut down the + // test with a build in progress. + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Logger: &logger}) owner := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, diff --git a/scaletest/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go index b69297f622676..d5e96e22fcc83 100644 --- a/scaletest/createworkspaces/run_test.go +++ b/scaletest/createworkspaces/run_test.go @@ -163,8 +163,12 @@ func Test_Runner(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + // need to include our own logger because the provisioner (rightly) drops error logs when we shut down the + // test with a build in progress. + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, + Logger: &logger, }) user := coderdtest.CreateFirstUser(t, client) @@ -251,14 +255,17 @@ func Test_Runner(t *testing.T) { if err != nil { return false } - for _, build := range builds { + for i, build := range builds { + t.Logf("checking build #%d: %s | %s", i, build.Transition, build.Job.Status) // One of the builds should be for creating the workspace, if build.Transition != codersdk.WorkspaceTransitionStart { continue } - // And it should be either canceled or canceling - if build.Job.Status == codersdk.ProvisionerJobCanceled || build.Job.Status == codersdk.ProvisionerJobCanceling { + // And it should be either failed (Echo returns an error when job is canceled), canceling, or canceled. + if build.Job.Status == codersdk.ProvisionerJobFailed || + build.Job.Status == codersdk.ProvisionerJobCanceling || + build.Job.Status == codersdk.ProvisionerJobCanceled { return true } } From aed891b4ff823e2ffcaae3fc69a87707beee75f6 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 25 Aug 2023 14:58:13 +0400 Subject: [PATCH 08/40] fix: fix coder template pull on Windows (#9327) * fix: fix coder template pull on Windows Signed-off-by: Spike Curtis * appease linter Signed-off-by: Spike Curtis * improvements from code review Signed-off-by: Spike Curtis --------- Signed-off-by: Spike Curtis --- cli/templatepull.go | 2 +- cli/templatepull_test.go | 275 +++++++++++++++++++++++---------------- 2 files changed, 165 insertions(+), 112 deletions(-) diff --git a/cli/templatepull.go b/cli/templatepull.go index 8f234447a6dba..eb772379b9611 100644 --- a/cli/templatepull.go +++ b/cli/templatepull.go @@ -83,7 +83,7 @@ func (r *RootCmd) templatePull() *clibase.Cmd { } if dest == "" { - dest = templateName + "/" + dest = templateName } err = os.MkdirAll(dest, 0o750) diff --git a/cli/templatepull_test.go b/cli/templatepull_test.go index 95b0a6cf9aa30..5a5e7bc6c9e06 100644 --- a/cli/templatepull_test.go +++ b/cli/templatepull_test.go @@ -40,165 +40,218 @@ func dirSum(t *testing.T, dir string) string { return hex.EncodeToString(sum.Sum(nil)) } -func TestTemplatePull(t *testing.T) { +func TestTemplatePull_NoName(t *testing.T) { t.Parallel() - t.Run("NoName", func(t *testing.T) { - t.Parallel() + inv, _ := clitest.New(t, "templates", "pull") + err := inv.Run() + require.Error(t, err) +} - inv, _ := clitest.New(t, "templates", "pull") - err := inv.Run() - require.Error(t, err) - }) +// Stdout tests that 'templates pull' pulls down the latest template +// and writes it to stdout. +func TestTemplatePull_Stdout(t *testing.T) { + t.Parallel() - // Stdout tests that 'templates pull' pulls down the latest template - // and writes it to stdout. - t.Run("Stdout", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) + // Create an initial template bundle. + source1 := genTemplateVersionSource() + // Create an updated template bundle. This will be used to ensure + // that templates are correctly returned in order from latest to oldest. + source2 := genTemplateVersionSource() - // Create an initial template bundle. - source1 := genTemplateVersionSource() - // Create an updated template bundle. This will be used to ensure - // that templates are correctly returned in order from latest to oldest. - source2 := genTemplateVersionSource() + expected, err := echo.Tar(source2) + require.NoError(t, err) - expected, err := echo.Tar(source2) - require.NoError(t, err) + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) - version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + // Update the template version so that we can assert that templates + // are being sorted correctly. + _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) - // Update the template version so that we can assert that templates - // are being sorted correctly. - _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name) + clitest.SetupConfig(t, client, root) - inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name) - clitest.SetupConfig(t, client, root) + var buf bytes.Buffer + inv.Stdout = &buf - var buf bytes.Buffer - inv.Stdout = &buf + err = inv.Run() + require.NoError(t, err) - err = inv.Run() - require.NoError(t, err) + require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") +} - require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ") - }) +// ToDir tests that 'templates pull' pulls down the latest template +// and writes it to the correct directory. +func TestTemplatePull_ToDir(t *testing.T) { + t.Parallel() - // ToDir tests that 'templates pull' pulls down the latest template - // and writes it to the correct directory. - t.Run("ToDir", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) + // Create an initial template bundle. + source1 := genTemplateVersionSource() + // Create an updated template bundle. This will be used to ensure + // that templates are correctly returned in order from latest to oldest. + source2 := genTemplateVersionSource() - // Create an initial template bundle. - source1 := genTemplateVersionSource() - // Create an updated template bundle. This will be used to ensure - // that templates are correctly returned in order from latest to oldest. - source2 := genTemplateVersionSource() + expected, err := echo.Tar(source2) + require.NoError(t, err) - expected, err := echo.Tar(source2) - require.NoError(t, err) + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) - version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + // Update the template version so that we can assert that templates + // are being sorted correctly. + _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) - // Update the template version so that we can assert that templates - // are being sorted correctly. - _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + dir := t.TempDir() - dir := t.TempDir() + expectedDest := filepath.Join(dir, "expected") + actualDest := filepath.Join(dir, "actual") + ctx := context.Background() - expectedDest := filepath.Join(dir, "expected") - actualDest := filepath.Join(dir, "actual") - ctx := context.Background() + err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil) + require.NoError(t, err) - err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil) - require.NoError(t, err) + inv, root := clitest.New(t, "templates", "pull", template.Name, actualDest) + clitest.SetupConfig(t, client, root) - inv, root := clitest.New(t, "templates", "pull", template.Name, actualDest) - clitest.SetupConfig(t, client, root) + ptytest.New(t).Attach(inv) - ptytest.New(t).Attach(inv) + require.NoError(t, inv.Run()) - require.NoError(t, inv.Run()) + require.Equal(t, + dirSum(t, expectedDest), + dirSum(t, actualDest), + ) +} - require.Equal(t, - dirSum(t, expectedDest), - dirSum(t, actualDest), - ) - }) +// ToDir tests that 'templates pull' pulls down the latest template +// and writes it to a directory with the name of the template if the path is not implicitly supplied. +// nolint: paralleltest +func TestTemplatePull_ToImplicit(t *testing.T) { + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) - // FolderConflict tests that 'templates pull' fails when a folder with has - // existing - t.Run("FolderConflict", func(t *testing.T) { - t.Parallel() + // Create an initial template bundle. + source1 := genTemplateVersionSource() + // Create an updated template bundle. This will be used to ensure + // that templates are correctly returned in order from latest to oldest. + source2 := genTemplateVersionSource() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) + expected, err := echo.Tar(source2) + require.NoError(t, err) - // Create an initial template bundle. - source1 := genTemplateVersionSource() - // Create an updated template bundle. This will be used to ensure - // that templates are correctly returned in order from latest to oldest. - source2 := genTemplateVersionSource() + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) - expected, err := echo.Tar(source2) - require.NoError(t, err) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) - version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) - _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) + // Update the template version so that we can assert that templates + // are being sorted correctly. + _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) + // create a tempdir and change the working directory to it for the duration of the test (cannot run in parallel) + dir := t.TempDir() + wd, err := os.Getwd() + require.NoError(t, err) + err = os.Chdir(dir) + require.NoError(t, err) + defer func() { + err := os.Chdir(wd) + require.NoError(t, err, "if this fails, it can break other subsequent tests due to wrong working directory") + }() - // Update the template version so that we can assert that templates - // are being sorted correctly. - _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + expectedDest := filepath.Join(dir, "expected") + actualDest := filepath.Join(dir, template.Name) - dir := t.TempDir() + ctx := context.Background() - expectedDest := filepath.Join(dir, "expected") - conflictDest := filepath.Join(dir, "conflict") + err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil) + require.NoError(t, err) - err = os.MkdirAll(conflictDest, 0o700) - require.NoError(t, err) + inv, root := clitest.New(t, "templates", "pull", template.Name) + clitest.SetupConfig(t, client, root) - err = os.WriteFile( - filepath.Join(conflictDest, "conflict-file"), - []byte("conflict"), 0o600, - ) - require.NoError(t, err) + ptytest.New(t).Attach(inv) - ctx := context.Background() + require.NoError(t, inv.Run()) - err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil) - require.NoError(t, err) + require.Equal(t, + dirSum(t, expectedDest), + dirSum(t, actualDest), + ) +} + +// FolderConflict tests that 'templates pull' fails when a folder with has +// existing +func TestTemplatePull_FolderConflict(t *testing.T) { + t.Parallel() - inv, root := clitest.New(t, "templates", "pull", template.Name, conflictDest) - clitest.SetupConfig(t, client, root) + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) - pty := ptytest.New(t).Attach(inv) + // Create an initial template bundle. + source1 := genTemplateVersionSource() + // Create an updated template bundle. This will be used to ensure + // that templates are correctly returned in order from latest to oldest. + source2 := genTemplateVersionSource() - waiter := clitest.StartWithWaiter(t, inv) + expected, err := echo.Tar(source2) + require.NoError(t, err) - pty.ExpectMatch("not empty") - pty.WriteLine("no") + version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID) - waiter.RequireError() + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID) - ents, err := os.ReadDir(conflictDest) - require.NoError(t, err) + // Update the template version so that we can assert that templates + // are being sorted correctly. + _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID) + + dir := t.TempDir() + + expectedDest := filepath.Join(dir, "expected") + conflictDest := filepath.Join(dir, "conflict") + + err = os.MkdirAll(conflictDest, 0o700) + require.NoError(t, err) + + err = os.WriteFile( + filepath.Join(conflictDest, "conflict-file"), + []byte("conflict"), 0o600, + ) + require.NoError(t, err) + + ctx := context.Background() + + err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil) + require.NoError(t, err) + + inv, root := clitest.New(t, "templates", "pull", template.Name, conflictDest) + clitest.SetupConfig(t, client, root) + + pty := ptytest.New(t).Attach(inv) + + waiter := clitest.StartWithWaiter(t, inv) + + pty.ExpectMatch("not empty") + pty.WriteLine("no") + + waiter.RequireError() + + ents, err := os.ReadDir(conflictDest) + require.NoError(t, err) - require.Len(t, ents, 1, "conflict folder should have single conflict file") - }) + require.Len(t, ents, 1, "conflict folder should have single conflict file") } // genTemplateVersionSource returns a unique bundle that can be used to create From d7a788d89d520968875cb49f907f6483f89b1a0f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 25 Aug 2023 14:50:38 +0200 Subject: [PATCH 09/40] test(site): e2e: restart workspace with ephemeral parameters (#9304) --- site/e2e/helpers.ts | 140 ++++++++++++------ site/e2e/parameters.ts | 36 +++-- site/e2e/tests/restartWorkspace.spec.ts | 45 ++++++ site/e2e/tests/startWorkspace.spec.ts | 49 ++++++ .../components/WorkspaceActions/Buttons.tsx | 1 + 5 files changed, 219 insertions(+), 52 deletions(-) create mode 100644 site/e2e/tests/restartWorkspace.spec.ts create mode 100644 site/e2e/tests/startWorkspace.spec.ts diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index aa0e2e32eb9ab..8743b13730b08 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -34,52 +34,16 @@ export const createWorkspace = async ( const name = randomName() await page.getByLabel("name").fill(name) - for (const buildParameter of buildParameters) { - const richParameter = richParameters.find( - (richParam) => richParam.name === buildParameter.name, - ) - if (!richParameter) { - throw new Error( - "build parameter is expected to be present in rich parameter schema", - ) - } - - const parameterLabel = await page.waitForSelector( - "[data-testid='parameter-field-" + richParameter.name + "']", - { state: "visible" }, - ) - - if (richParameter.type === "bool") { - const parameterField = await parameterLabel.waitForSelector( - "[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" + - buildParameter.value + - "']", - ) - await parameterField.check() - } else if (richParameter.options.length > 0) { - const parameterField = await parameterLabel.waitForSelector( - "[data-testid='parameter-field-options'] .MuiRadio-root input[value='" + - buildParameter.value + - "']", - ) - await parameterField.check() - } else if (richParameter.type === "list(string)") { - throw new Error("not implemented yet") // FIXME - } else { - // text or number - const parameterField = await parameterLabel.waitForSelector( - "[data-testid='parameter-field-text'] input", - ) - await parameterField.fill(buildParameter.value) - } - } - + await fillParameters(page, richParameters, buildParameters) await page.getByTestId("form-submit").click() await expect(page).toHaveURL("/@admin/" + name) - await page.waitForSelector("[data-testid='build-status']", { - state: "visible", - }) + await page.waitForSelector( + "span[data-testid='build-status'] >> text=Running", + { + state: "visible", + }, + ) return name } @@ -213,6 +177,50 @@ export const sshIntoWorkspace = async ( }) } +export const stopWorkspace = async (page: Page, workspaceName: string) => { + await page.goto("/@admin/" + workspaceName, { + waitUntil: "domcontentloaded", + }) + await expect(page).toHaveURL("/@admin/" + workspaceName) + + await page.getByTestId("workspace-stop-button").click() + + await page.waitForSelector( + "span[data-testid='build-status'] >> text=Stopped", + { + state: "visible", + }, + ) +} + +export const buildWorkspaceWithParameters = async ( + page: Page, + workspaceName: string, + richParameters: RichParameter[] = [], + buildParameters: WorkspaceBuildParameter[] = [], + confirm: boolean = false, +) => { + await page.goto("/@admin/" + workspaceName, { + waitUntil: "domcontentloaded", + }) + await expect(page).toHaveURL("/@admin/" + workspaceName) + + await page.getByTestId("build-parameters-button").click() + + await fillParameters(page, richParameters, buildParameters) + await page.getByTestId("build-parameters-submit").click() + if (confirm) { + await page.getByTestId("confirm-button").click() + } + + await page.waitForSelector( + "span[data-testid='build-status'] >> text=Running", + { + state: "visible", + }, + ) +} + // startAgent runs the coder agent with the provided token. // It awaits the agent to be ready before returning. export const startAgent = async (page: Page, token: string): Promise => { @@ -561,3 +569,49 @@ export const echoResponsesWithParameters = ( ], } } + +export const fillParameters = async ( + page: Page, + richParameters: RichParameter[] = [], + buildParameters: WorkspaceBuildParameter[] = [], +) => { + for (const buildParameter of buildParameters) { + const richParameter = richParameters.find( + (richParam) => richParam.name === buildParameter.name, + ) + if (!richParameter) { + throw new Error( + "build parameter is expected to be present in rich parameter schema", + ) + } + + const parameterLabel = await page.waitForSelector( + "[data-testid='parameter-field-" + richParameter.name + "']", + { state: "visible" }, + ) + + if (richParameter.type === "bool") { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" + + buildParameter.value + + "']", + ) + await parameterField.check() + } else if (richParameter.options.length > 0) { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-options'] .MuiRadio-root input[value='" + + buildParameter.value + + "']", + ) + await parameterField.check() + } else if (richParameter.type === "list(string)") { + throw new Error("not implemented yet") // FIXME + } else { + // text or number + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-text'] input", + ) + await parameterField.fill(buildParameter.value) + } + } +} diff --git a/site/e2e/parameters.ts b/site/e2e/parameters.ts index c575fdcf83162..240eeb0f8566c 100644 --- a/site/e2e/parameters.ts +++ b/site/e2e/parameters.ts @@ -28,7 +28,6 @@ export const firstParameter: RichParameter = { name: "first_parameter", displayName: "First parameter", type: "number", - options: [], description: "This is first parameter.", icon: "/emojis/1f310.png", defaultValue: "123", @@ -43,10 +42,8 @@ export const secondParameter: RichParameter = { name: "second_parameter", displayName: "Second parameter", type: "string", - options: [], description: "This is second parameter.", defaultValue: "abc", - icon: "", order: 2, } @@ -56,7 +53,6 @@ export const thirdParameter: RichParameter = { name: "third_parameter", type: "string", - options: [], description: "This is third parameter.", defaultValue: "", mutable: true, @@ -69,10 +65,8 @@ export const fourthParameter: RichParameter = { name: "fourth_parameter", type: "bool", - options: [], description: "This is fourth parameter.", defaultValue: "true", - icon: "", order: 3, } @@ -105,7 +99,6 @@ export const fifthParameter: RichParameter = { ], description: "This is fifth parameter.", defaultValue: "def", - icon: "", order: 3, } @@ -116,7 +109,6 @@ export const sixthParameter: RichParameter = { name: "sixth_parameter", displayName: "Sixth parameter", type: "number", - options: [], description: "This is sixth parameter.", icon: "/emojis/1f310.png", required: true, @@ -131,8 +123,34 @@ export const seventhParameter: RichParameter = { name: "seventh_parameter", displayName: "Seventh parameter", type: "string", - options: [], description: "This is seventh parameter.", required: true, order: 1, } + +// Build options + +export const firstBuildOption: RichParameter = { + ...emptyParameter, + + name: "first_build_option", + displayName: "First build option", + type: "string", + description: "This is first build option.", + icon: "/emojis/1f310.png", + defaultValue: "ABCDEF", + mutable: true, + ephemeral: true, +} + +export const secondBuildOption: RichParameter = { + ...emptyParameter, + + name: "second_build_option", + displayName: "Second build option", + type: "bool", + description: "This is second build option.", + defaultValue: "false", + mutable: true, + ephemeral: true, +} diff --git a/site/e2e/tests/restartWorkspace.spec.ts b/site/e2e/tests/restartWorkspace.spec.ts new file mode 100644 index 0000000000000..eb5d0c99c0217 --- /dev/null +++ b/site/e2e/tests/restartWorkspace.spec.ts @@ -0,0 +1,45 @@ +import { test } from "@playwright/test" +import { + buildWorkspaceWithParameters, + createTemplate, + createWorkspace, + echoResponsesWithParameters, + verifyParameters, +} from "../helpers" + +import { firstBuildOption, secondBuildOption } from "../parameters" +import { RichParameter } from "../provisionerGenerated" + +test("restart workspace with ephemeral parameters", async ({ page }) => { + const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace(page, template) + + // Verify that build options are default (not selected). + await verifyParameters(page, workspaceName, richParameters, [ + { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, + { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, + ]) + + // Now, restart the workspace with ephemeral parameters selected. + const buildParameters = [ + { name: firstBuildOption.name, value: "AAAAA" }, + { name: secondBuildOption.name, value: "true" }, + ] + await buildWorkspaceWithParameters( + page, + workspaceName, + richParameters, + buildParameters, + true, + ) + + // Verify that build options are default (not selected). + await verifyParameters(page, workspaceName, richParameters, [ + { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, + { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, + ]) +}) diff --git a/site/e2e/tests/startWorkspace.spec.ts b/site/e2e/tests/startWorkspace.spec.ts new file mode 100644 index 0000000000000..232ac27299849 --- /dev/null +++ b/site/e2e/tests/startWorkspace.spec.ts @@ -0,0 +1,49 @@ +import { test } from "@playwright/test" +import { + buildWorkspaceWithParameters, + createTemplate, + createWorkspace, + echoResponsesWithParameters, + stopWorkspace, + verifyParameters, +} from "../helpers" + +import { firstBuildOption, secondBuildOption } from "../parameters" +import { RichParameter } from "../provisionerGenerated" + +test("start workspace with ephemeral parameters", async ({ page }) => { + const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace(page, template) + + // Verify that build options are default (not selected). + await verifyParameters(page, workspaceName, richParameters, [ + { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, + { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, + ]) + + // Stop the workspace + await stopWorkspace(page, workspaceName) + + // Now, start the workspace with ephemeral parameters selected. + const buildParameters = [ + { name: firstBuildOption.name, value: "AAAAA" }, + { name: secondBuildOption.name, value: "true" }, + ] + + await buildWorkspaceWithParameters( + page, + workspaceName, + richParameters, + buildParameters, + ) + + // Verify that build options are default (not selected). + await verifyParameters(page, workspaceName, richParameters, [ + { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, + { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, + ]) +}) diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 4d62e0bb9cc32..e2a005929a5a7 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -94,6 +94,7 @@ export const StopButton: FC = ({ handleAction, loading }) => { loadingPosition="start" startIcon={} onClick={handleAction} + data-testid="workspace-stop-button" > Stop From 3b1ecd3c2fbbca6edfd1db9f9e0b0e44e8048bfd Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 25 Aug 2023 16:50:03 +0300 Subject: [PATCH 10/40] chore: update aws_linux template (#9325) --- examples/templates/aws-linux/main.tf | 73 +++++++--------------------- 1 file changed, 18 insertions(+), 55 deletions(-) diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf index 96d9136dfe758..f1f41024d938a 100644 --- a/examples/templates/aws-linux/main.tf +++ b/examples/templates/aws-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 0.7.0" + version = "~> 0.11.0" } aws = { source = "hashicorp/aws" @@ -30,24 +30,24 @@ data "coder_parameter" "region" { icon = "/emojis/1f1f0-1f1f7.png" } option { - name = "Asia Pacific (Osaka-Local)" + name = "Asia Pacific (Osaka)" value = "ap-northeast-3" - icon = "/emojis/1f1f0-1f1f7.png" + icon = "/emojis/1f1ef-1f1f5.png" } option { name = "Asia Pacific (Mumbai)" value = "ap-south-1" - icon = "/emojis/1f1f0-1f1f7.png" + icon = "/emojis/1f1ee-1f1f3.png" } option { name = "Asia Pacific (Singapore)" value = "ap-southeast-1" - icon = "/emojis/1f1f0-1f1f7.png" + icon = "/emojis/1f1f8-1f1ec.png" } option { name = "Asia Pacific (Sydney)" value = "ap-southeast-2" - icon = "/emojis/1f1f0-1f1f7.png" + icon = "/emojis/1f1e6-1f1fa.png" } option { name = "Canada (Central)" @@ -176,33 +176,21 @@ resource "coder_agent" "main" { display_name = "CPU Usage" interval = 5 timeout = 5 - script = <<-EOT - #!/bin/bash - set -e - top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}' - EOT + script = "coder stat cpu" } metadata { key = "memory" display_name = "Memory Usage" interval = 5 timeout = 5 - script = <<-EOT - #!/bin/bash - set -e - free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }' - EOT + script = "coder stat mem" } metadata { key = "disk" display_name = "Disk Usage" interval = 600 # every 10 minutes timeout = 30 # df can take a while on large filesystems - script = <<-EOT - #!/bin/bash - set -e - df /home/coder | awk '$NF=="/"{printf "%s", $5}' - EOT + script = "coder stat disk --path $HOME" } } @@ -223,11 +211,9 @@ resource "coder_app" "code-server" { } locals { - - # User data is used to stop/start AWS instances. See: - # https://github.com/hashicorp/terraform-provider-aws/issues/22 - - user_data_start = < Date: Fri, 25 Aug 2023 11:00:38 -0500 Subject: [PATCH 11/40] fix(cli): add --max-ttl to template create (#9319) It was just in template edit by mistake. --- cli/templatecreate.go | 16 ++++++++++++---- .../coder_templates_create_--help.golden | 5 +++++ docs/cli/templates_create.md | 8 ++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 38f5d7d0d7fd0..cc877adeec972 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -29,9 +29,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { variablesFile string variables []string disableEveryone bool - defaultTTL time.Duration - failureTTL time.Duration - inactivityTTL time.Duration + + defaultTTL time.Duration + failureTTL time.Duration + inactivityTTL time.Duration + maxTTL time.Duration uploadFlags templateUploadFlags ) @@ -44,7 +46,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { - if failureTTL != 0 || inactivityTTL != 0 { + if failureTTL != 0 || inactivityTTL != 0 || maxTTL != 0 { // This call can be removed when workspace_actions is no longer experimental experiments, exErr := client.Experiments(inv.Context()) if exErr != nil { @@ -134,6 +136,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { VersionID: job.ID, DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()), FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()), + MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()), TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()), DisableEveryoneGroupAccess: disableEveryone, } @@ -198,6 +201,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Default: "0h", Value: clibase.DurationOf(&inactivityTTL), }, + { + Flag: "max-ttl", + Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.", + Value: clibase.DurationOf(&maxTTL), + }, { Flag: "test.provisioner", Description: "Customize the provisioner backend.", diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index a88fe64bdeba3..030fd5f385ee8 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -21,6 +21,11 @@ Create a template from the current directory or as specified by flag Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off). + --max-ttl duration + Edit the template maximum time before shutdown - workspaces created + from this template must shutdown within the given duration after + starting. This is an enterprise-only feature. + -m, --message string Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md index 44081e8986120..8d7e0fa931cb5 100644 --- a/docs/cli/templates_create.md +++ b/docs/cli/templates_create.md @@ -57,6 +57,14 @@ Ignore warnings about not having a .terraform.lock.hcl file present in the templ Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off). +### --max-ttl + +| | | +| ---- | --------------------- | +| Type | duration | + +Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature. + ### -m, --message | | | From 91f900ec649433ea0d10b21eecb9737e76040dec Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Fri, 25 Aug 2023 13:39:12 -0400 Subject: [PATCH 12/40] docs: expand on TTL flags (#9286) * docs: expand on TTL flags * make: gen * Discard changes to site/src/api/api.ts * Discard changes to site/src/xServices/templateVersion/templateVersionXService.ts --------- Co-authored-by: Muhammad Atif Ali Co-authored-by: Muhammad Atif Ali --- cli/templatecreate.go | 6 +++--- cli/templateedit.go | 8 ++++---- .../coder_templates_create_--help.golden | 15 +++++++++++---- cli/testdata/coder_templates_edit_--help.golden | 17 ++++++++++++----- docs/cli/templates_create.md | 6 +++--- docs/cli/templates_edit.md | 8 ++++---- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index cc877adeec972..638f790dd811d 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -185,19 +185,19 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { }, { Flag: "default-ttl", - Description: "Specify a default TTL for workspaces created from this template.", + Description: "Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.", Default: "24h", Value: clibase.DurationOf(&defaultTTL), }, { Flag: "failure-ttl", - Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).", + Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\"in the UI.", Default: "0h", Value: clibase.DurationOf(&failureTTL), }, { Flag: "inactivity-ttl", - Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).", + Description: "Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.", Default: "0h", Value: clibase.DurationOf(&inactivityTTL), }, diff --git a/cli/templateedit.go b/cli/templateedit.go index 5fcd73c432f58..329187ef7ae7c 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -142,12 +142,12 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { }, { Flag: "default-ttl", - Description: "Edit the template default time before shutdown - workspaces created from this template default to this value.", + Description: "Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.", Value: clibase.DurationOf(&defaultTTL), }, { Flag: "max-ttl", - Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.", + Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to \"Max lifetime\" in the UI.", Value: clibase.DurationOf(&maxTTL), }, { @@ -176,13 +176,13 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { }, { Flag: "failure-ttl", - Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).", + Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\" in the UI.", Default: "0h", Value: clibase.DurationOf(&failureTTL), }, { Flag: "inactivity-ttl", - Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).", + Description: "Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.", Default: "0h", Value: clibase.DurationOf(&inactivityTTL), }, diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index 030fd5f385ee8..ce71793cebc27 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -4,14 +4,18 @@ Create a template from the current directory or as specified by flag Options --default-ttl duration (default: 24h) - Specify a default TTL for workspaces created from this template. + Specify a default TTL for workspaces created from this template. It is + the default time before shutdown - workspaces created from this + template default to this value. Maps to "Default autostop" in the UI. -d, --directory string (default: .) Specify the directory to create from, use '-' to read tar from stdin. --failure-ttl duration (default: 0h) - Specify a failure TTL for workspaces created from this template. This - licensed feature's default is 0h (off). + Specify a failure TTL for workspaces created from this template. It is + the amount of time after a failed "start" build before coder + automatically schedules a "stop" build to cleanup.This licensed + feature's default is 0h (off). Maps to "Failure cleanup"in the UI. --ignore-lockfile bool (default: false) Ignore warnings about not having a .terraform.lock.hcl file present in @@ -19,7 +23,10 @@ Create a template from the current directory or as specified by flag --inactivity-ttl duration (default: 0h) Specify an inactivity TTL for workspaces created from this template. - This licensed feature's default is 0h (off). + It is the amount of time the workspace is not used before it is be + stopped and auto-locked. This includes across multiple builds (e.g. + auto-starts and stops). This licensed feature's default is 0h (off). + Maps to "Dormancy threshold" in the UI. --max-ttl duration Edit the template maximum time before shutdown - workspaces created diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden index 09c0b7209e78a..19dfcd2953c33 100644 --- a/cli/testdata/coder_templates_edit_--help.golden +++ b/cli/testdata/coder_templates_edit_--help.golden @@ -16,7 +16,8 @@ Edit the metadata of a template by name. --default-ttl duration Edit the template default time before shutdown - workspaces created - from this template default to this value. + from this template default to this value. Maps to "Default autostop" + in the UI. --description string Edit the template description. @@ -25,20 +26,26 @@ Edit the metadata of a template by name. Edit the template display name. --failure-ttl duration (default: 0h) - Specify a failure TTL for workspaces created from this template. This - licensed feature's default is 0h (off). + Specify a failure TTL for workspaces created from this template. It is + the amount of time after a failed "start" build before coder + automatically schedules a "stop" build to cleanup.This licensed + feature's default is 0h (off). Maps to "Failure cleanup" in the UI. --icon string Edit the template icon path. --inactivity-ttl duration (default: 0h) Specify an inactivity TTL for workspaces created from this template. - This licensed feature's default is 0h (off). + It is the amount of time the workspace is not used before it is be + stopped and auto-locked. This includes across multiple builds (e.g. + auto-starts and stops). This licensed feature's default is 0h (off). + Maps to "Dormancy threshold" in the UI. --max-ttl duration Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after - starting. This is an enterprise-only feature. + starting, regardless of user activity. This is an enterprise-only + feature. Maps to "Max lifetime" in the UI. --name string Edit the template name. diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md index 8d7e0fa931cb5..2811e4a1ce021 100644 --- a/docs/cli/templates_create.md +++ b/docs/cli/templates_create.md @@ -19,7 +19,7 @@ coder templates create [flags] [name] | Type | duration | | Default | 24h | -Specify a default TTL for workspaces created from this template. +Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI. ### -d, --directory @@ -37,7 +37,7 @@ Specify the directory to create from, use '-' to read tar from stdin. | Type | duration | | Default | 0h | -Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off). +Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup"in the UI. ### --ignore-lockfile @@ -55,7 +55,7 @@ Ignore warnings about not having a .terraform.lock.hcl file present in the templ | Type | duration | | Default | 0h | -Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off). +Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. ### --max-ttl diff --git a/docs/cli/templates_edit.md b/docs/cli/templates_edit.md index 2d25da15b7cc1..79f4ec0ba29f6 100644 --- a/docs/cli/templates_edit.md +++ b/docs/cli/templates_edit.md @@ -45,7 +45,7 @@ Allow users to cancel in-progress workspace jobs. | ---- | --------------------- | | Type | duration | -Edit the template default time before shutdown - workspaces created from this template default to this value. +Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI. ### --description @@ -70,7 +70,7 @@ Edit the template display name. | Type | duration | | Default | 0h | -Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off). +Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup" in the UI. ### --icon @@ -87,7 +87,7 @@ Edit the template icon path. | Type | duration | | Default | 0h | -Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off). +Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. ### --max-ttl @@ -95,7 +95,7 @@ Specify an inactivity TTL for workspaces created from this template. This licens | ---- | --------------------- | | Type | duration | -Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature. +Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to "Max lifetime" in the UI. ### --name From e5c64a8ea9294f37ebdab3473a35706f4a16a36e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 25 Aug 2023 12:45:36 -0500 Subject: [PATCH 13/40] fix(site): render variable width unicode characters in terminal (#9259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, characters such as 🟢 were given insufficient space, leading to mangled output. --- site/package.json | 1 + site/pnpm-lock.yaml | 11 +++++++++++ site/src/pages/TerminalPage/TerminalPage.tsx | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/site/package.json b/site/package.json index 1b04cfb532ce6..6f49efce97652 100644 --- a/site/package.json +++ b/site/package.json @@ -103,6 +103,7 @@ "xterm": "5.2.1", "xterm-addon-canvas": "0.4.0", "xterm-addon-fit": "0.7.0", + "xterm-addon-unicode11": "0.5.0", "xterm-addon-web-links": "0.8.0", "yup": "1.2.0" }, diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index b3d7c20631048..6892f56aa2ff8 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -225,6 +225,9 @@ dependencies: xterm-addon-fit: specifier: 0.7.0 version: 0.7.0(xterm@5.2.1) + xterm-addon-unicode11: + specifier: 0.5.0 + version: 0.5.0(xterm@5.2.1) xterm-addon-web-links: specifier: 0.8.0 version: 0.8.0(xterm@5.2.1) @@ -14202,6 +14205,14 @@ packages: xterm: 5.2.1 dev: false + /xterm-addon-unicode11@0.5.0(xterm@5.2.1): + resolution: {integrity: sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.2.1 + dev: false + /xterm-addon-web-links@0.8.0(xterm@5.2.1): resolution: {integrity: sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==} peerDependencies: diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index a09f289da9805..8522daa2d30cb 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -10,6 +10,7 @@ import * as XTerm from "xterm" import { CanvasAddon } from "xterm-addon-canvas" import { FitAddon } from "xterm-addon-fit" import { WebLinksAddon } from "xterm-addon-web-links" +import { Unicode11Addon } from "xterm-addon-unicode11" import "xterm/css/xterm.css" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { pageTitle } from "../../utils/page" @@ -176,6 +177,7 @@ const TerminalPage: FC = ({ renderer }) => { return } const terminal = new XTerm.Terminal({ + allowProposedApi: true, allowTransparency: true, disableStdin: false, fontFamily: MONOSPACE_FONT_FAMILY, @@ -191,6 +193,8 @@ const TerminalPage: FC = ({ renderer }) => { const fitAddon = new FitAddon() setFitAddon(fitAddon) terminal.loadAddon(fitAddon) + terminal.loadAddon(new Unicode11Addon()) + terminal.unicode.activeVersion = "11" terminal.loadAddon( new WebLinksAddon((_, uri) => { handleWebLink(uri) From 14f769d229a81c6b4d07f65e0c273f8bc977862c Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 25 Aug 2023 12:46:14 -0500 Subject: [PATCH 14/40] fix(site): use WebGL renderer for terminal (#9320) --- site/package.json | 1 + site/pnpm-lock.yaml | 11 +++++++++++ site/src/AppRouter.tsx | 2 +- site/src/pages/TerminalPage/TerminalPage.tsx | 8 ++++---- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/site/package.json b/site/package.json index 6f49efce97652..a2230d57694d2 100644 --- a/site/package.json +++ b/site/package.json @@ -105,6 +105,7 @@ "xterm-addon-fit": "0.7.0", "xterm-addon-unicode11": "0.5.0", "xterm-addon-web-links": "0.8.0", + "xterm-addon-webgl": "0.15.0", "yup": "1.2.0" }, "devDependencies": { diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 6892f56aa2ff8..f137e12289fb1 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -231,6 +231,9 @@ dependencies: xterm-addon-web-links: specifier: 0.8.0 version: 0.8.0(xterm@5.2.1) + xterm-addon-webgl: + specifier: 0.15.0 + version: 0.15.0(xterm@5.2.1) yup: specifier: 1.2.0 version: 1.2.0 @@ -14221,6 +14224,14 @@ packages: xterm: 5.2.1 dev: false + /xterm-addon-webgl@0.15.0(xterm@5.2.1): + resolution: {integrity: sha512-ZLcqogMFHr4g/YRhcCh3xE8tTklnyut/M+O/XhVsFBRB/YCvYhPdLQ5/AQk54V0wjWAQpa8CF3W8DVR9OqyMCg==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.2.1 + dev: false + /xterm@5.2.1: resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==} dev: false diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index c1f222d90c334..785048fe7298d 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -322,7 +322,7 @@ export const AppRouter: FC = () => { {/* Terminal and CLI auth pages don't have the dashboard layout */} } + element={} /> } /> diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 8522daa2d30cb..61c3501976329 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -7,7 +7,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom" import { colors } from "theme/colors" import { v4 as uuidv4 } from "uuid" import * as XTerm from "xterm" -import { CanvasAddon } from "xterm-addon-canvas" +import { WebglAddon } from "xterm-addon-webgl" import { FitAddon } from "xterm-addon-fit" import { WebLinksAddon } from "xterm-addon-web-links" import { Unicode11Addon } from "xterm-addon-unicode11" @@ -58,7 +58,7 @@ const useTerminalWarning = ({ agent }: { agent?: WorkspaceAgent }) => { } type TerminalPageProps = React.PropsWithChildren<{ - renderer: "canvas" | "dom" + renderer: "webgl" | "dom" }> const TerminalPage: FC = ({ renderer }) => { @@ -187,8 +187,8 @@ const TerminalPage: FC = ({ renderer }) => { }, }) // DOM is the default renderer. - if (renderer === "canvas") { - terminal.loadAddon(new CanvasAddon()) + if (renderer === "webgl") { + terminal.loadAddon(new WebglAddon()) } const fitAddon = new FitAddon() setFitAddon(fitAddon) From 0a213a6ac32af45b3f80102bdc90a3705d21eef1 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 25 Aug 2023 14:59:41 -0300 Subject: [PATCH 15/40] refactor(site): improve the overall user table design (#9342) --- .../EditRolesButton/EditRolesButton.tsx | 11 +- .../UsersTable/UsersTable.stories.tsx | 113 ++++++++++-------- site/src/components/UsersTable/UsersTable.tsx | 9 +- .../components/UsersTable/UsersTableBody.tsx | 113 ++++++++++++++++-- site/src/pages/UsersPage/UsersPage.tsx | 25 ++-- .../pages/UsersPage/UsersPageView.stories.tsx | 2 + site/src/pages/UsersPage/UsersPageView.tsx | 3 + .../WorkspacesPage}/LastUsed.tsx | 0 .../pages/WorkspacesPage/WorkspacesTable.tsx | 2 +- 9 files changed, 198 insertions(+), 80 deletions(-) rename site/src/{components/LastUsed => pages/WorkspacesPage}/LastUsed.tsx (100%) diff --git a/site/src/components/EditRolesButton/EditRolesButton.tsx b/site/src/components/EditRolesButton/EditRolesButton.tsx index e49902936bee3..771ba95e7068e 100644 --- a/site/src/components/EditRolesButton/EditRolesButton.tsx +++ b/site/src/components/EditRolesButton/EditRolesButton.tsx @@ -37,7 +37,7 @@ const Option: React.FC<{ onChange(e.currentTarget.value) }} /> - + {name} {description} @@ -142,7 +142,7 @@ export const EditRolesButton: FC = ({
- + {t("member")} {t("roleDescription.member")} @@ -182,7 +182,7 @@ const useStyles = makeStyles((theme) => ({ padding: 0, "&:disabled": { - opacity: 0.5, + opacity: 0, }, }, options: { @@ -190,6 +190,7 @@ const useStyles = makeStyles((theme) => ({ }, option: { cursor: "pointer", + fontSize: 14, }, checkbox: { padding: 0, @@ -202,13 +203,15 @@ const useStyles = makeStyles((theme) => ({ }, }, optionDescription: { - fontSize: 12, + fontSize: 13, color: theme.palette.text.secondary, + lineHeight: "160%", }, footer: { padding: theme.spacing(3), backgroundColor: theme.palette.background.paper, borderTop: `1px solid ${theme.palette.divider}`, + fontSize: 14, }, userIcon: { width: theme.spacing(2.5), // Same as the checkbox diff --git a/site/src/components/UsersTable/UsersTable.stories.tsx b/site/src/components/UsersTable/UsersTable.stories.tsx index 536c39253fa44..a40d44e8611c3 100644 --- a/site/src/components/UsersTable/UsersTable.stories.tsx +++ b/site/src/components/UsersTable/UsersTable.stories.tsx @@ -1,73 +1,80 @@ -import { ComponentMeta, Story } from "@storybook/react" import { MockUser, MockUser2, MockAssignableSiteRoles, + MockAuthMethods, } from "testHelpers/entities" -import { UsersTable, UsersTableProps } from "./UsersTable" +import { UsersTable } from "./UsersTable" +import type { Meta, StoryObj } from "@storybook/react" -export default { +const meta: Meta = { title: "components/UsersTable", component: UsersTable, args: { isNonInitialPage: false, + authMethods: MockAuthMethods, }, -} as ComponentMeta +} -const Template: Story = (args) => +export default meta +type Story = StoryObj -export const Example = Template.bind({}) -Example.args = { - users: [MockUser, MockUser2], - roles: MockAssignableSiteRoles, - canEditUsers: false, +export const Example: Story = { + args: { + users: [MockUser, MockUser2], + roles: MockAssignableSiteRoles, + canEditUsers: false, + }, } -export const Editable = Template.bind({}) -Editable.args = { - users: [ - MockUser, - MockUser2, - { - ...MockUser, - username: "John Doe", - email: "john.doe@coder.com", - roles: [], - status: "dormant", - }, - { - ...MockUser, - username: "Roger Moore", - email: "roger.moore@coder.com", - roles: [], - status: "suspended", - }, - { - ...MockUser, - username: "OIDC User", - email: "oidc.user@coder.com", - roles: [], - status: "active", - login_type: "oidc", - }, - ], - roles: MockAssignableSiteRoles, - canEditUsers: true, - canViewActivity: true, +export const Editable: Story = { + args: { + users: [ + MockUser, + MockUser2, + { + ...MockUser, + username: "John Doe", + email: "john.doe@coder.com", + roles: [], + status: "dormant", + }, + { + ...MockUser, + username: "Roger Moore", + email: "roger.moore@coder.com", + roles: [], + status: "suspended", + }, + { + ...MockUser, + username: "OIDC User", + email: "oidc.user@coder.com", + roles: [], + status: "active", + login_type: "oidc", + }, + ], + roles: MockAssignableSiteRoles, + canEditUsers: true, + canViewActivity: true, + }, } -export const Empty = Template.bind({}) -Empty.args = { - users: [], - roles: MockAssignableSiteRoles, +export const Empty: Story = { + args: { + users: [], + roles: MockAssignableSiteRoles, + }, } -export const Loading = Template.bind({}) -Loading.args = { - users: [], - roles: MockAssignableSiteRoles, - isLoading: true, -} -Loading.parameters = { - chromatic: { pauseAnimationAtEnd: true }, +export const Loading: Story = { + args: { + users: [], + roles: MockAssignableSiteRoles, + isLoading: true, + }, + parameters: { + chromatic: { pauseAnimationAtEnd: true }, + }, } diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index d84386a2e968e..51edb7e23c2a4 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -38,6 +38,7 @@ export interface UsersTableProps { isNonInitialPage: boolean actorID: string oidcRoleSyncEnabled: boolean + authMethods?: TypesGen.AuthMethods } export const UsersTable: FC> = ({ @@ -57,6 +58,7 @@ export const UsersTable: FC> = ({ isNonInitialPage, actorID, oidcRoleSyncEnabled, + authMethods, }) => { return ( @@ -70,10 +72,8 @@ export const UsersTable: FC> = ({ - {Language.loginTypeLabel} - {Language.statusLabel} - {Language.lastSeenLabel} - + {Language.loginTypeLabel} + {Language.statusLabel} {/* 1% is a trick to make the table cell width fit the content */} {canEditUsers && } @@ -96,6 +96,7 @@ export const UsersTable: FC> = ({ isNonInitialPage={isNonInitialPage} actorID={actorID} oidcRoleSyncEnabled={oidcRoleSyncEnabled} + authMethods={authMethods} /> diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index 82e154a3d0d0b..73abfd639a273 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -1,9 +1,8 @@ -import Box from "@mui/material/Box" -import { makeStyles } from "@mui/styles" +import Box, { BoxProps } from "@mui/material/Box" +import { makeStyles, useTheme } from "@mui/styles" import TableCell from "@mui/material/TableCell" import TableRow from "@mui/material/TableRow" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { LastUsed } from "components/LastUsed/LastUsed" import { Pill } from "components/Pill/Pill" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -16,6 +15,16 @@ import { TableRowMenu } from "../TableRowMenu/TableRowMenu" import { EditRolesButton } from "components/EditRolesButton/EditRolesButton" import { Stack } from "components/Stack/Stack" import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges" +import dayjs from "dayjs" +import { SxProps, Theme } from "@mui/material/styles" +import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined" +import KeyOutlined from "@mui/icons-material/KeyOutlined" +import GitHub from "@mui/icons-material/GitHub" +import PasswordOutlined from "@mui/icons-material/PasswordOutlined" +import relativeTime from "dayjs/plugin/relativeTime" +import ShieldOutlined from "@mui/icons-material/ShieldOutlined" + +dayjs.extend(relativeTime) const isOwnerRole = (role: TypesGen.Role): boolean => { return role.name === "owner" @@ -31,6 +40,7 @@ const sortRoles = (roles: TypesGen.Role[]) => { interface UsersTableBodyProps { users?: TypesGen.User[] + authMethods?: TypesGen.AuthMethods roles?: TypesGen.AssignableRoles[] isUpdatingUserRoles?: boolean canEditUsers?: boolean @@ -58,6 +68,7 @@ export const UsersTableBody: FC< React.PropsWithChildren > = ({ users, + authMethods, roles, onSuspendUser, onDeleteUser, @@ -80,7 +91,7 @@ export const UsersTableBody: FC< return ( - + @@ -156,7 +167,10 @@ export const UsersTableBody: FC< -
{user.login_type}
+
- {user.status} - - - + {user.status} + + {canEditUsers && ( { + let displayName = value as string + let icon = <> + const iconStyles: SxProps = { width: 14, height: 14 } + + if (value === "password") { + displayName = "Password" + icon = + } else if (value === "none") { + displayName = "None" + icon = + } else if (value === "github") { + displayName = "GitHub" + icon = + } else if (value === "token") { + displayName = "Token" + icon = + } else if (value === "oidc") { + displayName = + authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText + icon = + authMethods.oidc.iconUrl === "" ? ( + + ) : ( + + ) + } + + return ( + + {icon} + {displayName} + + ) +} + +const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => { + const theme: Theme = useTheme() + const t = dayjs(value) + const now = dayjs() + + let message = t.fromNow() + let color = theme.palette.text.secondary + + if (t.isAfter(now.subtract(1, "hour"))) { + color = theme.palette.success.light + // Since the agent reports on a 10m interval, + // the last_used_at can be inaccurate when recent. + message = "Now" + } else if (t.isAfter(now.subtract(3, "day"))) { + color = theme.palette.text.secondary + } else if (t.isAfter(now.subtract(1, "month"))) { + color = theme.palette.warning.light + } else if (t.isAfter(now.subtract(100, "year"))) { + color = theme.palette.error.light + } else { + message = "Never" + } + + return ( + + {message} + + ) +} + const useStyles = makeStyles((theme) => ({ status: { textTransform: "capitalize", diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index ad3ccb25badb2..241f150650cef 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -20,6 +20,8 @@ import { useStatusFilterMenu } from "./UsersFilter" import { useFilter } from "components/Filter/filter" import { useDashboard } from "components/Dashboard/DashboardProvider" import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine" +import { useQuery } from "@tanstack/react-query" +import { getAuthMethods } from "api/api" export const Language = { suspendDialogTitle: "Suspend user", @@ -78,16 +80,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { const oidcRoleSyncEnabled = viewDeploymentValues && deploymentValues?.config.oidc?.user_role_field !== "" - - // Is loading if - // - users are loading or - // - the user can edit the users but the roles are loading - const isLoading = - usersState.matches("gettingUsers") || - (canEditUsers && rolesState.matches("gettingRoles")) - const me = useMe() - const useFilterResult = useFilter({ searchParamsResult, onUpdate: () => { @@ -105,6 +98,19 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { status: option?.value, }), }) + const authMethods = useQuery({ + queryKey: ["authMethods"], + queryFn: () => { + return getAuthMethods() + }, + }) + // Is loading if + // - users are loading or + // - the user can edit the users but the roles are loading + const isLoading = + usersState.matches("gettingUsers") || + (canEditUsers && rolesState.matches("gettingRoles")) || + authMethods.isLoading return ( <> @@ -115,6 +121,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { oidcRoleSyncEnabled={oidcRoleSyncEnabled} roles={roles} users={users} + authMethods={authMethods.data} count={count} onListWorkspaces={(user) => { navigate( diff --git a/site/src/pages/UsersPage/UsersPageView.stories.tsx b/site/src/pages/UsersPage/UsersPageView.stories.tsx index 1b860d2d4e29f..a08a758ad3dcd 100644 --- a/site/src/pages/UsersPage/UsersPageView.stories.tsx +++ b/site/src/pages/UsersPage/UsersPageView.stories.tsx @@ -5,6 +5,7 @@ import { MockUser2, MockAssignableSiteRoles, mockApiError, + MockAuthMethods, } from "testHelpers/entities" import { UsersPageView } from "./UsersPageView" import { ComponentProps } from "react" @@ -33,6 +34,7 @@ const meta: Meta = { count: 2, canEditUsers: true, filterProps: defaultFilterProps, + authMethods: MockAuthMethods, }, } diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index f11224eb609f8..13ba8ef9c4047 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -22,6 +22,7 @@ export interface UsersPageViewProps { oidcRoleSyncEnabled: boolean canViewActivity?: boolean isLoading?: boolean + authMethods?: TypesGen.AuthMethods onSuspendUser: (user: TypesGen.User) => void onDeleteUser: (user: TypesGen.User) => void onListWorkspaces: (user: TypesGen.User) => void @@ -58,6 +59,7 @@ export const UsersPageView: FC> = ({ paginationRef, isNonInitialPage, actorID, + authMethods, }) => { return ( <> @@ -89,6 +91,7 @@ export const UsersPageView: FC> = ({ isLoading={isLoading} isNonInitialPage={isNonInitialPage} actorID={actorID} + authMethods={authMethods} /> diff --git a/site/src/components/LastUsed/LastUsed.tsx b/site/src/pages/WorkspacesPage/LastUsed.tsx similarity index 100% rename from site/src/components/LastUsed/LastUsed.tsx rename to site/src/pages/WorkspacesPage/LastUsed.tsx diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 1e15f4c362d4f..9ebffb5cfc6da 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -27,7 +27,7 @@ import Box from "@mui/material/Box" import { AvatarData } from "components/AvatarData/AvatarData" import { Avatar } from "components/Avatar/Avatar" import { Stack } from "components/Stack/Stack" -import { LastUsed } from "components/LastUsed/LastUsed" +import { LastUsed } from "pages/WorkspacesPage/LastUsed" import { WorkspaceOutdatedTooltip } from "components/Tooltips" import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { getDisplayWorkspaceTemplateName } from "utils/workspace" From d9d4d74f99fd20854dc88f55476c53cfc6fde05a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 25 Aug 2023 14:34:07 -0500 Subject: [PATCH 16/40] test: add full OIDC fake IDP (#9317) * test: implement fake OIDC provider with full functionality * Refactor existing tests --- coderd/coderdtest/coderdtest.go | 166 +----- coderd/coderdtest/oidctest/helper.go | 103 ++++ coderd/coderdtest/oidctest/idp.go | 793 +++++++++++++++++++++++++ coderd/coderdtest/oidctest/idp_test.go | 72 +++ coderd/oauthpki/oidcpki.go | 5 +- coderd/oauthpki/okidcpki_test.go | 59 +- coderd/userauth_test.go | 316 +++++----- coderd/users_test.go | 28 +- coderd/util/syncmap/map.go | 77 +++ enterprise/coderd/userauth_test.go | 603 +++++++++++-------- 10 files changed, 1596 insertions(+), 626 deletions(-) create mode 100644 coderd/coderdtest/oidctest/helper.go create mode 100644 coderd/coderdtest/oidctest/idp.go create mode 100644 coderd/coderdtest/oidctest/idp_test.go create mode 100644 coderd/util/syncmap/map.go diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 18062a549a8bd..08979a67a8c45 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -31,15 +31,13 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "github.com/coreos/go-oidc/v3/oidc" "github.com/fullsailor/pkcs7" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" "golang.org/x/xerrors" "google.golang.org/api/idtoken" "google.golang.org/api/option" @@ -1020,152 +1018,6 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif } } -type OIDCConfig struct { - key *rsa.PrivateKey - issuer string - // These are optional - refreshToken string - oidcTokenExpires func() time.Time - tokenSource func() (*oauth2.Token, error) -} - -func WithRefreshToken(token string) func(cfg *OIDCConfig) { - return func(cfg *OIDCConfig) { - cfg.refreshToken = token - } -} - -func WithTokenExpires(expFunc func() time.Time) func(cfg *OIDCConfig) { - return func(cfg *OIDCConfig) { - cfg.oidcTokenExpires = expFunc - } -} - -func WithTokenSource(src func() (*oauth2.Token, error)) func(cfg *OIDCConfig) { - return func(cfg *OIDCConfig) { - cfg.tokenSource = src - } -} - -func NewOIDCConfig(t *testing.T, issuer string, opts ...func(cfg *OIDCConfig)) *OIDCConfig { - t.Helper() - - block, _ := pem.Decode([]byte(testRSAPrivateKey)) - pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) - require.NoError(t, err) - - if issuer == "" { - issuer = "https://coder.com" - } - - cfg := &OIDCConfig{ - key: pkey, - issuer: issuer, - } - for _, opt := range opts { - opt(cfg) - } - return cfg -} - -func (*OIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string { - return "/?state=" + url.QueryEscape(state) -} - -type tokenSource struct { - src func() (*oauth2.Token, error) -} - -func (s tokenSource) Token() (*oauth2.Token, error) { - return s.src() -} - -func (cfg *OIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { - if cfg.tokenSource == nil { - return nil - } - return tokenSource{ - src: cfg.tokenSource, - } -} - -func (cfg *OIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) { - token, err := base64.StdEncoding.DecodeString(code) - if err != nil { - return nil, xerrors.Errorf("decode code: %w", err) - } - - var exp time.Time - if cfg.oidcTokenExpires != nil { - exp = cfg.oidcTokenExpires() - } - - return (&oauth2.Token{ - AccessToken: "token", - RefreshToken: cfg.refreshToken, - Expiry: exp, - }).WithExtra(map[string]interface{}{ - "id_token": string(token), - }), nil -} - -func (cfg *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string { - t.Helper() - - if _, ok := claims["exp"]; !ok { - claims["exp"] = time.Now().Add(time.Hour).UnixMilli() - } - - if _, ok := claims["iss"]; !ok { - claims["iss"] = cfg.issuer - } - - if _, ok := claims["sub"]; !ok { - claims["sub"] = "testme" - } - - signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(cfg.key) - require.NoError(t, err) - - return base64.StdEncoding.EncodeToString([]byte(signed)) -} - -func (cfg *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { - // By default, the provider can be empty. - // This means it won't support any endpoints! - provider := &oidc.Provider{} - if userInfoClaims != nil { - resp, err := json.Marshal(userInfoClaims) - require.NoError(t, err) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write(resp) - })) - t.Cleanup(srv.Close) - cfg := &oidc.ProviderConfig{ - UserInfoURL: srv.URL, - } - provider = cfg.NewProvider(context.Background()) - } - newCFG := &coderd.OIDCConfig{ - OAuth2Config: cfg, - Verifier: oidc.NewVerifier(cfg.issuer, &oidc.StaticKeySet{ - PublicKeys: []crypto.PublicKey{cfg.key.Public()}, - }, &oidc.Config{ - SkipClientIDCheck: true, - }), - Provider: provider, - UsernameField: "preferred_username", - EmailField: "email", - AuthURLParams: map[string]string{"access_type": "offline"}, - GroupField: "groups", - } - for _, opt := range opts { - opt(newCFG) - } - return newCFG -} - // NewAzureInstanceIdentity returns a metadata client and ID token validator for faking // instance authentication for Azure. func NewAzureInstanceIdentity(t *testing.T, instanceID string) (x509.VerifyOptions, *http.Client) { @@ -1254,22 +1106,6 @@ func SDKError(t *testing.T, err error) *codersdk.Error { return cerr } -const testRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDLets8+7M+iAQAqN/5BVyCIjhTQ4cmXulL+gm3v0oGMWzLupUS -v8KPA+Tp7dgC/DZPfMLaNH1obBBhJ9DhS6RdS3AS3kzeFrdu8zFHLWF53DUBhS92 -5dCAEuJpDnNizdEhxTfoHrhuCmz8l2nt1pe5eUK2XWgd08Uc93h5ij098wIDAQAB -AoGAHLaZeWGLSaen6O/rqxg2laZ+jEFbMO7zvOTruiIkL/uJfrY1kw+8RLIn+1q0 -wLcWcuEIHgKKL9IP/aXAtAoYh1FBvRPLkovF1NZB0Je/+CSGka6wvc3TGdvppZJe -rKNcUvuOYLxkmLy4g9zuY5qrxFyhtIn2qZzXEtLaVOHzPQECQQDvN0mSajpU7dTB -w4jwx7IRXGSSx65c+AsHSc1Rj++9qtPC6WsFgAfFN2CEmqhMbEUVGPv/aPjdyWk9 -pyLE9xR/AkEA2cGwyIunijE5v2rlZAD7C4vRgdcMyCf3uuPcgzFtsR6ZhyQSgLZ8 -YRPuvwm4cdPJMmO3YwBfxT6XGuSc2k8MjQJBAI0+b8prvpV2+DCQa8L/pjxp+VhR -Xrq2GozrHrgR7NRokTB88hwFRJFF6U9iogy9wOx8HA7qxEbwLZuhm/4AhbECQC2a -d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf -sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u -QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8 ------END RSA PRIVATE KEY-----` - func DeploymentValues(t testing.TB) *codersdk.DeploymentValues { var cfg codersdk.DeploymentValues opts := cfg.Options() diff --git a/coderd/coderdtest/oidctest/helper.go b/coderd/coderdtest/oidctest/helper.go new file mode 100644 index 0000000000000..11d9114be2ce8 --- /dev/null +++ b/coderd/coderdtest/oidctest/helper.go @@ -0,0 +1,103 @@ +package oidctest + +import ( + "net/http" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" +) + +// LoginHelper helps with logging in a user and refreshing their oauth tokens. +// It is mainly because refreshing oauth tokens is a bit tricky and requires +// some database manipulation. +type LoginHelper struct { + fake *FakeIDP + client *codersdk.Client +} + +func NewLoginHelper(client *codersdk.Client, fake *FakeIDP) *LoginHelper { + if client == nil { + panic("client must not be nil") + } + if fake == nil { + panic("fake must not be nil") + } + return &LoginHelper{ + fake: fake, + client: client, + } +} + +// Login just helps by making an unauthenticated client and logging in with +// the given claims. All Logins should be unauthenticated, so this is a +// convenience method. +func (h *LoginHelper) Login(t *testing.T, idTokenClaims jwt.MapClaims) (*codersdk.Client, *http.Response) { + t.Helper() + unauthenticatedClient := codersdk.New(h.client.URL) + + return h.fake.Login(t, unauthenticatedClient, idTokenClaims) +} + +// ExpireOauthToken expires the oauth token for the given user. +func (*LoginHelper) ExpireOauthToken(t *testing.T, db database.Store, user *codersdk.Client) database.UserLink { + t.Helper() + + //nolint:gocritic // Testing + ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitMedium)) + + id, _, err := httpmw.SplitAPIToken(user.SessionToken()) + require.NoError(t, err) + + // We need to get the OIDC link and update it in the database to force + // it to be expired. + key, err := db.GetAPIKeyByID(ctx, id) + require.NoError(t, err, "get api key") + + link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{ + UserID: key.UserID, + LoginType: database.LoginTypeOIDC, + }) + require.NoError(t, err, "get user link") + + // Expire the oauth link for the given user. + updated, err := db.UpdateUserLink(ctx, database.UpdateUserLinkParams{ + OAuthAccessToken: link.OAuthAccessToken, + OAuthRefreshToken: link.OAuthRefreshToken, + OAuthExpiry: time.Now().Add(time.Hour * -1), + UserID: link.UserID, + LoginType: link.LoginType, + }) + require.NoError(t, err, "expire user link") + + return updated +} + +// ForceRefresh forces the client to refresh its oauth token. It does this by +// expiring the oauth token, then doing an authenticated call. This will force +// the API Key middleware to refresh the oauth token. +// +// A unit test assertion makes sure the refresh token is used. +func (h *LoginHelper) ForceRefresh(t *testing.T, db database.Store, user *codersdk.Client, idToken jwt.MapClaims) { + t.Helper() + + link := h.ExpireOauthToken(t, db, user) + // Updates the claims that the IDP will return. By default, it always + // uses the original claims for the original oauth token. + h.fake.UpdateRefreshClaims(link.OAuthRefreshToken, idToken) + + t.Cleanup(func() { + require.True(t, h.fake.RefreshUsed(link.OAuthRefreshToken), "refresh token must be used, but has not. Did you forget to call the returned function from this call?") + }) + + // Do any authenticated call to force the refresh + _, err := user.User(testutil.Context(t, testutil.WaitShort), "me") + require.NoError(t, err, "user must be able to be fetched") +} diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go new file mode 100644 index 0000000000000..912d9acd7c221 --- /dev/null +++ b/coderd/coderdtest/oidctest/idp.go @@ -0,0 +1,793 @@ +package oidctest + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/coder/coder/v2/coderd/util/syncmap" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-chi/chi/v5" + "github.com/go-jose/go-jose/v3" + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd" + "github.com/coder/coder/v2/codersdk" +) + +// FakeIDP is a functional OIDC provider. +// It only supports 1 OIDC client. +type FakeIDP struct { + issuer string + key *rsa.PrivateKey + provider providerJSON + handler http.Handler + cfg *oauth2.Config + + // clientID to be used by coderd + clientID string + clientSecret string + logger slog.Logger + + // These maps are used to control the state of the IDP. + // That is the various access tokens, refresh tokens, states, etc. + codeToStateMap *syncmap.Map[string, string] + // Token -> Email + accessTokens *syncmap.Map[string, string] + // Refresh Token -> Email + refreshTokensUsed *syncmap.Map[string, bool] + refreshTokens *syncmap.Map[string, string] + stateToIDTokenClaims *syncmap.Map[string, jwt.MapClaims] + refreshIDTokenClaims *syncmap.Map[string, jwt.MapClaims] + + // hooks + // hookValidRedirectURL can be used to reject a redirect url from the + // IDP -> Application. Almost all IDPs have the concept of + // "Authorized Redirect URLs". This can be used to emulate that. + hookValidRedirectURL func(redirectURL string) error + hookUserInfo func(email string) jwt.MapClaims + fakeCoderd func(req *http.Request) (*http.Response, error) + hookOnRefresh func(email string) error + // Custom authentication for the client. This is useful if you want + // to test something like PKI auth vs a client_secret. + hookAuthenticateClient func(t testing.TB, req *http.Request) (url.Values, error) + serve bool +} + +type FakeIDPOpt func(idp *FakeIDP) + +func WithAuthorizedRedirectURL(hook func(redirectURL string) error) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookValidRedirectURL = hook + } +} + +// WithRefreshHook is called when a refresh token is used. The email is +// the email of the user that is being refreshed assuming the claims are correct. +func WithRefreshHook(hook func(email string) error) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookOnRefresh = hook + } +} + +func WithCustomClientAuth(hook func(t testing.TB, req *http.Request) (url.Values, error)) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookAuthenticateClient = hook + } +} + +// WithLogging is optional, but will log some HTTP calls made to the IDP. +func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) { + return func(f *FakeIDP) { + f.logger = slogtest.Make(t, options) + } +} + +// WithStaticUserInfo is optional, but will return the same user info for +// every user on the /userinfo endpoint. +func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookUserInfo = func(_ string) jwt.MapClaims { + return info + } + } +} + +func WithDynamicUserInfo(userInfoFunc func(email string) jwt.MapClaims) func(*FakeIDP) { + return func(f *FakeIDP) { + f.hookUserInfo = userInfoFunc + } +} + +// WithServing makes the IDP run an actual http server. +func WithServing() func(*FakeIDP) { + return func(f *FakeIDP) { + f.serve = true + } +} + +func WithIssuer(issuer string) func(*FakeIDP) { + return func(f *FakeIDP) { + f.issuer = issuer + } +} + +const ( + // nolint:gosec // It thinks this is a secret lol + tokenPath = "/oauth2/token" + authorizePath = "/oauth2/authorize" + keysPath = "/oauth2/keys" + userInfoPath = "/oauth2/userinfo" +) + +func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP { + t.Helper() + + block, _ := pem.Decode([]byte(testRSAPrivateKey)) + pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + + idp := &FakeIDP{ + key: pkey, + clientID: uuid.NewString(), + clientSecret: uuid.NewString(), + logger: slog.Make(), + codeToStateMap: syncmap.New[string, string](), + accessTokens: syncmap.New[string, string](), + refreshTokens: syncmap.New[string, string](), + refreshTokensUsed: syncmap.New[string, bool](), + stateToIDTokenClaims: syncmap.New[string, jwt.MapClaims](), + refreshIDTokenClaims: syncmap.New[string, jwt.MapClaims](), + hookOnRefresh: func(_ string) error { return nil }, + hookUserInfo: func(email string) jwt.MapClaims { return jwt.MapClaims{} }, + hookValidRedirectURL: func(redirectURL string) error { return nil }, + } + + for _, opt := range opts { + opt(idp) + } + + if idp.issuer == "" { + idp.issuer = "https://coder.com" + } + + idp.handler = idp.httpHandler(t) + idp.updateIssuerURL(t, idp.issuer) + if idp.serve { + idp.realServer(t) + } + + return idp +} + +func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) { + t.Helper() + + u, err := url.Parse(issuer) + require.NoError(t, err, "invalid issuer URL") + + f.issuer = issuer + // providerJSON is the JSON representation of the OpenID Connect provider + // These are all the urls that the IDP will respond to. + f.provider = providerJSON{ + Issuer: issuer, + AuthURL: u.ResolveReference(&url.URL{Path: authorizePath}).String(), + TokenURL: u.ResolveReference(&url.URL{Path: tokenPath}).String(), + JWKSURL: u.ResolveReference(&url.URL{Path: keysPath}).String(), + UserInfoURL: u.ResolveReference(&url.URL{Path: userInfoPath}).String(), + Algorithms: []string{ + "RS256", + }, + } +} + +// realServer turns the FakeIDP into a real http server. +func (f *FakeIDP) realServer(t testing.TB) *httptest.Server { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + srv := httptest.NewUnstartedServer(f.handler) + srv.Config.BaseContext = func(_ net.Listener) context.Context { + return ctx + } + srv.Start() + t.Cleanup(srv.CloseClientConnections) + t.Cleanup(srv.Close) + t.Cleanup(cancel) + + f.updateIssuerURL(t, srv.URL) + return srv +} + +// Login does the full OIDC flow starting at the "LoginButton". +// The client argument is just to get the URL of the Coder instance. +// +// The client passed in is just to get the url of the Coder instance. +// The actual client that is used is 100% unauthenticated and fresh. +func (f *FakeIDP) Login(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) { + t.Helper() + + client, resp := f.AttemptLogin(t, client, idTokenClaims, opts...) + require.Equal(t, http.StatusOK, resp.StatusCode, "client failed to login") + return client, resp +} + +func (f *FakeIDP) AttemptLogin(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) { + t.Helper() + var err error + + cli := f.HTTPClient(client.HTTPClient) + shallowCpyCli := *cli + + if shallowCpyCli.Jar == nil { + shallowCpyCli.Jar, err = cookiejar.New(nil) + require.NoError(t, err, "failed to create cookie jar") + } + + unauthenticated := codersdk.New(client.URL) + unauthenticated.HTTPClient = &shallowCpyCli + + return f.LoginWithClient(t, unauthenticated, idTokenClaims, opts...) +} + +// LoginWithClient reuses the context of the passed in client. This means the same +// cookies will be used. This should be an unauthenticated client in most cases. +// +// This is a niche case, but it is needed for testing ConvertLoginType. +func (f *FakeIDP) LoginWithClient(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) { + t.Helper() + + coderOauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback") + require.NoError(t, err) + f.SetRedirect(t, coderOauthURL.String()) + + cli := f.HTTPClient(client.HTTPClient) + cli.CheckRedirect = func(req *http.Request, via []*http.Request) error { + // Store the idTokenClaims to the specific state request. This ties + // the claims 1:1 with a given authentication flow. + state := req.URL.Query().Get("state") + f.stateToIDTokenClaims.Store(state, idTokenClaims) + return nil + } + + req, err := http.NewRequestWithContext(context.Background(), "GET", coderOauthURL.String(), nil) + require.NoError(t, err) + if cli.Jar == nil { + cli.Jar, err = cookiejar.New(nil) + require.NoError(t, err, "failed to create cookie jar") + } + + for _, opt := range opts { + opt(req) + } + + res, err := cli.Do(req) + require.NoError(t, err) + + // If the coder session token exists, return the new authed client! + var user *codersdk.Client + cookies := cli.Jar.Cookies(client.URL) + for _, cookie := range cookies { + if cookie.Name == codersdk.SessionTokenCookie { + user = codersdk.New(client.URL) + user.SetSessionToken(cookie.Value) + } + } + + t.Cleanup(func() { + if res.Body != nil { + _ = res.Body.Close() + } + }) + + return user, res +} + +// OIDCCallback will emulate the IDP redirecting back to the Coder callback. +// This is helpful if no Coderd exists because the IDP needs to redirect to +// something. +// Essentially this is used to fake the Coderd side of the exchange. +// The flow starts at the user hitting the OIDC login page. +func (f *FakeIDP) OIDCCallback(t testing.TB, state string, idTokenClaims jwt.MapClaims) (*http.Response, error) { + t.Helper() + if f.serve { + panic("cannot use OIDCCallback with WithServing. This is only for the in memory usage") + } + + f.stateToIDTokenClaims.Store(state, idTokenClaims) + + cli := f.HTTPClient(nil) + u := f.cfg.AuthCodeURL(state) + req, err := http.NewRequest("GET", u, nil) + require.NoError(t, err) + + resp, err := cli.Do(req.WithContext(context.Background())) + require.NoError(t, err) + + t.Cleanup(func() { + if resp.Body != nil { + _ = resp.Body.Close() + } + }) + return resp, nil +} + +type providerJSON struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + JWKSURL string `json:"jwks_uri"` + UserInfoURL string `json:"userinfo_endpoint"` + Algorithms []string `json:"id_token_signing_alg_values_supported"` +} + +// newCode enforces the code exchanged is actually a valid code +// created by the IDP. +func (f *FakeIDP) newCode(state string) string { + code := uuid.NewString() + f.codeToStateMap.Store(code, state) + return code +} + +// newToken enforces the access token exchanged is actually a valid access token +// created by the IDP. +func (f *FakeIDP) newToken(email string) string { + accessToken := uuid.NewString() + f.accessTokens.Store(accessToken, email) + return accessToken +} + +func (f *FakeIDP) newRefreshTokens(email string) string { + refreshToken := uuid.NewString() + f.refreshTokens.Store(refreshToken, email) + return refreshToken +} + +// authenticateBearerTokenRequest enforces the access token is valid. +func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request) (string, error) { + t.Helper() + + auth := req.Header.Get("Authorization") + token := strings.TrimPrefix(auth, "Bearer ") + _, ok := f.accessTokens.Load(token) + if !ok { + return "", xerrors.New("invalid access token") + } + return token, nil +} + +// authenticateOIDCClientRequest enforces the client_id and client_secret are valid. +func (f *FakeIDP) authenticateOIDCClientRequest(t testing.TB, req *http.Request) (url.Values, error) { + t.Helper() + + if f.hookAuthenticateClient != nil { + return f.hookAuthenticateClient(t, req) + } + + data, err := io.ReadAll(req.Body) + if !assert.NoError(t, err, "read token request body") { + return nil, xerrors.Errorf("authenticate request, read body: %w", err) + } + values, err := url.ParseQuery(string(data)) + if !assert.NoError(t, err, "parse token request values") { + return nil, xerrors.New("invalid token request") + } + + if !assert.Equal(t, f.clientID, values.Get("client_id"), "client_id mismatch") { + return nil, xerrors.New("client_id mismatch") + } + + if !assert.Equal(t, f.clientSecret, values.Get("client_secret"), "client_secret mismatch") { + return nil, xerrors.New("client_secret mismatch") + } + + return values, nil +} + +// encodeClaims is a helper func to convert claims to a valid JWT. +func (f *FakeIDP) encodeClaims(t testing.TB, claims jwt.MapClaims) string { + t.Helper() + + if _, ok := claims["exp"]; !ok { + claims["exp"] = time.Now().Add(time.Hour).UnixMilli() + } + + if _, ok := claims["aud"]; !ok { + claims["aud"] = f.clientID + } + + if _, ok := claims["iss"]; !ok { + claims["iss"] = f.issuer + } + + signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(f.key) + require.NoError(t, err) + + return signed +} + +// httpHandler is the IDP http server. +func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { + t.Helper() + + mux := chi.NewMux() + // This endpoint is required to initialize the OIDC provider. + // It is used to get the OIDC configuration. + mux.Get("/.well-known/openid-configuration", func(rw http.ResponseWriter, r *http.Request) { + f.logger.Info(r.Context(), "http OIDC config", slog.F("url", r.URL.String())) + + _ = json.NewEncoder(rw).Encode(f.provider) + }) + + // Authorize is called when the user is redirected to the IDP to login. + // This is the browser hitting the IDP and the user logging into Google or + // w/e and clicking "Allow". They will be redirected back to the redirect + // when this is done. + mux.Handle(authorizePath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + f.logger.Info(r.Context(), "http call authorize", slog.F("url", r.URL.String())) + + clientID := r.URL.Query().Get("client_id") + if !assert.Equal(t, f.clientID, clientID, "unexpected client_id") { + http.Error(rw, "invalid client_id", http.StatusBadRequest) + return + } + + redirectURI := r.URL.Query().Get("redirect_uri") + state := r.URL.Query().Get("state") + + scope := r.URL.Query().Get("scope") + assert.NotEmpty(t, scope, "scope is empty") + + responseType := r.URL.Query().Get("response_type") + switch responseType { + case "code": + case "token": + t.Errorf("response_type %q not supported", responseType) + http.Error(rw, "invalid response_type", http.StatusBadRequest) + return + default: + t.Errorf("unexpected response_type %q", responseType) + http.Error(rw, "invalid response_type", http.StatusBadRequest) + return + } + + err := f.hookValidRedirectURL(redirectURI) + if err != nil { + t.Errorf("not authorized redirect_uri by custom hook %q: %s", redirectURI, err.Error()) + http.Error(rw, fmt.Sprintf("invalid redirect_uri: %s", err.Error()), http.StatusBadRequest) + return + } + + ru, err := url.Parse(redirectURI) + if err != nil { + t.Errorf("invalid redirect_uri %q: %s", redirectURI, err.Error()) + http.Error(rw, fmt.Sprintf("invalid redirect_uri: %s", err.Error()), http.StatusBadRequest) + return + } + + q := ru.Query() + q.Set("state", state) + q.Set("code", f.newCode(state)) + ru.RawQuery = q.Encode() + + http.Redirect(rw, r, ru.String(), http.StatusTemporaryRedirect) + })) + + mux.Handle(tokenPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + values, err := f.authenticateOIDCClientRequest(t, r) + f.logger.Info(r.Context(), "http idp call token", + slog.Error(err), + slog.F("values", values.Encode()), + ) + if err != nil { + http.Error(rw, fmt.Sprintf("invalid token request: %s", err.Error()), http.StatusBadRequest) + return + } + getEmail := func(claims jwt.MapClaims) string { + email, ok := claims["email"] + if !ok { + return "unknown" + } + emailStr, ok := email.(string) + if !ok { + return "wrong-type" + } + return emailStr + } + + var claims jwt.MapClaims + switch values.Get("grant_type") { + case "authorization_code": + code := values.Get("code") + if !assert.NotEmpty(t, code, "code is empty") { + http.Error(rw, "invalid code", http.StatusBadRequest) + return + } + stateStr, ok := f.codeToStateMap.Load(code) + if !assert.True(t, ok, "invalid code") { + http.Error(rw, "invalid code", http.StatusBadRequest) + return + } + // Always invalidate the code after it is used. + f.codeToStateMap.Delete(code) + + idTokenClaims, ok := f.stateToIDTokenClaims.Load(stateStr) + if !ok { + t.Errorf("missing id token claims") + http.Error(rw, "missing id token claims", http.StatusBadRequest) + return + } + claims = idTokenClaims + case "refresh_token": + refreshToken := values.Get("refresh_token") + if !assert.NotEmpty(t, refreshToken, "refresh_token is empty") { + http.Error(rw, "invalid refresh_token", http.StatusBadRequest) + return + } + + _, ok := f.refreshTokens.Load(refreshToken) + if !assert.True(t, ok, "invalid refresh_token") { + http.Error(rw, "invalid refresh_token", http.StatusBadRequest) + return + } + + idTokenClaims, ok := f.refreshIDTokenClaims.Load(refreshToken) + if !ok { + t.Errorf("missing id token claims in refresh") + http.Error(rw, "missing id token claims in refresh", http.StatusBadRequest) + return + } + + claims = idTokenClaims + err := f.hookOnRefresh(getEmail(claims)) + if err != nil { + http.Error(rw, fmt.Sprintf("refresh hook blocked refresh: %s", err.Error()), http.StatusBadRequest) + return + } + + f.refreshTokensUsed.Store(refreshToken, true) + // Always invalidate the refresh token after it is used. + f.refreshTokens.Delete(refreshToken) + default: + t.Errorf("unexpected grant_type %q", values.Get("grant_type")) + http.Error(rw, "invalid grant_type", http.StatusBadRequest) + return + } + + exp := time.Now().Add(time.Minute * 5) + claims["exp"] = exp.UnixMilli() + email := getEmail(claims) + refreshToken := f.newRefreshTokens(email) + token := map[string]interface{}{ + "access_token": f.newToken(email), + "refresh_token": refreshToken, + "token_type": "Bearer", + "expires_in": int64((time.Minute * 5).Seconds()), + "id_token": f.encodeClaims(t, claims), + } + // Store the claims for the next refresh + f.refreshIDTokenClaims.Store(refreshToken, claims) + + rw.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(rw).Encode(token) + })) + + mux.Handle(userInfoPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + token, err := f.authenticateBearerTokenRequest(t, r) + f.logger.Info(r.Context(), "http call idp user info", + slog.Error(err), + slog.F("url", r.URL.String()), + ) + if err != nil { + http.Error(rw, fmt.Sprintf("invalid user info request: %s", err.Error()), http.StatusBadRequest) + return + } + + email, ok := f.accessTokens.Load(token) + if !ok { + t.Errorf("access token user for user_info has no email to indicate which user") + http.Error(rw, "invalid access token, missing user info", http.StatusBadRequest) + return + } + _ = json.NewEncoder(rw).Encode(f.hookUserInfo(email)) + })) + + mux.Handle(keysPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + f.logger.Info(r.Context(), "http call idp /keys") + set := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: f.key.Public(), + KeyID: "test-key", + Algorithm: "RSA", + }, + }, + } + _ = json.NewEncoder(rw).Encode(set) + })) + + mux.NotFound(func(rw http.ResponseWriter, r *http.Request) { + f.logger.Error(r.Context(), "http call not found", slog.F("path", r.URL.Path)) + t.Errorf("unexpected request to IDP at path %q. Not supported", r.URL.Path) + }) + + return mux +} + +// HTTPClient does nothing if IsServing is used. +// +// If IsServing is not used, then it will return a client that will make requests +// to the IDP all in memory. If a request is not to the IDP, then the passed in +// client will be used. If no client is passed in, then any regular network +// requests will fail. +func (f *FakeIDP) HTTPClient(rest *http.Client) *http.Client { + if f.serve { + if rest == nil || rest.Transport == nil { + return &http.Client{} + } + return rest + } + + var jar http.CookieJar + if rest != nil { + jar = rest.Jar + } + return &http.Client{ + Jar: jar, + Transport: fakeRoundTripper{ + roundTrip: func(req *http.Request) (*http.Response, error) { + u, _ := url.Parse(f.issuer) + if req.URL.Host != u.Host { + if f.fakeCoderd != nil { + return f.fakeCoderd(req) + } + if rest == nil || rest.Transport == nil { + return nil, fmt.Errorf("unexpected network request to %q", req.URL.Host) + } + return rest.Transport.RoundTrip(req) + } + resp := httptest.NewRecorder() + f.handler.ServeHTTP(resp, req) + return resp.Result(), nil + }, + }, + } +} + +// RefreshUsed returns if the refresh token has been used. All refresh tokens +// can only be used once, then they are deleted. +func (f *FakeIDP) RefreshUsed(refreshToken string) bool { + used, _ := f.refreshTokensUsed.Load(refreshToken) + return used +} + +// UpdateRefreshClaims allows the caller to change what claims are returned +// for a given refresh token. By default, all refreshes use the same claims as +// the original IDToken issuance. +func (f *FakeIDP) UpdateRefreshClaims(refreshToken string, claims jwt.MapClaims) { + f.refreshIDTokenClaims.Store(refreshToken, claims) +} + +// SetRedirect is required for the IDP to know where to redirect and call +// Coderd. +func (f *FakeIDP) SetRedirect(t testing.TB, u string) { + t.Helper() + + f.cfg.RedirectURL = u +} + +// SetCoderdCallback is optional and only works if not using the IsServing. +// It will setup a fake "Coderd" for the IDP to call when the IDP redirects +// back after authenticating. +func (f *FakeIDP) SetCoderdCallback(callback func(req *http.Request) (*http.Response, error)) { + if f.serve { + panic("cannot set callback handler when using 'WithServing'. Must implement an actual 'Coderd'") + } + f.fakeCoderd = callback +} + +func (f *FakeIDP) SetCoderdCallbackHandler(handler http.HandlerFunc) { + f.SetCoderdCallback(func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + return resp.Result(), nil + }) +} + +// OIDCConfig returns the OIDC config to use for Coderd. +func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { + t.Helper() + if len(scopes) == 0 { + scopes = []string{"openid", "email", "profile"} + } + + oauthCfg := &oauth2.Config{ + ClientID: f.clientID, + ClientSecret: f.clientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: f.provider.AuthURL, + TokenURL: f.provider.TokenURL, + AuthStyle: oauth2.AuthStyleInParams, + }, + // If the user is using a real network request, they will need to do + // 'fake.SetRedirect()' + RedirectURL: "https://redirect.com", + Scopes: scopes, + } + + ctx := oidc.ClientContext(context.Background(), f.HTTPClient(nil)) + p, err := oidc.NewProvider(ctx, f.provider.Issuer) + require.NoError(t, err, "failed to create OIDC provider") + cfg := &coderd.OIDCConfig{ + OAuth2Config: oauthCfg, + Provider: p, + Verifier: oidc.NewVerifier(f.provider.Issuer, &oidc.StaticKeySet{ + PublicKeys: []crypto.PublicKey{f.key.Public()}, + }, &oidc.Config{ + ClientID: oauthCfg.ClientID, + SupportedSigningAlgs: []string{ + "RS256", + }, + // Todo: add support for Now() + }), + UsernameField: "preferred_username", + EmailField: "email", + AuthURLParams: map[string]string{"access_type": "offline"}, + } + + for _, opt := range opts { + if opt == nil { + continue + } + opt(cfg) + } + + f.cfg = oauthCfg + + return cfg +} + +type fakeRoundTripper struct { + roundTrip func(req *http.Request) (*http.Response, error) +} + +func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return f.roundTrip(req) +} + +const testRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDLets8+7M+iAQAqN/5BVyCIjhTQ4cmXulL+gm3v0oGMWzLupUS +v8KPA+Tp7dgC/DZPfMLaNH1obBBhJ9DhS6RdS3AS3kzeFrdu8zFHLWF53DUBhS92 +5dCAEuJpDnNizdEhxTfoHrhuCmz8l2nt1pe5eUK2XWgd08Uc93h5ij098wIDAQAB +AoGAHLaZeWGLSaen6O/rqxg2laZ+jEFbMO7zvOTruiIkL/uJfrY1kw+8RLIn+1q0 +wLcWcuEIHgKKL9IP/aXAtAoYh1FBvRPLkovF1NZB0Je/+CSGka6wvc3TGdvppZJe +rKNcUvuOYLxkmLy4g9zuY5qrxFyhtIn2qZzXEtLaVOHzPQECQQDvN0mSajpU7dTB +w4jwx7IRXGSSx65c+AsHSc1Rj++9qtPC6WsFgAfFN2CEmqhMbEUVGPv/aPjdyWk9 +pyLE9xR/AkEA2cGwyIunijE5v2rlZAD7C4vRgdcMyCf3uuPcgzFtsR6ZhyQSgLZ8 +YRPuvwm4cdPJMmO3YwBfxT6XGuSc2k8MjQJBAI0+b8prvpV2+DCQa8L/pjxp+VhR +Xrq2GozrHrgR7NRokTB88hwFRJFF6U9iogy9wOx8HA7qxEbwLZuhm/4AhbECQC2a +d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf +sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u +QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8 +-----END RSA PRIVATE KEY-----` diff --git a/coderd/coderdtest/oidctest/idp_test.go b/coderd/coderdtest/oidctest/idp_test.go new file mode 100644 index 0000000000000..0dc1149d93fa9 --- /dev/null +++ b/coderd/coderdtest/oidctest/idp_test.go @@ -0,0 +1,72 @@ +package oidctest_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" +) + +// TestFakeIDPBasicFlow tests the basic flow of the fake IDP. +// It is done all in memory with no actual network requests. +// nolint:bodyclose +func TestFakeIDPBasicFlow(t *testing.T) { + t.Parallel() + + fake := oidctest.NewFakeIDP(t, + oidctest.WithLogging(t, nil), + ) + + var handler http.Handler + srv := httptest.NewServer(http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler.ServeHTTP(w, r) + }))) + defer srv.Close() + + cfg := fake.OIDCConfig(t, nil) + cli := fake.HTTPClient(nil) + ctx := oidc.ClientContext(context.Background(), cli) + + const expectedState = "random-state" + var token *oauth2.Token + // This is the Coder callback using an actual network request. + fake.SetCoderdCallbackHandler(func(w http.ResponseWriter, r *http.Request) { + // Emulate OIDC flow + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + assert.Equal(t, expectedState, state, "state mismatch") + + oauthToken, err := cfg.Exchange(ctx, code) + if assert.NoError(t, err, "failed to exchange code") { + assert.NotEmpty(t, oauthToken.AccessToken, "access token is empty") + assert.NotEmpty(t, oauthToken.RefreshToken, "refresh token is empty") + } + token = oauthToken + }) + + resp, err := fake.OIDCCallback(t, expectedState, jwt.MapClaims{}) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Test the user info + _, err = cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token)) + require.NoError(t, err) + + // Now test it can refresh + refreshed, err := cfg.TokenSource(ctx, &oauth2.Token{ + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Expiry: time.Now().Add(time.Minute * -1), + }).Token() + require.NoError(t, err, "failed to refresh token") + require.NotEmpty(t, refreshed.AccessToken, "access token is empty on refresh") +} diff --git a/coderd/oauthpki/oidcpki.go b/coderd/oauthpki/oidcpki.go index d5bc625336ab7..c44d130e5be9f 100644 --- a/coderd/oauthpki/oidcpki.go +++ b/coderd/oauthpki/oidcpki.go @@ -215,7 +215,10 @@ func (src *jwtTokenSource) Token() (*oauth2.Token, error) { } var tokenRes struct { - oauth2.Token + AccessToken string `json:"access_token"` + TokenType string `json:"token_type,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + // Extra fields returned by the refresh that are needed IDToken string `json:"id_token"` ExpiresIn int64 `json:"expires_in"` // relative seconds from now diff --git a/coderd/oauthpki/okidcpki_test.go b/coderd/oauthpki/okidcpki_test.go index 27593607f2a16..ab6e3e3a08179 100644 --- a/coderd/oauthpki/okidcpki_test.go +++ b/coderd/oauthpki/okidcpki_test.go @@ -12,12 +12,15 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "golang.org/x/xerrors" + "github.com/coder/coder/v2/coderd" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coder/coder/v2/coderd/oauthpki" "github.com/coder/coder/v2/testutil" ) @@ -123,6 +126,58 @@ func TestAzureADPKIOIDC(t *testing.T) { require.Error(t, err, "error expected") } +// TestAzureAKPKIWithCoderd uses a fake IDP and a real Coderd to test PKI auth. +// nolint:bodyclose +func TestAzureAKPKIWithCoderd(t *testing.T) { + t.Parallel() + + scopes := []string{"openid", "email", "profile", "offline_access"} + fake := oidctest.NewFakeIDP(t, + oidctest.WithIssuer("https://login.microsoftonline.com/fake_app"), + oidctest.WithCustomClientAuth(func(t testing.TB, req *http.Request) (url.Values, error) { + values := assertJWTAuth(t, req) + if values == nil { + return nil, xerrors.New("authorizatin failed in request") + } + return values, nil + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, scopes, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + }) + + oauthCfg := cfg.OAuth2Config.(*oauth2.Config) + // Create the oauthpki config + pki, err := oauthpki.NewOauth2PKIConfig(oauthpki.ConfigParams{ + ClientID: oauthCfg.ClientID, + TokenURL: oauthCfg.Endpoint.TokenURL, + Scopes: scopes, + PemEncodedKey: []byte(testClientKey), + PemEncodedCert: []byte(testClientCert), + Config: oauthCfg, + }) + require.NoError(t, err) + cfg.OAuth2Config = pki + + owner, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + OIDCConfig: cfg, + }) + + // Create a user and login + const email = "alice@coder.com" + claims := jwt.MapClaims{ + "email": email, + } + helper := oidctest.NewLoginHelper(owner, fake) + user, _ := helper.Login(t, claims) + + // Try refreshing the token more than once. + for i := 0; i < 2; i++ { + helper.ForceRefresh(t, api.Database, user, claims) + } +} + // TestSavedAzureADPKIOIDC was created by capturing actual responses from an Azure // AD instance and saving them to replay, removing some details. // The reason this is done is that this is the only way to assert values @@ -269,7 +324,7 @@ func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // assertJWTAuth will assert the basic JWT auth assertions. It will return the // url.Values from the request body for any additional assertions to be made. -func assertJWTAuth(t *testing.T, r *http.Request) url.Values { +func assertJWTAuth(t testing.TB, r *http.Request) url.Values { body, err := io.ReadAll(r.Body) if !assert.NoError(t, err) { return nil diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 10bf7ecf67234..1f37a0721a1e7 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -4,28 +4,25 @@ import ( "context" "crypto" "fmt" - "io" "net/http" "net/http/cookiejar" + "net/url" "strings" "testing" - "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" "github.com/google/go-github/v43/github" "github.com/google/uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" "golang.org/x/xerrors" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/codersdk" @@ -35,85 +32,42 @@ import ( // This test specifically tests logging in with OIDC when an expired // OIDC session token exists. // The token refreshing should not happen since we are reauthenticating. +// nolint:bodyclose func TestOIDCOauthLoginWithExisting(t *testing.T) { t.Parallel() - conf := coderdtest.NewOIDCConfig(t, "", - // Provide a refresh token so we use the refresh token flow - coderdtest.WithRefreshToken("refresh_token"), - // We need to set the expire in the future for the first api calls. - coderdtest.WithTokenExpires(func() time.Time { - return time.Now().Add(time.Hour).UTC() - }), - // No refresh should actually happen in this test. - coderdtest.WithTokenSource(func() (*oauth2.Token, error) { - return nil, xerrors.New("token should not require refresh") + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") }), + oidctest.WithServing(), ) - logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) - auditor := audit.NewMock() + + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.IgnoreUserInfo = true + }) + + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + OIDCConfig: cfg, + }) + const username = "alice" claims := jwt.MapClaims{ "email": "alice@coder.com", "email_verified": true, "preferred_username": username, } - config := conf.OIDCConfig(t, claims) - - config.AllowSignups = true - config.IgnoreUserInfo = true - client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - Auditor: auditor, - OIDCConfig: config, - Logger: &logger, - }) + helper := oidctest.NewLoginHelper(client, fake) // Signup alice - resp := oidcCallback(t, client, conf.EncodeClaims(t, claims)) - // Set the client to use this OIDC context - authCookie := authCookieValue(resp.Cookies()) - client.SetSessionToken(authCookie) - _ = resp.Body.Close() + userClient, _ := helper.Login(t, claims) - ctx := testutil.Context(t, testutil.WaitLong) - // Verify the user and oauth link - user, err := client.User(ctx, "me") - require.NoError(t, err) - require.Equal(t, username, user.Username) - - // nolint:gocritic - link, err := api.Database.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{ - UserID: user.ID, - LoginType: database.LoginType(user.LoginType), - }) - require.NoError(t, err, "failed to get user link") - - // Expire the link - // nolint:gocritic - _, err = api.Database.UpdateUserLink(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLinkParams{ - OAuthAccessToken: link.OAuthAccessToken, - OAuthRefreshToken: link.OAuthRefreshToken, - OAuthExpiry: time.Now().Add(time.Hour * -1).UTC(), - UserID: link.UserID, - LoginType: link.LoginType, - }) - require.NoError(t, err, "failed to update user link") - - // Log in again with OIDC - loginAgain := oidcCallbackWithState(t, client, conf.EncodeClaims(t, claims), "seconds_login", func(req *http.Request) { - req.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenCookie, - Value: authCookie, - Path: "/", - }) - }) - require.Equal(t, http.StatusTemporaryRedirect, loginAgain.StatusCode) - _ = loginAgain.Body.Close() + // Expire the link. This will force the client to refresh the token. + helper.ExpireOauthToken(t, api.Database, userClient) - // Try to use new login - client.SetSessionToken(authCookieValue(resp.Cookies())) - _, err = client.User(ctx, "me") - require.NoError(t, err, "use new session") + // Instead of refreshing, just log in again. + helper.Login(t, claims) } func TestUserLogin(t *testing.T) { @@ -660,7 +614,7 @@ func TestUserOIDC(t *testing.T) { "email": "kyle@kwc.io", }, AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, Username: "kyle", }, { Name: "EmailNotVerified", @@ -685,7 +639,7 @@ func TestUserOIDC(t *testing.T) { "email_verified": false, }, AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, Username: "kyle", IgnoreEmailVerified: true, }, { @@ -709,7 +663,7 @@ func TestUserOIDC(t *testing.T) { EmailDomain: []string{ "kwc.io", }, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "EmptyClaims", IDTokenClaims: jwt.MapClaims{}, @@ -730,7 +684,7 @@ func TestUserOIDC(t *testing.T) { }, Username: "kyle", AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "UsernameFromClaims", IDTokenClaims: jwt.MapClaims{ @@ -740,7 +694,7 @@ func TestUserOIDC(t *testing.T) { }, Username: "hotdog", AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { // Services like Okta return the email as the username: // https://developer.okta.com/docs/reference/api/oidc/#base-claims-always-present @@ -752,7 +706,7 @@ func TestUserOIDC(t *testing.T) { }, Username: "kyle", AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { // See: https://github.com/coder/coder/issues/4472 Name: "UsernameIsEmail", @@ -761,7 +715,7 @@ func TestUserOIDC(t *testing.T) { }, Username: "kyle", AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "WithPicture", IDTokenClaims: jwt.MapClaims{ @@ -773,7 +727,7 @@ func TestUserOIDC(t *testing.T) { Username: "kyle", AllowSignups: true, AvatarURL: "/example.png", - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "WithUserInfoClaims", IDTokenClaims: jwt.MapClaims{ @@ -787,7 +741,7 @@ func TestUserOIDC(t *testing.T) { Username: "potato", AllowSignups: true, AvatarURL: "/example.png", - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "GroupsDoesNothing", IDTokenClaims: jwt.MapClaims{ @@ -795,7 +749,7 @@ func TestUserOIDC(t *testing.T) { "groups": []string{"pingpong"}, }, AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "UserInfoOverridesIDTokenClaims", IDTokenClaims: jwt.MapClaims{ @@ -810,7 +764,7 @@ func TestUserOIDC(t *testing.T) { Username: "user", AllowSignups: true, IgnoreEmailVerified: false, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }, { Name: "InvalidUserInfo", IDTokenClaims: jwt.MapClaims{ @@ -837,36 +791,41 @@ func TestUserOIDC(t *testing.T) { Username: "user", IgnoreUserInfo: true, AllowSignups: true, - StatusCode: http.StatusTemporaryRedirect, + StatusCode: http.StatusOK, }} { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() - conf := coderdtest.NewOIDCConfig(t, "") - - config := conf.OIDCConfig(t, tc.UserInfoClaims) - config.AllowSignups = tc.AllowSignups - config.EmailDomain = tc.EmailDomain - config.IgnoreEmailVerified = tc.IgnoreEmailVerified - config.IgnoreUserInfo = tc.IgnoreUserInfo + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + oidctest.WithStaticUserInfo(tc.UserInfoClaims), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = tc.AllowSignups + cfg.EmailDomain = tc.EmailDomain + cfg.IgnoreEmailVerified = tc.IgnoreEmailVerified + cfg.IgnoreUserInfo = tc.IgnoreUserInfo + }) + auditor := audit.NewMock() logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) - client := coderdtest.New(t, &coderdtest.Options{ + owner := coderdtest.New(t, &coderdtest.Options{ Auditor: auditor, - OIDCConfig: config, + OIDCConfig: cfg, Logger: &logger, }) numLogs := len(auditor.AuditLogs()) - resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.IDTokenClaims)) + client, resp := fake.AttemptLogin(t, owner, tc.IDTokenClaims) numLogs++ // add an audit log for login - assert.Equal(t, tc.StatusCode, resp.StatusCode) + require.Equal(t, tc.StatusCode, resp.StatusCode) ctx := testutil.Context(t, testutil.WaitLong) if tc.Username != "" { - client.SetSessionToken(authCookieValue(resp.Cookies())) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.Username, user.Username) @@ -877,7 +836,6 @@ func TestUserOIDC(t *testing.T) { } if tc.AvatarURL != "" { - client.SetSessionToken(authCookieValue(resp.Cookies())) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.AvatarURL, user.AvatarURL) @@ -890,26 +848,29 @@ func TestUserOIDC(t *testing.T) { t.Run("OIDCConvert", func(t *testing.T) { t.Parallel() - auditor := audit.NewMock() - conf := coderdtest.NewOIDCConfig(t, "") - config := conf.OIDCConfig(t, nil) - config.AllowSignups = true + auditor := audit.NewMock() + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + }) - cfg := coderdtest.DeploymentValues(t) client := coderdtest.New(t, &coderdtest.Options{ - Auditor: auditor, - OIDCConfig: config, - DeploymentValues: cfg, + Auditor: auditor, + OIDCConfig: cfg, }) - owner := coderdtest.CreateFirstUser(t, client) + owner := coderdtest.CreateFirstUser(t, client) user, userData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - code := conf.EncodeClaims(t, jwt.MapClaims{ + claims := jwt.MapClaims{ "email": userData.Email, - }) - + } var err error user.HTTPClient.Jar, err = cookiejar.New(nil) require.NoError(t, err) @@ -921,52 +882,58 @@ func TestUserOIDC(t *testing.T) { }) require.NoError(t, err) - resp := oidcCallbackWithState(t, user, code, convertResponse.StateString, nil) - require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + fake.LoginWithClient(t, user, claims, func(r *http.Request) { + r.URL.RawQuery = url.Values{ + "oidc_merge_state": {convertResponse.StateString}, + }.Encode() + r.Header.Set(codersdk.SessionTokenHeader, user.SessionToken()) + cookies := user.HTTPClient.Jar.Cookies(r.URL) + for _, cookie := range cookies { + r.AddCookie(cookie) + } + }) }) t.Run("AlternateUsername", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() - conf := coderdtest.NewOIDCConfig(t, "") - - config := conf.OIDCConfig(t, nil) - config.AllowSignups = true + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + }) client := coderdtest.New(t, &coderdtest.Options{ Auditor: auditor, - OIDCConfig: config, + OIDCConfig: cfg, }) - numLogs := len(auditor.AuditLogs()) - code := conf.EncodeClaims(t, jwt.MapClaims{ + numLogs := len(auditor.AuditLogs()) + claims := jwt.MapClaims{ "email": "jon@coder.com", - }) - resp := oidcCallback(t, client, code) - numLogs++ // add an audit log for login + } - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + userClient, _ := fake.Login(t, client, claims) + numLogs++ // add an audit log for login ctx := testutil.Context(t, testutil.WaitLong) - - client.SetSessionToken(authCookieValue(resp.Cookies())) - user, err := client.User(ctx, "me") + user, err := userClient.User(ctx, "me") require.NoError(t, err) require.Equal(t, "jon", user.Username) // Pass a different subject field so that we prompt creating a - // new user. - code = conf.EncodeClaims(t, jwt.MapClaims{ + // new user + userClient, _ = fake.Login(t, client, jwt.MapClaims{ "email": "jon@example2.com", "sub": "diff", }) - resp = oidcCallback(t, client, code) numLogs++ // add an audit log for login - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - - client.SetSessionToken(authCookieValue(resp.Cookies())) - user, err = client.User(ctx, "me") + user, err = userClient.User(ctx, "me") require.NoError(t, err) require.True(t, strings.HasPrefix(user.Username, "jon-"), "username %q should have prefix %q", user.Username, "jon-") @@ -977,45 +944,62 @@ func TestUserOIDC(t *testing.T) { t.Run("Disabled", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - resp := oidcCallback(t, client, "asdf") + oauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback") + require.NoError(t, err) + + req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) + require.NoError(t, err) + resp, err := client.HTTPClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("NoIDToken", func(t *testing.T) { t.Parallel() + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + }) + client := coderdtest.New(t, &coderdtest.Options{ - OIDCConfig: &coderd.OIDCConfig{ - OAuth2Config: &testutil.OAuth2Config{}, - }, + OIDCConfig: cfg, }) - resp := oidcCallback(t, client, "asdf") + _, resp := fake.AttemptLogin(t, client, jwt.MapClaims{}) require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) t.Run("BadVerify", func(t *testing.T) { t.Parallel() - verifier := oidc.NewVerifier("", &oidc.StaticKeySet{ + badVerifier := oidc.NewVerifier("", &oidc.StaticKeySet{ PublicKeys: []crypto.PublicKey{}, }, &oidc.Config{}) - provider := &oidc.Provider{} + badProvider := &oidc.Provider{} + + fake := oidctest.NewFakeIDP(t, + oidctest.WithRefreshHook(func(_ string) error { + return xerrors.New("refreshing token should never occur") + }), + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.Provider = badProvider + cfg.Verifier = badVerifier + }) client := coderdtest.New(t, &coderdtest.Options{ - OIDCConfig: &coderd.OIDCConfig{ - OAuth2Config: &testutil.OAuth2Config{ - Token: (&oauth2.Token{ - AccessToken: "token", - }).WithExtra(map[string]interface{}{ - "id_token": "invalid", - }), - }, - Provider: provider, - Verifier: verifier, - }, + OIDCConfig: cfg, }) - resp := oidcCallback(t, client, "asdf") - + _, resp := fake.AttemptLogin(t, client, jwt.MapClaims{}) require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) } @@ -1146,36 +1130,6 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response { return res } -func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response { - return oidcCallbackWithState(t, client, code, "somestate", nil) -} - -func oidcCallbackWithState(t *testing.T, client *codersdk.Client, code, state string, modify func(r *http.Request)) *http.Response { - t.Helper() - - client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - oauthURL, err := client.URL.Parse(fmt.Sprintf("/api/v2/users/oidc/callback?code=%s&state=%s", code, state)) - require.NoError(t, err) - req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateCookie, - Value: state, - }) - if modify != nil { - modify(req) - } - res, err := client.HTTPClient.Do(req) - require.NoError(t, err) - defer res.Body.Close() - data, err := io.ReadAll(res.Body) - require.NoError(t, err) - t.Log(string(data)) - return res -} - func i64ptr(i int64) *int64 { return &i } diff --git a/coderd/users_test.go b/coderd/users_test.go index c36b4fad98afd..60e6ddb82aecf 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -8,7 +8,10 @@ import ( "testing" "time" - "github.com/golang-jwt/jwt" + "github.com/coder/coder/v2/coderd" + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + + "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -403,6 +406,7 @@ func TestPostLogout(t *testing.T) { }) } +// nolint:bodyclose func TestPostUsers(t *testing.T) { t.Parallel() t.Run("NoAuth", func(t *testing.T) { @@ -593,15 +597,15 @@ func TestPostUsers(t *testing.T) { t.Run("CreateOIDCLoginType", func(t *testing.T) { t.Parallel() email := "another@user.org" - conf := coderdtest.NewOIDCConfig(t, "") - config := conf.OIDCConfig(t, jwt.MapClaims{ - "email": email, + fake := oidctest.NewFakeIDP(t, + oidctest.WithServing(), + ) + cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true }) - config.AllowSignups = false - config.IgnoreUserInfo = true client := coderdtest.New(t, &coderdtest.Options{ - OIDCConfig: config, + OIDCConfig: cfg, }) first := coderdtest.CreateFirstUser(t, client) @@ -618,15 +622,9 @@ func TestPostUsers(t *testing.T) { require.NoError(t, err) // Try to log in with OIDC. - userClient := codersdk.New(client.URL) - resp := oidcCallback(t, userClient, conf.EncodeClaims(t, jwt.MapClaims{ + userClient, _ := fake.Login(t, client, jwt.MapClaims{ "email": email, - })) - require.Equal(t, resp.StatusCode, http.StatusTemporaryRedirect) - // Set the client to use this OIDC context - authCookie := authCookieValue(resp.Cookies()) - userClient.SetSessionToken(authCookie) - _ = resp.Body.Close() + }) found, err := userClient.User(ctx, "me") require.NoError(t, err) diff --git a/coderd/util/syncmap/map.go b/coderd/util/syncmap/map.go new file mode 100644 index 0000000000000..d245973efa844 --- /dev/null +++ b/coderd/util/syncmap/map.go @@ -0,0 +1,77 @@ +package syncmap + +import "sync" + +// Map is a type safe sync.Map +type Map[K, V any] struct { + m sync.Map +} + +func New[K, V any]() *Map[K, V] { + return &Map[K, V]{ + m: sync.Map{}, + } +} + +func (m *Map[K, V]) Store(k K, v V) { + m.m.Store(k, v) +} + +//nolint:forcetypeassert +func (m *Map[K, V]) Load(key K) (value V, ok bool) { + v, ok := m.m.Load(key) + if !ok { + var empty V + return empty, false + } + return v.(V), ok +} + +func (m *Map[K, V]) Delete(key K) { + m.m.Delete(key) +} + +//nolint:forcetypeassert +func (m *Map[K, V]) LoadAndDelete(key K) (actual V, loaded bool) { + act, loaded := m.m.LoadAndDelete(key) + if !loaded { + var empty V + return empty, loaded + } + return act.(V), loaded +} + +//nolint:forcetypeassert +func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + act, loaded := m.m.LoadOrStore(key, value) + if !loaded { + var empty V + return empty, loaded + } + return act.(V), loaded +} + +func (m *Map[K, V]) CompareAndSwap(key K, old V, new V) bool { + return m.m.CompareAndSwap(key, old, new) +} + +func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool) { + return m.m.CompareAndDelete(key, old) +} + +//nolint:forcetypeassert +func (m *Map[K, V]) Swap(key K, value V) (previous any, loaded bool) { + previous, loaded = m.m.Swap(key, value) + if !loaded { + var empty V + return empty, loaded + } + return previous.(V), loaded +} + +//nolint:forcetypeassert +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + m.m.Range(func(key, value interface{}) bool { + return f(key.(K), value.(V)) + }) +} diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go index d6f6db3cbedbd..8e76a36b1df14 100644 --- a/enterprise/coderd/userauth_test.go +++ b/enterprise/coderd/userauth_test.go @@ -1,25 +1,22 @@ package coderd_test import ( - "context" - "fmt" - "io" "net/http" "regexp" "testing" - "github.com/golang-jwt/jwt" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" + coderden "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/testutil" @@ -31,128 +28,123 @@ func TestUserOIDC(t *testing.T) { t.Run("RoleSync", func(t *testing.T) { t.Parallel() + // NoRoles is the "control group". It has claims with 0 roles + // assigned, and asserts that the user has no roles. t.Run("NoRoles", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - conf := coderdtest.NewOIDCConfig(t, "") - - oidcRoleName := "TemplateAuthor" - - config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) { - cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}} - }) - config.AllowSignups = true - config.UserRoleField = "roles" - - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureUserRoleManagement: 1}, + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.UserRoleField = "roles" }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ + claims := jwt.MapClaims{ "email": "alice@coder.com", - })) - require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - user, err := client.User(ctx, "alice") - require.NoError(t, err) - - require.Len(t, user.Roles, 0) - roleNames := []string{} - require.ElementsMatch(t, roleNames, []string{}) + } + // Login a new client that signs up + client, resp := runner.Login(t, claims) + require.Equal(t, http.StatusOK, resp.StatusCode) + // User should be in 0 groups. + runner.AssertRoles(t, "alice", []string{}) + // Force a refresh, and assert nothing has changes + runner.ForceRefresh(t, client, claims) + runner.AssertRoles(t, "alice", []string{}) }) - t.Run("NewUserAndRemoveRoles", func(t *testing.T) { + // A user has some roles, then on an oauth refresh will lose said + // roles from an updated claim. + t.Run("NewUserAndRemoveRolesOnRefresh", func(t *testing.T) { + // TODO: Implement new feature to update roles/groups on OIDC + // refresh tokens. https://github.com/coder/coder/issues/9312 + t.Skip("Refreshing tokens does not update roles :(") t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - conf := coderdtest.NewOIDCConfig(t, "") + const oidcRoleName = "TemplateAuthor" + runner := setupOIDCTest(t, oidcTestConfig{ + Userinfo: jwt.MapClaims{oidcRoleName: []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}, + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.UserRoleField = "roles" + cfg.UserRoleMapping = map[string][]string{ + oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}, + } + }, + }) - oidcRoleName := "TemplateAuthor" + // User starts with the owner role + client, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + "roles": []string{"random", oidcRoleName, rbac.RoleOwner()}, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()}) - config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) { - cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}} + // Now refresh the oauth, and check the roles are removed. + // Force a refresh, and assert nothing has changes + runner.ForceRefresh(t, client, jwt.MapClaims{ + "email": "alice@coder.com", + "roles": []string{"random"}, }) - config.AllowSignups = true - config.UserRoleField = "roles" + runner.AssertRoles(t, "alice", []string{}) + }) - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureUserRoleManagement: 1}, + // A user has some roles, then on another oauth login will lose said + // roles from an updated claim. + t.Run("NewUserAndRemoveRolesOnReAuth", func(t *testing.T) { + t.Parallel() + + const oidcRoleName = "TemplateAuthor" + runner := setupOIDCTest(t, oidcTestConfig{ + Userinfo: jwt.MapClaims{oidcRoleName: []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}, + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.UserRoleField = "roles" + cfg.UserRoleMapping = map[string][]string{ + oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}, + } }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ + // User starts with the owner role + _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{"random", oidcRoleName, rbac.RoleOwner()}, - })) - require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - _ = resp.Body.Close() - user, err := client.User(ctx, "alice") - require.NoError(t, err) - - require.Len(t, user.Roles, 3) - roleNames := []string{user.Roles[0].Name, user.Roles[1].Name, user.Roles[2].Name} - require.ElementsMatch(t, roleNames, []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()}) + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()}) - // Now remove the roles with a new oidc login - resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ + // Now login with oauth again, and check the roles are removed. + _, resp = runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{"random"}, - })) - require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - _ = resp.Body.Close() - user, err = client.User(ctx, "alice") - require.NoError(t, err) + }) + require.Equal(t, http.StatusOK, resp.StatusCode) - require.Len(t, user.Roles, 0) + runner.AssertRoles(t, "alice", []string{}) }) + + // All manual role updates should fail when role sync is enabled. t.Run("BlockAssignRoles", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - conf := coderdtest.NewOIDCConfig(t, "") - - config := conf.OIDCConfig(t, jwt.MapClaims{}) - config.AllowSignups = true - config.UserRoleField = "roles" - - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureUserRoleManagement: 1}, + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.UserRoleField = "roles" }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ + _, resp := runner.Login(t, jwt.MapClaims{ "email": "alice@coder.com", "roles": []string{}, - })) - require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + }) + require.Equal(t, http.StatusOK, resp.StatusCode) // Try to manually update user roles, even though controlled by oidc // role sync. - _, err = client.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{ + ctx := testutil.Context(t, testutil.WaitShort) + _, err := runner.AdminClient.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{ Roles: []string{ rbac.RoleTemplateAdmin(), }, @@ -164,199 +156,211 @@ func TestUserOIDC(t *testing.T) { t.Run("Groups", func(t *testing.T) { t.Parallel() + + // Assigns does a simple test of assigning a user to a group based + // on the oidc claims. t.Run("Assigns", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) - conf := coderdtest.NewOIDCConfig(t, "") - const groupClaim = "custom-groups" - config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) { - cfg.GroupField = groupClaim - }) - config.AllowSignups = true - - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + const groupName = "bingbong" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - - groupName := "bingbong" - group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{ + ctx := testutil.Context(t, testutil.WaitShort) + group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{ Name: groupName, }) require.NoError(t, err) require.Len(t, group.Members, 0) - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ - "email": "colin@coder.com", + _, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", groupClaim: []string{groupName}, - })) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - - group, err = client.Group(ctx, group.ID) - require.NoError(t, err) - require.Len(t, group.Members, 1) + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{groupName}) }) + + // Tests the group mapping feature. t.Run("AssignsMapped", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - conf := coderdtest.NewOIDCConfig(t, "") - - oidcGroupName := "pingpong" - coderGroupName := "bingbong" - - config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) { - cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName} - }) - config.AllowSignups = true + const groupClaim = "custom-groups" - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + const oidcGroupName = "pingpong" + const coderGroupName = "bingbong" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim + cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName} }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - - group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{ + ctx := testutil.Context(t, testutil.WaitShort) + group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{ Name: coderGroupName, }) require.NoError(t, err) require.Len(t, group.Members, 0) - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ - "email": "colin@coder.com", - "groups": []string{oidcGroupName}, - })) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - - group, err = client.Group(ctx, group.ID) - require.NoError(t, err) - require.Len(t, group.Members, 1) + _, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + groupClaim: []string{oidcGroupName}, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{coderGroupName}) }) - t.Run("AddThenRemove", func(t *testing.T) { + // User is in a group, then on an oauth refresh will lose said + // group. + t.Run("AddThenRemoveOnRefresh", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) - conf := coderdtest.NewOIDCConfig(t, "") - - config := conf.OIDCConfig(t, jwt.MapClaims{}) - config.AllowSignups = true + // TODO: Implement new feature to update roles/groups on OIDC + // refresh tokens. https://github.com/coder/coder/issues/9312 + t.Skip("Refreshing tokens does not update groups :(") - client, firstUser := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + const groupClaim = "custom-groups" + const groupName = "bingbong" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim }, }) - // Add some extra users/groups that should be asserted after. - // Adding this user as there was a bug that removing 1 user removed - // all users from the group. - _, extra := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) - groupName := "bingbong" - group, err := client.CreateGroup(ctx, firstUser.OrganizationID, codersdk.CreateGroupRequest{ + ctx := testutil.Context(t, testutil.WaitShort) + group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{ Name: groupName, }) - require.NoError(t, err, "create group") + require.NoError(t, err) + require.Len(t, group.Members, 0) - group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ - AddUsers: []string{ - firstUser.UserID.String(), - extra.ID.String(), - }, + client, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + groupClaim: []string{groupName}, }) - require.NoError(t, err, "patch group") - require.Len(t, group.Members, 2, "expect both members") + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{groupName}) - // Now add OIDC user into the group - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ - "email": "colin@coder.com", - "groups": []string{groupName}, - })) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + // Refresh without the group claim + runner.ForceRefresh(t, client, jwt.MapClaims{ + "email": "alice@coder.com", + }) + runner.AssertGroups(t, "alice", []string{}) + }) - group, err = client.Group(ctx, group.ID) - require.NoError(t, err) - require.Len(t, group.Members, 3) + t.Run("AddThenRemoveOnReAuth", func(t *testing.T) { + t.Parallel() - // Login to remove the OIDC user from the group - resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ - "email": "colin@coder.com", - "groups": []string{}, - })) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + const groupClaim = "custom-groups" + const groupName = "bingbong" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim + }, + }) - group, err = client.Group(ctx, group.ID) + ctx := testutil.Context(t, testutil.WaitShort) + group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{ + Name: groupName, + }) require.NoError(t, err) - require.Len(t, group.Members, 2) - var expected []uuid.UUID - for _, mem := range group.Members { - expected = append(expected, mem.ID) - } - require.ElementsMatchf(t, expected, []uuid.UUID{firstUser.UserID, extra.ID}, "expected members") + require.Len(t, group.Members, 0) + + _, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + groupClaim: []string{groupName}, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{groupName}) + + // Refresh without the group claim + _, resp = runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{}) }) + // Updating groups where the claimed group does not exist. t.Run("NoneMatch", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) - conf := coderdtest.NewOIDCConfig(t, "") + const groupClaim = "custom-groups" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim + }, + }) - config := conf.OIDCConfig(t, jwt.MapClaims{}) - config.AllowSignups = true + _, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + groupClaim: []string{"not-exists"}, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{}) + }) - client, _ := coderdenttest.New(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + // Updating groups where the claimed group does not exist creates + // the group. + t.Run("AutoCreate", func(t *testing.T) { + t.Parallel() + + const groupClaim = "custom-groups" + const groupName = "make-me" + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.GroupField = groupClaim + cfg.CreateMissingGroups = true }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) + _, resp := runner.Login(t, jwt.MapClaims{ + "email": "alice@coder.com", + groupClaim: []string{groupName}, + }) + require.Equal(t, http.StatusOK, resp.StatusCode) + runner.AssertGroups(t, "alice", []string{groupName}) + }) + }) - groupName := "bingbong" - group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{ - Name: groupName, + t.Run("Refresh", func(t *testing.T) { + t.Run("RefreshTokensMultiple", func(t *testing.T) { + t.Parallel() + + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.AllowSignups = true + cfg.UserRoleField = "roles" + }, }) - require.NoError(t, err) - require.Len(t, group.Members, 0) - resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{ - "email": "colin@coder.com", - "groups": []string{"coolin"}, - })) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + claims := jwt.MapClaims{ + "email": "alice@coder.com", + } + // Login a new client that signs up + client, resp := runner.Login(t, claims) + require.Equal(t, http.StatusOK, resp.StatusCode) - group, err = client.Group(ctx, group.ID) - require.NoError(t, err) - require.Len(t, group.Members, 0) + // Refresh multiple times. + for i := 0; i < 3; i++ { + runner.ForceRefresh(t, client, claims) + } }) }) } +// nolint:bodyclose func TestGroupSync(t *testing.T) { t.Parallel() @@ -470,28 +474,20 @@ func TestGroupSync(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) - conf := coderdtest.NewOIDCConfig(t, "") - - config := conf.OIDCConfig(t, jwt.MapClaims{}, tc.modCfg) - - client, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ - Options: &coderdtest.Options{ - OIDCConfig: config, - }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureTemplateRBAC: 1}, + runner := setupOIDCTest(t, oidcTestConfig{ + Config: func(cfg *coderd.OIDCConfig) { + cfg.GroupField = "groups" + tc.modCfg(cfg) }, }) - admin, err := client.User(ctx, "me") - require.NoError(t, err) - require.Len(t, admin.OrganizationIDs, 1) - // Setup + ctx := testutil.Context(t, testutil.WaitLong) + org := runner.AdminUser.OrganizationIDs[0] + initialGroups := make(map[string]codersdk.Group) for _, group := range tc.initialOrgGroups { - newGroup, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{ + newGroup, err := runner.AdminClient.CreateGroup(ctx, org, codersdk.CreateGroupRequest{ Name: group, }) require.NoError(t, err) @@ -500,16 +496,16 @@ func TestGroupSync(t *testing.T) { } // Create the user and add them to their initial groups - _, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationIDs[0]) + _, user := coderdtest.CreateAnotherUser(t, runner.AdminClient, org) for _, group := range tc.initialUserGroups { - _, err := client.PatchGroup(ctx, initialGroups[group].ID, codersdk.PatchGroupRequest{ + _, err := runner.AdminClient.PatchGroup(ctx, initialGroups[group].ID, codersdk.PatchGroupRequest{ AddUsers: []string{user.ID.String()}, }) require.NoError(t, err) } // nolint:gocritic - _, err = api.Database.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ + _, err := runner.API.Database.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ NewLoginType: database.LoginTypeOIDC, UserID: user.ID, }) @@ -517,11 +513,11 @@ func TestGroupSync(t *testing.T) { // Log in the new user tc.claims["email"] = user.Email - resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.claims)) - assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - _ = resp.Body.Close() + _, resp := runner.Login(t, tc.claims) + require.Equal(t, http.StatusOK, resp.StatusCode) - orgGroups, err := client.GroupsByOrganization(ctx, admin.OrganizationIDs[0]) + // Check group sources + orgGroups, err := runner.AdminClient.GroupsByOrganization(ctx, org) require.NoError(t, err) for _, group := range orgGroups { @@ -567,24 +563,107 @@ func TestGroupSync(t *testing.T) { } } -func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response { +// oidcTestRunner is just a helper to setup and run oidc tests. +// An actual Coderd instance is used to run the tests. +type oidcTestRunner struct { + AdminClient *codersdk.Client + AdminUser codersdk.User + API *coderden.API + + // Login will call the OIDC flow with an unauthenticated client. + // The IDP will return the idToken claims. + Login func(t *testing.T, idToken jwt.MapClaims) (*codersdk.Client, *http.Response) + // ForceRefresh will use an authenticated codersdk.Client, and force their + // OIDC token to be expired and require a refresh. The refresh will use the claims provided. + // It just calls the /users/me endpoint to trigger the refresh. + ForceRefresh func(t *testing.T, client *codersdk.Client, idToken jwt.MapClaims) +} + +type oidcTestConfig struct { + Userinfo jwt.MapClaims + + // Config allows modifying the Coderd OIDC configuration. + Config func(cfg *coderd.OIDCConfig) +} + +func (r *oidcTestRunner) AssertRoles(t *testing.T, userIdent string, roles []string) { t.Helper() - client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse + + ctx := testutil.Context(t, testutil.WaitMedium) + user, err := r.AdminClient.User(ctx, userIdent) + require.NoError(t, err) + + roleNames := []string{} + for _, role := range user.Roles { + roleNames = append(roleNames, role.Name) } - oauthURL, err := client.URL.Parse(fmt.Sprintf("/api/v2/users/oidc/callback?code=%s&state=somestate", code)) + require.ElementsMatch(t, roles, roleNames, "expected roles") +} + +func (r *oidcTestRunner) AssertGroups(t *testing.T, userIdent string, groups []string) { + t.Helper() + + if !slice.Contains(groups, database.EveryoneGroup) { + var cpy []string + cpy = append(cpy, groups...) + // always include everyone group + cpy = append(cpy, database.EveryoneGroup) + groups = cpy + } + ctx := testutil.Context(t, testutil.WaitMedium) + user, err := r.AdminClient.User(ctx, userIdent) require.NoError(t, err) - req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil) + + allGroups, err := r.AdminClient.GroupsByOrganization(ctx, user.OrganizationIDs[0]) require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: codersdk.OAuth2StateCookie, - Value: "somestate", + + userInGroups := []string{} + for _, g := range allGroups { + for _, mem := range g.Members { + if mem.ID == user.ID { + userInGroups = append(userInGroups, g.Name) + } + } + } + + require.ElementsMatch(t, groups, userInGroups, "expected groups") +} + +func setupOIDCTest(t *testing.T, settings oidcTestConfig) *oidcTestRunner { + t.Helper() + + fake := oidctest.NewFakeIDP(t, + oidctest.WithStaticUserInfo(settings.Userinfo), + oidctest.WithLogging(t, nil), + // Run fake IDP on a real webserver + oidctest.WithServing(), + ) + + ctx := testutil.Context(t, testutil.WaitMedium) + cfg := fake.OIDCConfig(t, nil, settings.Config) + owner, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + OIDCConfig: cfg, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureUserRoleManagement: 1, + codersdk.FeatureTemplateRBAC: 1, + }, + }, }) - res, err := client.HTTPClient.Do(req) - require.NoError(t, err) - defer res.Body.Close() - data, err := io.ReadAll(res.Body) + admin, err := owner.User(ctx, "me") require.NoError(t, err) - t.Log(string(data)) - return res + + helper := oidctest.NewLoginHelper(owner, fake) + + return &oidcTestRunner{ + AdminClient: owner, + AdminUser: admin, + API: api, + Login: helper.Login, + ForceRefresh: func(t *testing.T, client *codersdk.Client, idToken jwt.MapClaims) { + helper.ForceRefresh(t, api.Database, client, idToken) + }, + } } From 1de1e3b98ad18f652ee76c4444b2bc423285ed19 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 25 Aug 2023 16:52:10 -0300 Subject: [PATCH 17/40] fix(site): make right panel scrollable on template editor (#9344) --- .../src/components/Resources/ResourceCard.tsx | 2 +- .../TemplateVersionEditor.stories.tsx | 378 ++++++++++++++++-- .../TemplateVersionEditor.tsx | 34 +- 3 files changed, 372 insertions(+), 42 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index f951cb0a4d2be..e3667e95bf587 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -28,7 +28,7 @@ export const ResourceCard: FC = ({ resource, agentRow }) => { : metadataToDisplay.slice(0, 4) return ( -
+
= { title: "components/TemplateVersionEditor", component: TemplateVersionEditor, + args: { + template: MockTemplate, + templateVersion: MockTemplateVersion, + defaultFileTree: MockTemplateVersionFileTree, + }, parameters: { layout: "fullscreen", }, } -const Template: Story = ( - args: TemplateVersionEditorProps, -) => - -export const Example = Template.bind({}) -Example.args = { - template: MockTemplate, - templateVersion: MockTemplateVersion, - defaultFileTree: MockTemplateVersionFileTree, -} +export default meta +type Story = StoryObj -export const Logs = Template.bind({}) +export const Example: Story = {} -Logs.args = { - template: MockTemplate, - templateVersion: MockTemplateVersion, - defaultFileTree: MockTemplateVersionFileTree, - buildLogs: MockWorkspaceBuildLogs, +export const Logs = { + args: { + buildLogs: MockWorkspaceBuildLogs, + }, } -export const Resources = Template.bind({}) +export const Resources: Story = { + args: { + buildLogs: MockWorkspaceBuildLogs, + resources: [ + MockWorkspaceResource, + MockWorkspaceResource2, + MockWorkspaceResource3, + ], + }, +} -Resources.args = { - template: MockTemplate, - templateVersion: MockTemplateVersion, - defaultFileTree: MockTemplateVersionFileTree, - buildLogs: MockWorkspaceBuildLogs, - resources: [ - MockWorkspaceResource, - MockWorkspaceResource2, - MockWorkspaceResource3, - ], +export const ManyLogs = { + args: { + templateVersion: { + ...MockTemplateVersion, + job: { + ...MockTemplateVersion.job, + error: + "template import provision for start: terraform plan: exit status 1", + }, + }, + buildLogs: [ + { + id: 938494, + created_at: "2023-08-25T19:07:43.331Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Setting up", + output: "", + }, + { + id: 938495, + created_at: "2023-08-25T19:07:43.331Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Parsing template parameters", + output: "", + }, + { + id: 938496, + created_at: "2023-08-25T19:07:43.339Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938497, + created_at: "2023-08-25T19:07:44.15Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing the backend...", + }, + { + id: 938498, + created_at: "2023-08-25T19:07:44.215Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing provider plugins...", + }, + { + id: 938499, + created_at: "2023-08-25T19:07:44.216Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding coder/coder versions matching "~> 0.11.0"...', + }, + { + id: 938500, + created_at: "2023-08-25T19:07:44.668Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding kreuzwerker/docker versions matching "~> 3.0.1"...', + }, + { + id: 938501, + created_at: "2023-08-25T19:07:44.722Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Using coder/coder v0.11.1 from the shared cache directory", + }, + { + id: 938502, + created_at: "2023-08-25T19:07:44.857Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "- Using kreuzwerker/docker v3.0.2 from the shared cache directory", + }, + { + id: 938503, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "Terraform has created a lock file .terraform.lock.hcl to record the provider", + }, + { + id: 938504, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "selections it made above. Include this file in your version control repository", + }, + { + id: 938505, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "so that Terraform can guarantee to make the same selections by default when", + }, + { + id: 938506, + created_at: "2023-08-25T19:07:45.082Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: 'you run "terraform init" in the future.', + }, + { + id: 938507, + created_at: "2023-08-25T19:07:45.083Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Terraform has been successfully initialized!", + }, + { + id: 938508, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + 'You may now begin working with Terraform. Try running "terraform plan" to see', + }, + { + id: 938509, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "any changes that are required for your infrastructure. All Terraform commands", + }, + { + id: 938510, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "should now work.", + }, + { + id: 938511, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "If you ever set or change modules or backend configuration for Terraform,", + }, + { + id: 938512, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "rerun this command to reinitialize your working directory. If you forget, other", + }, + { + id: 938513, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "commands will detect it and remind you to do so if necessary.", + }, + { + id: 938514, + created_at: "2023-08-25T19:07:45.143Z", + log_source: "provisioner", + log_level: "info", + stage: "Detecting persistent resources", + output: "Terraform 1.1.9", + }, + { + id: 938515, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "Warning: Argument is deprecated", + }, + { + id: 938516, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938517, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: " 15: feature_use_managed_variables = true", + }, + { + id: 938518, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938519, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: + "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", + }, + { + id: 938520, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "Error: ephemeral parameter requires the default property", + }, + { + id: 938521, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: + 'on devcontainer-on-docker.tf line 27, in data "coder_parameter" "another_one":', + }, + { + id: 938522, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: ' 27: data "coder_parameter" "another_one" {', + }, + { + id: 938523, + created_at: "2023-08-25T19:07:46.301Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938524, + created_at: "2023-08-25T19:07:46.301Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938525, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "Warning: Argument is deprecated", + }, + { + id: 938526, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938527, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: " 15: feature_use_managed_variables = true", + }, + { + id: 938528, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938529, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: + "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", + }, + { + id: 938530, + created_at: "2023-08-25T19:07:46.311Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Cleaning Up", + output: "", + }, + ], + }, } diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 9ffe7e0e41b4c..33577ea4488c4 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -376,7 +376,17 @@ export const TemplateVersionEditor: FC = ({ > {templateVersion.job.error && (
- + + `1px solid ${theme.palette.divider}`, + borderLeft: (theme) => + `2px solid ${theme.palette.error.main}`, + }} + > Error during the build {templateVersion.job.error} @@ -385,7 +395,7 @@ export const TemplateVersionEditor: FC = ({ {buildLogs && buildLogs.length > 0 && ( @@ -393,7 +403,7 @@ export const TemplateVersionEditor: FC = ({
@@ -470,6 +480,7 @@ const useStyles = makeStyles< display: "flex", flex: 1, flexBasis: 0, + overflow: "hidden", }, sidebar: { minWidth: 256, @@ -505,17 +516,23 @@ const useStyles = makeStyles< }, panelWrapper: { flex: 1, + borderLeft: `1px solid ${theme.palette.divider}`, + overflow: "hidden", display: "flex", flexDirection: "column", - borderLeft: `1px solid ${theme.palette.divider}`, - overflowY: "auto", }, panel: { - padding: theme.spacing(1), + overflowY: "auto", + height: "100%", "&.hidden": { display: "none", }, + + // Hack to access customize resource-card from here + "& .resource-card": { + border: 0, + }, }, tabs: { borderBottom: `1px solid ${theme.palette.divider}`, @@ -586,7 +603,8 @@ const useStyles = makeStyles< buildLogs: { display: "flex", flexDirection: "column", - overflowY: "auto", - gap: theme.spacing(1), + }, + resources: { + paddingBottom: theme.spacing(2), }, })) From 7904d0b92ffe8164e06b624dfe3c2d7ab0392084 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 25 Aug 2023 23:48:35 +0300 Subject: [PATCH 18/40] docs: list firewall exceptions for restricted internet installations (#8936) * docs: add firewall exceptions for restricted internet installtions closes #7542 * fix link * fmt --- docs/install/offline.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/install/offline.md b/docs/install/offline.md index 93039ec3e6670..e7798670c18d5 100644 --- a/docs/install/offline.md +++ b/docs/install/offline.md @@ -225,3 +225,14 @@ server, as demonstrated in the example below: With these steps, you'll have the Coder documentation hosted on your server and accessible for your team to use. + +## Firewall exceptions + +In restricted internet networks, Coder may require connection to internet. +Ensure that the following web addresses are accessible from the machine where +Coder is installed. + +- code-server.dev (install via AUR) +- open-vsx.org (optional if someone would use code-server) +- registry.terraform.io (to create and push template) +- v2-licensor.coder.com (developing Coder in Coder) From 451ca042ceb7228d3784bd5d2ffd4848182265c7 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 25 Aug 2023 17:16:30 -0500 Subject: [PATCH 19/40] feat(site): show entity name in DeleteDialog (#9347) --- site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx | 2 +- site/src/i18n/en/common.json | 2 +- site/src/pages/UsersPage/UsersPage.test.tsx | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx index 0a56fe65b25c6..f46405cbb4c31 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx @@ -39,7 +39,7 @@ export const DeleteDialog: FC> = ({

{info}

-

{t("deleteDialog.confirm", { entity })}

+

{t("deleteDialog.confirm", { entity, name })}

{ diff --git a/site/src/i18n/en/common.json b/site/src/i18n/en/common.json index a22e318e6e8c5..875976e4d8e41 100644 --- a/site/src/i18n/en/common.json +++ b/site/src/i18n/en/common.json @@ -16,7 +16,7 @@ "deleteDialog": { "title": "Delete {{entity}}", "intro": "Deleting this {{entity}} is irreversible!", - "confirm": "Are you sure you want to proceed? Type the name of this {{entity}} below to confirm.", + "confirm": "Are you sure you want to proceed? Type {{name}} below to confirm.", "confirmLabel": "Name of {{entity}} to delete", "incorrectName": "Incorrect {{entity}} name." }, diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index ef2ec64d44107..16ca542a5d916 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -72,7 +72,11 @@ const deleteUser = async (setupActionSpies: () => void) => { // Check if the confirm message is displayed const confirmDialog = await screen.findByRole("dialog") expect(confirmDialog).toHaveTextContent( - t("deleteDialog.confirm", { ns: "common", entity: "user" }).toString(), + t("deleteDialog.confirm", { + ns: "common", + entity: "user", + name: MockUser2.username, + }).toString(), ) // Confirm with text input From f97b49796628619efc1e39fc4b2f2b6a6c426610 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 27 Aug 2023 01:22:28 +0300 Subject: [PATCH 20/40] chore(dogfood): update docker tf provider and metadata (#9356) --- dogfood/main.tf | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dogfood/main.tf b/dogfood/main.tf index 72774cba12f92..6f225f7f32dec 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -6,7 +6,7 @@ terraform { } docker = { source = "kreuzwerker/docker" - version = "~> 2.22.0" + version = "~> 3.0.0" } } } @@ -40,9 +40,10 @@ data "coder_parameter" "dotfiles_url" { } data "coder_parameter" "region" { - type = "string" - name = "Region" - icon = "/emojis/1f30e.png" + type = "string" + name = "Region" + icon = "/emojis/1f30e.png" + default = "us-pittsburgh" option { icon = "/emojis/1f1fa-1f1f8.png" name = "Pittsburgh" @@ -326,4 +327,8 @@ resource "coder_metadata" "container_info" { key = "runtime" value = docker_container.workspace[0].runtime } + item { + key = "region" + value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name + } } From 54032ccfe8b6d1347d12ba0a23ffac1060466b4e Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 27 Aug 2023 02:02:22 +0300 Subject: [PATCH 21/40] ci: update pr-cleanup.yaml to remove `set -x` (#9358) --- .github/workflows/pr-cleanup.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index 510c8f4299361..d32ea2f5d49b7 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -1,4 +1,4 @@ -name: Cleanup PR deployment and image +name: pr-cleanup on: pull_request: types: closed @@ -35,14 +35,14 @@ jobs: - name: Set up kubeconfig run: | - set -euxo pipefail + set -euo pipefail mkdir -p ~/.kube echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config export KUBECONFIG=~/.kube/config - name: Delete helm release run: | - set -euxo pipefail + set -euo pipefail helm delete --namespace "pr${{ steps.pr_number.outputs.PR_NUMBER }}" "pr${{ steps.pr_number.outputs.PR_NUMBER }}" || echo "helm release not found" - name: "Remove PR namespace" @@ -51,7 +51,7 @@ jobs: - name: "Remove DNS records" run: | - set -euxo pipefail + set -euo pipefail # Get identifier for the record record_id=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records?name=%2A.pr${{ steps.pr_number.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" \ -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \ From c3ac55ff4244b32168065c5a81c247fde5a8c8dd Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 27 Aug 2023 11:26:20 -0500 Subject: [PATCH 22/40] feat: add `template_active_version_id` to workspaces (#9226) * feat: add `template_active_version_id` to workspaces This reduces a fetch in the VS Code extension when getting the active version update message! * Fix entities.ts * Fix golden gen --- cli/testdata/coder_list_--output_json.golden | 1 + coderd/apidoc/docs.go | 4 ++++ coderd/apidoc/swagger.json | 4 ++++ coderd/workspaces.go | 1 + codersdk/workspaces.go | 1 + docs/api/schemas.md | 3 +++ docs/api/workspaces.md | 5 +++++ site/src/api/typesGenerated.ts | 1 + site/src/testHelpers/entities.ts | 1 + 9 files changed, 21 insertions(+) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index f680c9e210cbc..2e317f996047b 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -11,6 +11,7 @@ "template_display_name": "", "template_icon": "", "template_allow_user_cancel_workspace_jobs": false, + "template_active_version_id": "[version ID]", "latest_build": { "id": "[workspace build ID]", "created_at": "[timestamp]", diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2668bf41b024d..58624b22a908f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10555,6 +10555,10 @@ const docTemplate = `{ "owner_name": { "type": "string" }, + "template_active_version_id": { + "type": "string", + "format": "uuid" + }, "template_allow_user_cancel_workspace_jobs": { "type": "boolean" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ccf504f4d7cc7..7342e5140598e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9557,6 +9557,10 @@ "owner_name": { "type": "string" }, + "template_active_version_id": { + "type": "string", + "format": "uuid" + }, "template_allow_user_cancel_workspace_jobs": { "type": "boolean" }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 92e4c029f3777..707c85200488b 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1187,6 +1187,7 @@ func convertWorkspace( TemplateIcon: template.Icon, TemplateDisplayName: template.DisplayName, TemplateAllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + TemplateActiveVersionID: template.ActiveVersionID, Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(), Name: workspace.Name, AutostartSchedule: autostartSchedule, diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index d7b191c6273b6..05e1a156d1122 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -28,6 +28,7 @@ type Workspace struct { TemplateDisplayName string `json:"template_display_name"` TemplateIcon string `json:"template_icon"` TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"` + TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"` LatestBuild WorkspaceBuild `json:"latest_build"` Outdated bool `json:"outdated"` Name string `json:"name"` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index f62da873c5c3d..60bb7a6208c3c 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -5497,6 +5497,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", @@ -5524,6 +5525,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `outdated` | boolean | false | | | | `owner_id` | string | false | | | | `owner_name` | string | false | | | +| `template_active_version_id` | string | false | | | | `template_allow_user_cancel_workspace_jobs` | boolean | false | | | | `template_display_name` | string | false | | | | `template_icon` | string | false | | | @@ -6629,6 +6631,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 7c4e9319cd2b8..ac4eda1069fd3 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -188,6 +188,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", @@ -376,6 +377,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", @@ -563,6 +565,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", @@ -752,6 +755,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", @@ -1020,6 +1024,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "outdated": true, "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", "template_icon": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7315caf7ded97..2cd98259380b0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1303,6 +1303,7 @@ export interface Workspace { readonly template_display_name: string readonly template_icon: string readonly template_allow_user_cancel_workspace_jobs: boolean + readonly template_active_version_id: string readonly latest_build: WorkspaceBuild readonly outdated: boolean readonly name: string diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 2e3438c1fe293..349857550abf5 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -848,6 +848,7 @@ export const MockWorkspace: TypesGen.Workspace = { template_display_name: MockTemplate.display_name, template_allow_user_cancel_workspace_jobs: MockTemplate.allow_user_cancel_workspace_jobs, + template_active_version_id: MockTemplate.active_version_id, outdated: false, owner_id: MockUser.id, organization_id: MockOrganization.id, From 61634d482fdf45d0ef4c09ba7c18e5d7f6cc2687 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 27 Aug 2023 11:26:31 -0500 Subject: [PATCH 23/40] fix: truncate websocket close error (#9360) Related #9324 --- coderd/provisionerjobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 087fd0e367aab..4b49c385c80f4 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -402,7 +402,7 @@ func (f *logFollower) follow() { if f.ctx.Err() == nil && !xerrors.Is(err, io.EOF) { // neither context expiry, nor EOF, close and log f.logger.Error(f.ctx, "failed to query logs", slog.Error(err)) - err = f.conn.Close(websocket.StatusInternalError, err.Error()) + err = f.conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("%s", err.Error())) if err != nil { f.logger.Warn(f.ctx, "failed to close webscoket", slog.Error(err)) } From 4a140536e1c58bfff6aca2542c848d962843ff02 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sun, 27 Aug 2023 11:42:51 -0500 Subject: [PATCH 24/40] ci: lint against `dupl` (#9357) This lint rule should help us keep Go code redundancy under control. --- .golangci.yaml | 5 +++++ cli/update_test.go | 17 +++-------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 156d6649890b3..15c381a682116 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,6 +2,10 @@ # Over time we should try tightening some of these. linters-settings: + dupl: + # goal: 100 + threshold: 412 + exhaustruct: include: # Gradually extend to cover more of the codebase. @@ -268,3 +272,4 @@ linters: - typecheck - unconvert - unused + - dupl diff --git a/cli/update_test.go b/cli/update_test.go index 0efa1f997cb69..38b042d2813f0 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -602,13 +602,9 @@ func TestUpdateValidateRichParameters(t *testing.T) { // Update the workspace inv, root = clitest.New(t, "update", "my-workspace") clitest.SetupConfig(t, client, root) - doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) - go func() { - defer close(doneChan) - err := inv.Run() - assert.NoError(t, err) - }() + clitest.Start(t, inv) matches := []string{ stringParameterName, "second_option", @@ -623,7 +619,6 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.WriteLine(value) } } - <-doneChan }) t.Run("ParameterOptionDisappeared", func(t *testing.T) { @@ -668,13 +663,8 @@ func TestUpdateValidateRichParameters(t *testing.T) { // Update the workspace inv, root = clitest.New(t, "update", "my-workspace") clitest.SetupConfig(t, client, root) - doneChan := make(chan struct{}) pty := ptytest.New(t).Attach(inv) - go func() { - defer close(doneChan) - err := inv.Run() - assert.NoError(t, err) - }() + clitest.Start(t, inv) matches := []string{ stringParameterName, "Third option", @@ -689,7 +679,6 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.WriteLine(value) } } - <-doneChan }) t.Run("ImmutableRequiredParameterExists_MutableRequiredParameterAdded", func(t *testing.T) { From 173aac959ca734b771b0b5bcfb06532d106e59b3 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sun, 27 Aug 2023 14:35:06 -0500 Subject: [PATCH 25/40] fix(systemd): use more reasonable restart limit (#9355) --- scripts/linux-pkg/coder.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/linux-pkg/coder.service b/scripts/linux-pkg/coder.service index 4ff2cc260a9bf..32246491880d4 100644 --- a/scripts/linux-pkg/coder.service +++ b/scripts/linux-pkg/coder.service @@ -4,7 +4,7 @@ Documentation=https://coder.com/docs/coder-oss Requires=network-online.target After=network-online.target ConditionFileNotEmpty=/etc/coder.d/coder.env -StartLimitIntervalSec=60 +StartLimitIntervalSec=10 StartLimitBurst=3 [Service] From 6ba92ef924e9ae35cca598bfb1ee7820a44a06b0 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sun, 27 Aug 2023 14:46:44 -0500 Subject: [PATCH 26/40] ci: enable gocognit (#9359) And, bring the server under 300: * Removed the undocumented "disable" STUN address in favor of the --disable-direct flag. --- .golangci.yaml | 12 +- cli/configssh.go | 1 - cli/root.go | 7 + cli/server.go | 476 +++++++++--------- cli/server_test.go | 25 - cli/ssh.go | 1 - coderd/coderd.go | 2 - coderd/coderdtest/coderdtest.go | 2 +- coderd/database/dbfake/dbfake.go | 1 - .../provisionerdserver/provisionerdserver.go | 2 - coderd/workspacebuilds.go | 1 - provisioner/terraform/resources.go | 1 - scripts/apitypings/main.go | 2 - tailnet/derpmap.go | 4 +- 14 files changed, 264 insertions(+), 273 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 15c381a682116..5f474602b2cfd 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -11,7 +11,7 @@ linters-settings: # Gradually extend to cover more of the codebase. - 'httpmw\.\w+' gocognit: - min-complexity: 46 # Min code complexity (def 30). + min-complexity: 300 goconst: min-len: 4 # Min length of string consts (def 3). @@ -122,10 +122,6 @@ linters-settings: goimports: local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder - gocyclo: - # goal: 30 - min-complexity: 47 - importas: no-unaliased: true @@ -236,7 +232,11 @@ linters: - exportloopref - forcetypeassert - gocritic - - gocyclo + # gocyclo is may be useful in the future when we start caring + # about testing complexity, but for the time being we should + # create a good culture around cognitive complexity. + # - gocyclo + - gocognit - goimports - gomodguard - gosec diff --git a/cli/configssh.go b/cli/configssh.go index d153814013efc..7e9e8109ea554 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -190,7 +190,6 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r } } -//nolint:gocyclo func (r *RootCmd) configSSH() *clibase.Cmd { var ( sshConfigFile string diff --git a/cli/root.go b/cli/root.go index fad5625d909c0..3ab2f0d7f33b9 100644 --- a/cli/root.go +++ b/cli/root.go @@ -831,6 +831,13 @@ func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client) return nil } +// Verbosef logs a message if verbose mode is enabled. +func (r *RootCmd) Verbosef(inv *clibase.Invocation, fmtStr string, args ...interface{}) { + if r.verbose { + cliui.Infof(inv.Stdout, fmtStr, args...) + } +} + type headerTransport struct { transport http.RoundTripper header http.Header diff --git a/cli/server.go b/cli/server.go index 9d6b4f975cc79..779215f0fce35 100644 --- a/cli/server.go +++ b/cli/server.go @@ -176,18 +176,147 @@ func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, er return providers, nil } -// nolint:gocyclo +func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) { + if vals.OIDC.ClientID == "" { + return nil, xerrors.Errorf("OIDC client ID must be set!") + } + if vals.OIDC.IssuerURL == "" { + return nil, xerrors.Errorf("OIDC issuer URL must be set!") + } + + oidcProvider, err := oidc.NewProvider( + ctx, vals.OIDC.IssuerURL.String(), + ) + if err != nil { + return nil, xerrors.Errorf("configure oidc provider: %w", err) + } + redirectURL, err := vals.AccessURL.Value().Parse("/api/v2/users/oidc/callback") + if err != nil { + return nil, xerrors.Errorf("parse oidc oauth callback url: %w", err) + } + // If the scopes contain 'groups', we enable group support. + // Do not override any custom value set by the user. + if slice.Contains(vals.OIDC.Scopes, "groups") && vals.OIDC.GroupField == "" { + vals.OIDC.GroupField = "groups" + } + oauthCfg := &oauth2.Config{ + ClientID: vals.OIDC.ClientID.String(), + ClientSecret: vals.OIDC.ClientSecret.String(), + RedirectURL: redirectURL.String(), + Endpoint: oidcProvider.Endpoint(), + Scopes: vals.OIDC.Scopes, + } + + var useCfg httpmw.OAuth2Config = oauthCfg + if vals.OIDC.ClientKeyFile != "" { + // PKI authentication is done in the params. If a + // counter example is found, we can add a config option to + // change this. + oauthCfg.Endpoint.AuthStyle = oauth2.AuthStyleInParams + if vals.OIDC.ClientSecret != "" { + return nil, xerrors.Errorf("cannot specify both oidc client secret and oidc client key file") + } + + pkiCfg, err := configureOIDCPKI(oauthCfg, vals.OIDC.ClientKeyFile.Value(), vals.OIDC.ClientCertFile.Value()) + if err != nil { + return nil, xerrors.Errorf("configure oauth pki authentication: %w", err) + } + useCfg = pkiCfg + } + return &coderd.OIDCConfig{ + OAuth2Config: useCfg, + Provider: oidcProvider, + Verifier: oidcProvider.Verifier(&oidc.Config{ + ClientID: vals.OIDC.ClientID.String(), + }), + EmailDomain: vals.OIDC.EmailDomain, + AllowSignups: vals.OIDC.AllowSignups.Value(), + UsernameField: vals.OIDC.UsernameField.String(), + EmailField: vals.OIDC.EmailField.String(), + AuthURLParams: vals.OIDC.AuthURLParams.Value, + IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(), + GroupField: vals.OIDC.GroupField.String(), + GroupFilter: vals.OIDC.GroupRegexFilter.Value(), + CreateMissingGroups: vals.OIDC.GroupAutoCreate.Value(), + GroupMapping: vals.OIDC.GroupMapping.Value, + UserRoleField: vals.OIDC.UserRoleField.String(), + UserRoleMapping: vals.OIDC.UserRoleMapping.Value, + UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(), + SignInText: vals.OIDC.SignInText.String(), + IconURL: vals.OIDC.IconURL.String(), + IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(), + }, nil +} + +func afterCtx(ctx context.Context, fn func()) { + go func() { + <-ctx.Done() + fn() + }() +} + +func enablePrometheus( + ctx context.Context, + logger slog.Logger, + vals *codersdk.DeploymentValues, + options *coderd.Options, +) (closeFn func(), err error) { + options.PrometheusRegistry.MustRegister(collectors.NewGoCollector()) + options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + + closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0) + if err != nil { + return nil, xerrors.Errorf("register active users prometheus metric: %w", err) + } + afterCtx(ctx, closeUsersFunc) + + closeWorkspacesFunc, err := prometheusmetrics.Workspaces(ctx, options.PrometheusRegistry, options.Database, 0) + if err != nil { + return nil, xerrors.Errorf("register workspaces prometheus metric: %w", err) + } + afterCtx(ctx, closeWorkspacesFunc) + + if vals.Prometheus.CollectAgentStats { + closeAgentStatsFunc, err := prometheusmetrics.AgentStats(ctx, logger, options.PrometheusRegistry, options.Database, time.Now(), 0) + if err != nil { + return nil, xerrors.Errorf("register agent stats prometheus metric: %w", err) + } + afterCtx(ctx, closeAgentStatsFunc) + + metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(logger, options.PrometheusRegistry, 0) + if err != nil { + return nil, xerrors.Errorf("can't initialize metrics aggregator: %w", err) + } + + cancelMetricsAggregator := metricsAggregator.Run(ctx) + afterCtx(ctx, cancelMetricsAggregator) + + options.UpdateAgentMetrics = metricsAggregator.Update + err = options.PrometheusRegistry.Register(metricsAggregator) + if err != nil { + return nil, xerrors.Errorf("can't register metrics aggregator as collector: %w", err) + } + } + + //nolint:revive + return ServeHandler( + ctx, logger, promhttp.InstrumentMetricHandler( + options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}), + ), vals.Prometheus.Address.String(), "prometheus", + ), nil +} + func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *clibase.Cmd { var ( - cfg = new(codersdk.DeploymentValues) - opts = cfg.Options() + vals = new(codersdk.DeploymentValues) + opts = vals.Options() ) serverCmd := &clibase.Cmd{ Use: "server", Short: "Start a Coder server", Options: opts, Middleware: clibase.Chain( - WriteConfigMW(cfg), + WriteConfigMW(vals), PrintDeprecatedOptions(), clibase.RequireNArgs(0), ), @@ -197,32 +326,32 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. ctx, cancel := context.WithCancel(inv.Context()) defer cancel() - if cfg.Config != "" { + if vals.Config != "" { cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.") } go DumpHandler(ctx) // Validate bind addresses. - if cfg.Address.String() != "" { - if cfg.TLS.Enable { - cfg.HTTPAddress = "" - cfg.TLS.Address = cfg.Address + if vals.Address.String() != "" { + if vals.TLS.Enable { + vals.HTTPAddress = "" + vals.TLS.Address = vals.Address } else { - _ = cfg.HTTPAddress.Set(cfg.Address.String()) - cfg.TLS.Address.Host = "" - cfg.TLS.Address.Port = "" + _ = vals.HTTPAddress.Set(vals.Address.String()) + vals.TLS.Address.Host = "" + vals.TLS.Address.Port = "" } } - if cfg.TLS.Enable && cfg.TLS.Address.String() == "" { + if vals.TLS.Enable && vals.TLS.Address.String() == "" { return xerrors.Errorf("TLS address must be set if TLS is enabled") } - if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" { + if !vals.TLS.Enable && vals.HTTPAddress.String() == "" { return xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address") } - if cfg.AccessURL.String() != "" && - !(cfg.AccessURL.Scheme == "http" || cfg.AccessURL.Scheme == "https") { + if vals.AccessURL.String() != "" && + !(vals.AccessURL.Scheme == "http" || vals.AccessURL.Scheme == "https") { return xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)") } @@ -230,14 +359,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // was specified. loginRateLimit := 60 filesRateLimit := 12 - if cfg.RateLimit.DisableAll { - cfg.RateLimit.API = -1 + if vals.RateLimit.DisableAll { + vals.RateLimit.API = -1 loginRateLimit = -1 filesRateLimit = -1 } PrintLogo(inv, "Coder") - logger, logCloser, err := BuildLogger(inv, cfg) + logger, logCloser, err := BuildLogger(inv, vals) if err != nil { return xerrors.Errorf("make logger: %w", err) } @@ -260,7 +389,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. notifyCtx, notifyStop := signal.NotifyContext(ctx, InterruptSignals...) defer notifyStop() - cacheDir := cfg.CacheDir.String() + cacheDir := vals.CacheDir.String() err = os.MkdirAll(cacheDir, 0o700) if err != nil { return xerrors.Errorf("create cache directory: %w", err) @@ -271,14 +400,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // which is caught by goleaks. defer http.DefaultClient.CloseIdleConnections() - tracerProvider, sqlDriver, closeTracing := ConfigureTraceProvider(ctx, logger, inv, cfg) + tracerProvider, sqlDriver, closeTracing := ConfigureTraceProvider(ctx, logger, inv, vals) defer func() { logger.Debug(ctx, "closing tracing") traceCloseErr := shutdownWithTimeout(closeTracing, 5*time.Second) logger.Debug(ctx, "tracing closed", slog.Error(traceCloseErr)) }() - httpServers, err := ConfigureHTTPServers(inv, cfg) + httpServers, err := ConfigureHTTPServers(inv, vals) if err != nil { return xerrors.Errorf("configure http(s): %w", err) } @@ -288,7 +417,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. builtinPostgres := false // Only use built-in if PostgreSQL URL isn't specified! - if !cfg.InMemoryDatabase && cfg.PostgresURL == "" { + if !vals.InMemoryDatabase && vals.PostgresURL == "" { var closeFunc func() error cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", config.PostgresPath()) pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger) @@ -296,7 +425,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return err } - err = cfg.PostgresURL.Set(pgURL) + err = vals.PostgresURL.Set(pgURL) if err != nil { return err } @@ -320,9 +449,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. ctx, httpClient, err := ConfigureHTTPClient( ctx, - cfg.TLS.ClientCertFile.String(), - cfg.TLS.ClientKeyFile.String(), - cfg.TLS.ClientCAFile.String(), + vals.TLS.ClientCertFile.String(), + vals.TLS.ClientKeyFile.String(), + vals.TLS.ClientCAFile.String(), ) if err != nil { return xerrors.Errorf("configure http client: %w", err) @@ -334,30 +463,30 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. tunnel *tunnelsdk.Tunnel tunnelDone <-chan struct{} = make(chan struct{}, 1) ) - if cfg.AccessURL.String() == "" { + if vals.AccessURL.String() == "" { cliui.Infof(inv.Stderr, "Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL") - tunnel, err = devtunnel.New(ctx, logger.Named("net.devtunnel"), cfg.WgtunnelHost.String()) + tunnel, err = devtunnel.New(ctx, logger.Named("net.devtunnel"), vals.WgtunnelHost.String()) if err != nil { return xerrors.Errorf("create tunnel: %w", err) } defer tunnel.Close() tunnelDone = tunnel.Wait() - cfg.AccessURL = clibase.URL(*tunnel.URL) + vals.AccessURL = clibase.URL(*tunnel.URL) - if cfg.WildcardAccessURL.String() == "" { + if vals.WildcardAccessURL.String() == "" { // Suffixed wildcard access URL. u, err := url.Parse(fmt.Sprintf("*--%s", tunnel.URL.Hostname())) if err != nil { return xerrors.Errorf("parse wildcard url: %w", err) } - cfg.WildcardAccessURL = clibase.URL(*u) + vals.WildcardAccessURL = clibase.URL(*u) } } - _, accessURLPortRaw, _ := net.SplitHostPort(cfg.AccessURL.Host) + _, accessURLPortRaw, _ := net.SplitHostPort(vals.AccessURL.Host) if accessURLPortRaw == "" { accessURLPortRaw = "80" - if cfg.AccessURL.Scheme == "https" { + if vals.AccessURL.Scheme == "https" { accessURLPortRaw = "443" } } @@ -367,8 +496,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("parse access URL port: %w", err) } - // Warn the user if the access URL appears to be a loopback address. - isLocal, err := IsLocalURL(ctx, cfg.AccessURL.Value()) + // Warn the user if the access URL is loopback or unresolvable. + isLocal, err := IsLocalURL(ctx, vals.AccessURL.Value()) if isLocal || err != nil { reason := "could not be resolved" if isLocal { @@ -377,12 +506,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. cliui.Warnf( inv.Stderr, "The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", - cliui.DefaultStyles.Field.Render(cfg.AccessURL.String()), reason, + cliui.DefaultStyles.Field.Render(vals.AccessURL.String()), reason, ) } // A newline is added before for visibility in terminal output. - cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String()) + cliui.Infof(inv.Stdout, "\nView the Web UI: %s", vals.AccessURL.String()) // Used for zero-trust instance identity with Google Cloud. googleTokenValidator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication()) @@ -390,51 +519,39 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return err } - sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.String()) + sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(vals.SSHKeygenAlgorithm.String()) if err != nil { - return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm, err) + return xerrors.Errorf("parse ssh keygen algorithm %s: %w", vals.SSHKeygenAlgorithm, err) } defaultRegion := &tailcfg.DERPRegion{ EmbeddedRelay: true, - RegionID: int(cfg.DERP.Server.RegionID.Value()), - RegionCode: cfg.DERP.Server.RegionCode.String(), - RegionName: cfg.DERP.Server.RegionName.String(), + RegionID: int(vals.DERP.Server.RegionID.Value()), + RegionCode: vals.DERP.Server.RegionCode.String(), + RegionName: vals.DERP.Server.RegionName.String(), Nodes: []*tailcfg.DERPNode{{ - Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID), - RegionID: int(cfg.DERP.Server.RegionID.Value()), - HostName: cfg.AccessURL.Value().Hostname(), + Name: fmt.Sprintf("%db", vals.DERP.Server.RegionID), + RegionID: int(vals.DERP.Server.RegionID.Value()), + HostName: vals.AccessURL.Value().Hostname(), DERPPort: accessURLPort, STUNPort: -1, - ForceHTTP: cfg.AccessURL.Scheme == "http", + ForceHTTP: vals.AccessURL.Scheme == "http", }}, } - if !cfg.DERP.Server.Enable { + if !vals.DERP.Server.Enable { defaultRegion = nil } - // HACK: see https://github.com/coder/coder/issues/6791. - for _, addr := range cfg.DERP.Server.STUNAddresses { - if addr != "disable" { - continue - } - err := cfg.DERP.Server.STUNAddresses.Replace(nil) - if err != nil { - panic(err) - } - break - } - derpMap, err := tailnet.NewDERPMap( - ctx, defaultRegion, cfg.DERP.Server.STUNAddresses, - cfg.DERP.Config.URL.String(), cfg.DERP.Config.Path.String(), - cfg.DERP.Config.BlockDirect.Value(), + ctx, defaultRegion, vals.DERP.Server.STUNAddresses, + vals.DERP.Config.URL.String(), vals.DERP.Config.Path.String(), + vals.DERP.Config.BlockDirect.Value(), ) if err != nil { return xerrors.Errorf("create derp map: %w", err) } - appHostname := cfg.WildcardAccessURL.String() + appHostname := vals.WildcardAccessURL.String() var appHostnameRegex *regexp.Regexp if appHostname != "" { appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) @@ -448,10 +565,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("read git auth providers from env: %w", err) } - cfg.GitAuthProviders.Value = append(cfg.GitAuthProviders.Value, gitAuthEnv...) + vals.GitAuthProviders.Value = append(vals.GitAuthProviders.Value, gitAuthEnv...) gitAuthConfigs, err := gitauth.ConvertConfig( - cfg.GitAuthProviders.Value, - cfg.AccessURL.Value(), + vals.GitAuthProviders.Value, + vals.AccessURL.Value(), ) if err != nil { return xerrors.Errorf("convert git auth config: %w", err) @@ -463,18 +580,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. ) } - realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins) + realIPConfig, err := httpmw.ParseRealIPConfig(vals.ProxyTrustedHeaders, vals.ProxyTrustedOrigins) if err != nil { return xerrors.Errorf("parse real ip config: %w", err) } - configSSHOptions, err := cfg.SSHConfig.ParseOptions() + configSSHOptions, err := vals.SSHConfig.ParseOptions() if err != nil { - return xerrors.Errorf("parse ssh config options %q: %w", cfg.SSHConfig.SSHConfigOptions.String(), err) + return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err) } options := &coderd.Options{ - AccessURL: cfg.AccessURL.Value(), + AccessURL: vals.AccessURL.Value(), AppHostname: appHostname, AppHostnameRegex: appHostnameRegex, Logger: logger.Named("coderd"), @@ -485,22 +602,22 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. GoogleTokenValidator: googleTokenValidator, GitAuthConfigs: gitAuthConfigs, RealIPConfig: realIPConfig, - SecureAuthCookie: cfg.SecureAuthCookie.Value(), + SecureAuthCookie: vals.SecureAuthCookie.Value(), SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), - MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value(), - AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value(), - DeploymentValues: cfg, + MetricsCacheRefreshInterval: vals.MetricsCacheRefreshInterval.Value(), + AgentStatsRefreshInterval: vals.AgentStatRefreshInterval.Value(), + DeploymentValues: vals, PrometheusRegistry: prometheus.NewRegistry(), - APIRateLimit: int(cfg.RateLimit.API.Value()), + APIRateLimit: int(vals.RateLimit.API.Value()), LoginRateLimit: loginRateLimit, FilesRateLimit: filesRateLimit, HTTPClient: httpClient, TemplateScheduleStore: &atomic.Pointer[schedule.TemplateScheduleStore]{}, UserQuietHoursScheduleStore: &atomic.Pointer[schedule.UserQuietHoursScheduleStore]{}, SSHConfig: codersdk.SSHConfigResponse{ - HostnamePrefix: cfg.SSHConfig.DeploymentName.String(), + HostnamePrefix: vals.SSHConfig.DeploymentName.String(), SSHConfigOptions: configSSHOptions, }, } @@ -508,16 +625,16 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. options.TLSCertificates = httpServers.TLSConfig.Certificates } - if cfg.StrictTransportSecurity > 0 { + if vals.StrictTransportSecurity > 0 { options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions( - int(cfg.StrictTransportSecurity.Value()), cfg.StrictTransportSecurityOptions, + int(vals.StrictTransportSecurity.Value()), vals.StrictTransportSecurityOptions, ) if err != nil { - return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions, err) + return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", vals.StrictTransportSecurityOptions, err) } } - if cfg.UpdateCheck { + if vals.UpdateCheck { options.UpdateCheckOptions = &updatecheck.Options{ // Avoid spamming GitHub API checking for updates. Interval: 24 * time.Hour, @@ -536,103 +653,39 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } - if cfg.OAuth2.Github.ClientSecret != "" { - options.GithubOAuth2Config, err = configureGithubOAuth2(cfg.AccessURL.Value(), - cfg.OAuth2.Github.ClientID.String(), - cfg.OAuth2.Github.ClientSecret.String(), - cfg.OAuth2.Github.AllowSignups.Value(), - cfg.OAuth2.Github.AllowEveryone.Value(), - cfg.OAuth2.Github.AllowedOrgs, - cfg.OAuth2.Github.AllowedTeams, - cfg.OAuth2.Github.EnterpriseBaseURL.String(), + if vals.OAuth2.Github.ClientSecret != "" { + options.GithubOAuth2Config, err = configureGithubOAuth2(vals.AccessURL.Value(), + vals.OAuth2.Github.ClientID.String(), + vals.OAuth2.Github.ClientSecret.String(), + vals.OAuth2.Github.AllowSignups.Value(), + vals.OAuth2.Github.AllowEveryone.Value(), + vals.OAuth2.Github.AllowedOrgs, + vals.OAuth2.Github.AllowedTeams, + vals.OAuth2.Github.EnterpriseBaseURL.String(), ) if err != nil { return xerrors.Errorf("configure github oauth2: %w", err) } } - if cfg.OIDC.ClientKeyFile != "" || cfg.OIDC.ClientSecret != "" { - if cfg.OIDC.ClientID == "" { - return xerrors.Errorf("OIDC client ID must be set!") - } - if cfg.OIDC.IssuerURL == "" { - return xerrors.Errorf("OIDC issuer URL must be set!") - } - - if cfg.OIDC.IgnoreEmailVerified { + if vals.OIDC.ClientKeyFile != "" || vals.OIDC.ClientSecret != "" { + if vals.OIDC.IgnoreEmailVerified { logger.Warn(ctx, "coder will not check email_verified for OIDC logins") } - oidcProvider, err := oidc.NewProvider( - ctx, cfg.OIDC.IssuerURL.String(), - ) - if err != nil { - return xerrors.Errorf("configure oidc provider: %w", err) - } - redirectURL, err := cfg.AccessURL.Value().Parse("/api/v2/users/oidc/callback") + oc, err := createOIDCConfig(ctx, vals) if err != nil { - return xerrors.Errorf("parse oidc oauth callback url: %w", err) - } - // If the scopes contain 'groups', we enable group support. - // Do not override any custom value set by the user. - if slice.Contains(cfg.OIDC.Scopes, "groups") && cfg.OIDC.GroupField == "" { - cfg.OIDC.GroupField = "groups" - } - oauthCfg := &oauth2.Config{ - ClientID: cfg.OIDC.ClientID.String(), - ClientSecret: cfg.OIDC.ClientSecret.String(), - RedirectURL: redirectURL.String(), - Endpoint: oidcProvider.Endpoint(), - Scopes: cfg.OIDC.Scopes, - } - - var useCfg httpmw.OAuth2Config = oauthCfg - if cfg.OIDC.ClientKeyFile != "" { - // PKI authentication is done in the params. If a - // counter example is found, we can add a config option to - // change this. - oauthCfg.Endpoint.AuthStyle = oauth2.AuthStyleInParams - if cfg.OIDC.ClientSecret != "" { - return xerrors.Errorf("cannot specify both oidc client secret and oidc client key file") - } - - pkiCfg, err := configureOIDCPKI(oauthCfg, cfg.OIDC.ClientKeyFile.Value(), cfg.OIDC.ClientCertFile.Value()) - if err != nil { - return xerrors.Errorf("configure oauth pki authentication: %w", err) - } - useCfg = pkiCfg - } - options.OIDCConfig = &coderd.OIDCConfig{ - OAuth2Config: useCfg, - Provider: oidcProvider, - Verifier: oidcProvider.Verifier(&oidc.Config{ - ClientID: cfg.OIDC.ClientID.String(), - }), - EmailDomain: cfg.OIDC.EmailDomain, - AllowSignups: cfg.OIDC.AllowSignups.Value(), - UsernameField: cfg.OIDC.UsernameField.String(), - EmailField: cfg.OIDC.EmailField.String(), - AuthURLParams: cfg.OIDC.AuthURLParams.Value, - IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(), - GroupField: cfg.OIDC.GroupField.String(), - GroupFilter: cfg.OIDC.GroupRegexFilter.Value(), - CreateMissingGroups: cfg.OIDC.GroupAutoCreate.Value(), - GroupMapping: cfg.OIDC.GroupMapping.Value, - UserRoleField: cfg.OIDC.UserRoleField.String(), - UserRoleMapping: cfg.OIDC.UserRoleMapping.Value, - UserRolesDefault: cfg.OIDC.UserRolesDefault.GetSlice(), - SignInText: cfg.OIDC.SignInText.String(), - IconURL: cfg.OIDC.IconURL.String(), - IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(), + return xerrors.Errorf("create oidc config: %w", err) } + options.OIDCConfig = oc } - if cfg.InMemoryDatabase { + if vals.InMemoryDatabase { // This is only used for testing. options.Database = dbfake.New() options.Pubsub = pubsub.NewInMemory() } else { - sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.String()) + sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, vals.PostgresURL.String()) if err != nil { return xerrors.Errorf("connect to postgres: %w", err) } @@ -641,7 +694,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. }() options.Database = database.New(sqlDB) - options.Pubsub, err = pubsub.New(ctx, sqlDB, cfg.PostgresURL.String()) + options.Pubsub, err = pubsub.New(ctx, sqlDB, vals.PostgresURL.String()) if err != nil { return xerrors.Errorf("create pubsub: %w", err) } @@ -748,7 +801,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return err } - if cfg.Telemetry.Enable { + if vals.Telemetry.Enable { gitAuth := make([]telemetry.GitAuth, 0) // TODO: var gitAuthConfigs []codersdk.GitAuthConfig @@ -763,15 +816,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. DeploymentID: deploymentID, Database: options.Database, Logger: logger.Named("telemetry"), - URL: cfg.Telemetry.URL.Value(), - Wildcard: cfg.WildcardAccessURL.String() != "", - DERPServerRelayURL: cfg.DERP.Server.RelayURL.String(), + URL: vals.Telemetry.URL.Value(), + Wildcard: vals.WildcardAccessURL.String() != "", + DERPServerRelayURL: vals.DERP.Server.RelayURL.String(), GitAuth: gitAuth, - GitHubOAuth: cfg.OAuth2.Github.ClientID != "", - OIDCAuth: cfg.OIDC.ClientID != "", - OIDCIssuerURL: cfg.OIDC.IssuerURL.String(), - Prometheus: cfg.Prometheus.Enable.Value(), - STUN: len(cfg.DERP.Server.STUNAddresses) != 0, + GitHubOAuth: vals.OAuth2.Github.ClientID != "", + OIDCAuth: vals.OIDC.ClientID != "", + OIDCIssuerURL: vals.OIDC.IssuerURL.String(), + Prometheus: vals.Prometheus.Enable.Value(), + STUN: len(vals.DERP.Server.STUNAddresses) != 0, Tunnel: tunnel != nil, }) if err != nil { @@ -782,56 +835,25 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // This prevents the pprof import from being accidentally deleted. _ = pprof.Handler - if cfg.Pprof.Enable { + if vals.Pprof.Enable { //nolint:revive - defer ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")() - } - if cfg.Prometheus.Enable { - options.PrometheusRegistry.MustRegister(collectors.NewGoCollector()) - options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - - closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0) - if err != nil { - return xerrors.Errorf("register active users prometheus metric: %w", err) - } - defer closeUsersFunc() - - closeWorkspacesFunc, err := prometheusmetrics.Workspaces(ctx, options.PrometheusRegistry, options.Database, 0) + defer ServeHandler(ctx, logger, nil, vals.Pprof.Address.String(), "pprof")() + } + if vals.Prometheus.Enable { + closeFn, err := enablePrometheus( + ctx, + logger.Named("prometheus"), + vals, + options, + ) if err != nil { - return xerrors.Errorf("register workspaces prometheus metric: %w", err) - } - defer closeWorkspacesFunc() - - if cfg.Prometheus.CollectAgentStats { - closeAgentStatsFunc, err := prometheusmetrics.AgentStats(ctx, logger, options.PrometheusRegistry, options.Database, time.Now(), 0) - if err != nil { - return xerrors.Errorf("register agent stats prometheus metric: %w", err) - } - defer closeAgentStatsFunc() - - metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(logger, options.PrometheusRegistry, 0) - if err != nil { - return xerrors.Errorf("can't initialize metrics aggregator: %w", err) - } - - cancelMetricsAggregator := metricsAggregator.Run(ctx) - defer cancelMetricsAggregator() - - options.UpdateAgentMetrics = metricsAggregator.Update - err = options.PrometheusRegistry.Register(metricsAggregator) - if err != nil { - return xerrors.Errorf("can't register metrics aggregator as collector: %w", err) - } + return xerrors.Errorf("enable prometheus: %w", err) } - - //nolint:revive - defer ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler( - options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}), - ), cfg.Prometheus.Address.String(), "prometheus")() + defer closeFn() } - if cfg.Swagger.Enable { - options.SwaggerEndpoint = cfg.Swagger.Enable.Value() + if vals.Swagger.Enable { + options.SwaggerEndpoint = vals.Swagger.Enable.Value() } batcher, closeBatcher, err := batchstats.New(ctx, @@ -855,7 +877,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("create coder API: %w", err) } - if cfg.Prometheus.Enable { + if vals.Prometheus.Enable { // Agent metrics require reference to the tailnet coordinator, so must be initiated after Coder API. closeAgentsFunc, err := prometheusmetrics.Agents(ctx, logger, options.PrometheusRegistry, coderAPI.Database, &coderAPI.TailnetCoordinator, coderAPI.DERPMap, coderAPI.Options.AgentInactiveDisconnectTimeout, 0) if err != nil { @@ -903,10 +925,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. var provisionerdWaitGroup sync.WaitGroup defer provisionerdWaitGroup.Wait() provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry) - for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ { + for i := int64(0); i < vals.Provisioner.Daemons.Value(); i++ { daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i)) daemon, err := newProvisionerDaemon( - ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, &provisionerdWaitGroup, + ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, ) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) @@ -925,8 +947,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // Wrap the server in middleware that redirects to the access URL if // the request is not to a local IP. var handler http.Handler = coderAPI.RootHandler - if cfg.RedirectToAccessURL { - handler = redirectToAccessURL(handler, cfg.AccessURL.Value(), tunnel != nil, appHostnameRegex) + if vals.RedirectToAccessURL { + handler = redirectToAccessURL(handler, vals.AccessURL.Value(), tunnel != nil, appHostnameRegex) } // ReadHeaderTimeout is purposefully not enabled. It caused some @@ -983,12 +1005,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("notify systemd: %w", err) } - autobuildTicker := time.NewTicker(cfg.AutobuildPollInterval.Value()) + autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value()) defer autobuildTicker.Stop() autobuildExecutor := autobuild.NewExecutor(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildTicker.C) autobuildExecutor.Run() - hangDetectorTicker := time.NewTicker(cfg.JobHangDetectorInterval.Value()) + hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value()) defer hangDetectorTicker.Stop() hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, logger, hangDetectorTicker.C) hangDetector.Start() @@ -1047,9 +1069,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. go func() { defer wg.Done() - if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok { - cliui.Infof(inv.Stdout, "Shutting down provisioner daemon %d...", id) - } + r.Verbosef(inv, "Shutting down provisioner daemon %d...", id) err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second) if err != nil { cliui.Errorf(inv.Stderr, "Failed to shutdown provisioner daemon %d: %s\n", id, err) @@ -1060,9 +1080,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. cliui.Errorf(inv.Stderr, "Close provisioner daemon %d: %s\n", id, err) return } - if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok { - cliui.Infof(inv.Stdout, "Gracefully shut down provisioner daemon %d", id) - } + r.Verbosef(inv, "Gracefully shut down provisioner daemon %d", id) }() } wg.Wait() diff --git a/cli/server_test.go b/cli/server_test.go index 5609ac76472d8..7b38bb76f9e15 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1496,31 +1496,6 @@ func TestServer(t *testing.T) { w.RequireSuccess() }) }) - t.Run("DisableDERP", func(t *testing.T) { - t.Parallel() - - // Make sure that $CODER_DERP_SERVER_STUN_ADDRESSES can be set to - // disable STUN. - - inv, cfg := clitest.New(t, - "server", - "--in-memory", - "--http-address", ":0", - "--access-url", "https://example.com", - ) - inv.Environ.Set("CODER_DERP_SERVER_STUN_ADDRESSES", "disable") - ptytest.New(t).Attach(inv) - clitest.Start(t, inv) - gotURL := waitAccessURL(t, cfg) - client := codersdk.New(gotURL) - - ctx := testutil.Context(t, testutil.WaitMedium) - _ = coderdtest.CreateFirstUser(t, client) - gotConfig, err := client.DeploymentConfig(ctx) - require.NoError(t, err) - - require.Len(t, gotConfig.Values.DERP.Server.STUNAddresses, 0) - }) } func TestServer_Production(t *testing.T) { diff --git a/cli/ssh.go b/cli/ssh.go index 9cc86e1d3781a..4455b8987cc5f 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -40,7 +40,6 @@ var ( autostopNotifyCountdown = []time.Duration{30 * time.Minute} ) -//nolint:gocyclo func (r *RootCmd) ssh() *clibase.Cmd { var ( stdio bool diff --git a/coderd/coderd.go b/coderd/coderd.go index 4aac3867b60f9..0338a020eae36 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -184,8 +184,6 @@ type Options struct { // @in header // @name Coder-Session-Token // New constructs a Coder API handler. -// -//nolint:gocyclo func New(options *Options) *API { if options == nil { options = &Options{} diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 08979a67a8c45..5ef17af359ca7 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -329,7 +329,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can t.Cleanup(stunCleanup) stunAddresses = []string{stunAddr.String()} options.DeploymentValues.DERP.Server.STUNAddresses = stunAddresses - } else if dvStunAddresses[0] != "disable" { + } else if dvStunAddresses[0] != tailnet.DisableSTUN { stunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value() } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index b8be4b2e64ef8..d26be831db122 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -6053,7 +6053,6 @@ func (q *FakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]d return users, nil } -//nolint:gocyclo func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) { if err := validateDatabaseType(arg); err != nil { return nil, err diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index f61606a1425b9..695892c86c9cc 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -744,8 +744,6 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p } // CompleteJob is triggered by a provision daemon to mark a provisioner job as completed. -// -//nolint:gocyclo func (server *Server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) { ctx, span := server.startTrace(ctx, tracing.FuncName()) defer span.End() diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 1914bfa2ee182..6f03d63dff785 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -303,7 +303,6 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ // @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request" // @Success 200 {object} codersdk.WorkspaceBuild // @Router /workspaces/{workspace}/builds [post] -// nolint:gocyclo func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index 96ef993ffc4d2..d2e1541c7c89b 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -97,7 +97,6 @@ type State struct { // ConvertState consumes Terraform state and a GraphViz representation // produced by `terraform graph` to produce resources consumable by Coder. -// nolint:gocyclo func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) { parsedGraph, err := gographviz.ParseString(rawGraph) if err != nil { diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index af8d4fcdf2c85..9eeaf5664911e 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -655,8 +655,6 @@ type TypescriptType struct { // Eg: // // []byte returns "string" -// -//nolint:gocyclo func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { switch ty := ty.(type) { case *types.Basic: diff --git a/tailnet/derpmap.go b/tailnet/derpmap.go index f35e48156b768..817ef6a79dd78 100644 --- a/tailnet/derpmap.go +++ b/tailnet/derpmap.go @@ -13,10 +13,12 @@ import ( "tailscale.com/tailcfg" ) +const DisableSTUN = "disable" + func STUNRegions(baseRegionID int, stunAddrs []string) ([]*tailcfg.DERPRegion, error) { regions := make([]*tailcfg.DERPRegion, 0, len(stunAddrs)) for index, stunAddr := range stunAddrs { - if stunAddr == "disable" { + if stunAddr == DisableSTUN { return []*tailcfg.DERPRegion{}, nil } From 594a6aae19634cf8550bd50da36862648e68064e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sun, 27 Aug 2023 14:51:13 -0500 Subject: [PATCH 27/40] chore: format oidctest (#9362) --- coderd/coderdtest/oidctest/idp.go | 2 +- coderd/coderdtest/oidctest/idp_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index 912d9acd7c221..3ca8cadbc9ff9 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -663,7 +663,7 @@ func (f *FakeIDP) HTTPClient(rest *http.Client) *http.Client { return f.fakeCoderd(req) } if rest == nil || rest.Transport == nil { - return nil, fmt.Errorf("unexpected network request to %q", req.URL.Host) + return nil, xerrors.Errorf("unexpected network request to %q", req.URL.Host) } return rest.Transport.RoundTrip(req) } diff --git a/coderd/coderdtest/oidctest/idp_test.go b/coderd/coderdtest/oidctest/idp_test.go index 0dc1149d93fa9..519635b067916 100644 --- a/coderd/coderdtest/oidctest/idp_test.go +++ b/coderd/coderdtest/oidctest/idp_test.go @@ -10,10 +10,11 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" - "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/require" "golang.org/x/oauth2" + + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" ) // TestFakeIDPBasicFlow tests the basic flow of the fake IDP. From 79aba1d5ff75d1fdfe82c347f7d8bd027ea45813 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 28 Aug 2023 12:21:54 +0300 Subject: [PATCH 28/40] ci: remove redundant groups from dependabot.yaml (#9365) --- .github/dependabot.yaml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 5b9f7a9c6597a..ebd0c3c55ce59 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -117,11 +117,6 @@ updates: - "@eslint*" - "@typescript-eslint/eslint-plugin" - "@typescript-eslint/parser" - jest: - patterns: - - "jest*" - - "@swc/jest" - - "@types/jest" - package-ecosystem: "npm" directory: "/offlinedocs/" @@ -146,20 +141,6 @@ updates: - version-update:semver-major # Update dogfood. - - package-ecosystem: "docker" - directory: "/dogfood/" - schedule: - interval: "weekly" - time: "06:00" - timezone: "America/Chicago" - commit-message: - prefix: "chore" - labels: [] - groups: - dogfood-docker: - patterns: - - "*" - - package-ecosystem: "terraform" directory: "/dogfood/" schedule: From 506b81adeb2ff475197da0f1bff22a346ebba8fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:41:05 +0000 Subject: [PATCH 29/40] ci: bump crate-ci/typos@v1.16.6 to crate-ci/typos@v1.16.8 (#9372) bumps crate-ci/typos@v1.16.6 to crate-ci/typos@v1.16.8 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 66bf59853bb97..17782be96319c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -137,7 +137,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@v1.16.6 + uses: crate-ci/typos@v1.16.8 with: config: .github/workflows/typos.toml From b6e808d11628f6c407ca1c025a717edae0533173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:08:32 +0300 Subject: [PATCH 30/40] chore: bump github.com/charmbracelet/lipgloss from 0.7.1 to 0.8.0 (#9370) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 5 ++--- go.sum | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 496f42ebf312d..67aac5fa2cbe9 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/charmbracelet/glamour v0.6.0 // In later at least v0.7.1, lipgloss changes its terminal detection // which breaks most of our CLI golden files tests. - github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/lipgloss v0.8.0 github.com/cli/safeexec v1.0.1 github.com/codeclysm/extract/v3 v3.1.1 github.com/coder/flog v1.1.0 @@ -116,7 +116,6 @@ require ( github.com/go-playground/validator/v10 v10.15.0 github.com/gofrs/flock v0.8.1 github.com/gohugoio/hugo v0.117.0 - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-migrate/migrate/v4 v4.16.0 github.com/golang/mock v1.6.0 @@ -185,7 +184,7 @@ require ( golang.org/x/sys v0.11.0 golang.org/x/term v0.11.0 golang.org/x/text v0.12.0 - golang.org/x/time v0.3.0 + golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.12.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b diff --git a/go.sum b/go.sum index 3971c3fed5e5f..87a117866bdf6 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,8 @@ github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM2 github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -391,8 +391,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/hugo v0.117.0 h1:VP7MVke7R36zjUkWezg7mEtfnwHm2L5u0JwvMHvcsZQ= github.com/gohugoio/hugo v0.117.0/go.mod h1:b6mjGjvIqRD36x+ePNDhn1ZCXdWBkvu95+dZNTCuoDM= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.16.0 h1:FU2GR7EdAO0LmhNLcKthfDzuYCtMcWNR7rUbZjsgH3o= From 35d0809830f3a9953a9da9edcf398c531e9d789c Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 28 Aug 2023 18:20:52 +0300 Subject: [PATCH 31/40] ci: prefix dependabot github-actions PRs with `ci:` (#9376) --- .github/dependabot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index ebd0c3c55ce59..76048b9fe398d 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -8,7 +8,7 @@ updates: timezone: "America/Chicago" labels: [] commit-message: - prefix: "chore" + prefix: "ci" ignore: # These actions deliver the latest versions by updating the major # release tag, so ignore minor and patch versions From 80425c32bf1ce8ac530ee75c244f4f8469d917ea Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 28 Aug 2023 18:22:39 +0200 Subject: [PATCH 32/40] fix(site): workaround: reload page every 3sec (#9387) --- site/e2e/helpers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 8743b13730b08..1cc442ce72c70 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -38,12 +38,18 @@ export const createWorkspace = async ( await page.getByTestId("form-submit").click() await expect(page).toHaveURL("/@admin/" + name) + + // FIXME: workaround for https://github.com/coder/coder/issues/8566 + const reloadTimer = setInterval(async () => { + await page.reload() + }, 3000) await page.waitForSelector( "span[data-testid='build-status'] >> text=Running", { state: "visible", }, ) + clearInterval(reloadTimer) return name } From ce9b048f06646ce5f368e4b93691aab5200cf4ee Mon Sep 17 00:00:00 2001 From: Kayla Washburn Date: Mon, 28 Aug 2023 11:27:51 -0600 Subject: [PATCH 33/40] feat(site): improve template publishing flow (#9346) --- site/src/components/Alert/Alert.stories.tsx | 8 + .../PublishTemplateVersionDialog.tsx | 14 +- .../TemplateVersionEditor.stories.tsx | 317 +----------------- .../TemplateVersionEditor.tsx | 39 ++- .../TemplateVersionEditorPage.test.tsx | 7 +- .../TemplateVersionEditorPage.tsx | 12 +- site/src/testHelpers/entities.ts | 309 +++++++++++++++++ .../templateVersionEditorXService.ts | 20 +- 8 files changed, 399 insertions(+), 327 deletions(-) diff --git a/site/src/components/Alert/Alert.stories.tsx b/site/src/components/Alert/Alert.stories.tsx index ada32cd994ec9..c5139f7c1c1a1 100644 --- a/site/src/components/Alert/Alert.stories.tsx +++ b/site/src/components/Alert/Alert.stories.tsx @@ -17,6 +17,14 @@ const ExampleAction = ( ) +export const Success: Story = { + args: { + children: "You're doing great!", + severity: "success", + onRetry: undefined, + }, +} + export const Warning: Story = { args: { children: "This is a warning", diff --git a/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx b/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx index 8fcd96bcce157..a834f4446abd2 100644 --- a/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx +++ b/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx @@ -11,6 +11,12 @@ import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import { Stack } from "components/Stack/Stack" +export const Language = { + versionNameLabel: "Version name", + messagePlaceholder: "Write a short message about the changes you made...", + defaultCheckboxLabel: "Promote to default version", +} + export type PublishTemplateVersionDialogProps = DialogProps & { defaultName: string isPublishing: boolean @@ -33,7 +39,7 @@ export const PublishTemplateVersionDialog: FC< initialValues: { name: defaultName, message: "", - isActiveVersion: false, + isActiveVersion: true, }, validationSchema: Yup.object({ name: Yup.string().required(), @@ -67,7 +73,7 @@ export const PublishTemplateVersionDialog: FC< @@ -75,7 +81,7 @@ export const PublishTemplateVersionDialog: FC< 0.11.0"...', - }, - { - id: 938500, - created_at: "2023-08-25T19:07:44.668Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: '- Finding kreuzwerker/docker versions matching "~> 3.0.1"...', - }, - { - id: 938501, - created_at: "2023-08-25T19:07:44.722Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "- Using coder/coder v0.11.1 from the shared cache directory", - }, - { - id: 938502, - created_at: "2023-08-25T19:07:44.857Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "- Using kreuzwerker/docker v3.0.2 from the shared cache directory", - }, - { - id: 938503, - created_at: "2023-08-25T19:07:45.081Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "Terraform has created a lock file .terraform.lock.hcl to record the provider", - }, - { - id: 938504, - created_at: "2023-08-25T19:07:45.081Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "selections it made above. Include this file in your version control repository", - }, - { - id: 938505, - created_at: "2023-08-25T19:07:45.081Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "so that Terraform can guarantee to make the same selections by default when", - }, - { - id: 938506, - created_at: "2023-08-25T19:07:45.082Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: 'you run "terraform init" in the future.', - }, - { - id: 938507, - created_at: "2023-08-25T19:07:45.083Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "Terraform has been successfully initialized!", - }, - { - id: 938508, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - 'You may now begin working with Terraform. Try running "terraform plan" to see', - }, - { - id: 938509, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "any changes that are required for your infrastructure. All Terraform commands", - }, - { - id: 938510, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "should now work.", - }, - { - id: 938511, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "If you ever set or change modules or backend configuration for Terraform,", - }, - { - id: 938512, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "rerun this command to reinitialize your working directory. If you forget, other", - }, - { - id: 938513, - created_at: "2023-08-25T19:07:45.084Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "commands will detect it and remind you to do so if necessary.", - }, - { - id: 938514, - created_at: "2023-08-25T19:07:45.143Z", - log_source: "provisioner", - log_level: "info", - stage: "Detecting persistent resources", - output: "Terraform 1.1.9", - }, - { - id: 938515, - created_at: "2023-08-25T19:07:46.297Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: "Warning: Argument is deprecated", - }, - { - id: 938516, - created_at: "2023-08-25T19:07:46.297Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', - }, - { - id: 938517, - created_at: "2023-08-25T19:07:46.297Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: " 15: feature_use_managed_variables = true", - }, - { - id: 938518, - created_at: "2023-08-25T19:07:46.297Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 938519, - created_at: "2023-08-25T19:07:46.297Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: - "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", - }, - { - id: 938520, - created_at: "2023-08-25T19:07:46.3Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: "Error: ephemeral parameter requires the default property", - }, - { - id: 938521, - created_at: "2023-08-25T19:07:46.3Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: - 'on devcontainer-on-docker.tf line 27, in data "coder_parameter" "another_one":', - }, - { - id: 938522, - created_at: "2023-08-25T19:07:46.3Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: ' 27: data "coder_parameter" "another_one" {', - }, - { - id: 938523, - created_at: "2023-08-25T19:07:46.301Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 938524, - created_at: "2023-08-25T19:07:46.301Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 938525, - created_at: "2023-08-25T19:07:46.303Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: "Warning: Argument is deprecated", - }, - { - id: 938526, - created_at: "2023-08-25T19:07:46.303Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', - }, - { - id: 938527, - created_at: "2023-08-25T19:07:46.303Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: " 15: feature_use_managed_variables = true", - }, - { - id: 938528, - created_at: "2023-08-25T19:07:46.303Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 938529, - created_at: "2023-08-25T19:07:46.303Z", - log_source: "provisioner", - log_level: "warn", - stage: "Detecting persistent resources", - output: - "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", - }, - { - id: 938530, - created_at: "2023-08-25T19:07:46.311Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Cleaning Up", - output: "", - }, - ], + buildLogs: MockWorkspaceExtendedBuildLogs, + }, +} + +export const Published = { + args: { + publishedVersion: MockTemplateVersion, }, } diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 33577ea4488c4..aca932c29180a 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -64,6 +64,9 @@ export interface TemplateVersionEditorProps { onConfirmPublish: (data: PublishVersionData) => void onCancelPublish: () => void publishingError: unknown + publishedVersion?: TemplateVersion + publishedVersionIsDefault?: boolean + onCreateWorkspace: () => void isAskingPublishParameters: boolean isPromptingMissingVariables: boolean isPublishing: boolean @@ -97,9 +100,12 @@ export const TemplateVersionEditor: FC = ({ onPublish, onConfirmPublish, onCancelPublish, - publishingError, isAskingPublishParameters, isPublishing, + publishingError, + publishedVersion, + publishedVersionIsDefault, + onCreateWorkspace, buildLogs, resources, isPromptingMissingVariables, @@ -107,11 +113,9 @@ export const TemplateVersionEditor: FC = ({ onSubmitMissingVariableValues, onCancelSubmitMissingVariableValues, }) => { - const [selectedTab, setSelectedTab] = useState(() => { - // If resources are provided, show them by default! - // This is for Storybook! - return resources ? 1 : 0 - }) + // If resources are provided, show them by default! + // This is for Storybook! + const [selectedTab, setSelectedTab] = useState(() => (resources ? 1 : 0)) const [fileTree, setFileTree] = useState(defaultFileTree) const [createFileOpen, setCreateFileOpen] = useState(false) const [deleteFileOpen, setDeleteFileOpen] = useState() @@ -204,6 +208,29 @@ export const TemplateVersionEditor: FC = ({
+ {publishedVersion && ( + + Create a workspace + + ) + } + > + Successfully published {publishedVersion.name}! + + )} +
{/* Only start to show the build when a new template version is building */} {templateVersion.id !== firstTemplateVersionOnEditor.current.id && ( diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index b969363951fc8..0a1fbdcdb25a4 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -7,6 +7,7 @@ import { MockTemplateVersion, MockWorkspaceBuildLogs, } from "testHelpers/entities" +import { Language } from "../../../components/TemplateVersionEditor/PublishTemplateVersionDialog" // For some reason this component in Jest is throwing a MUI style warning so, // since we don't need it for this test, we can mock it out @@ -67,9 +68,6 @@ test("Use custom name, message and set it as active when publishing", async () = const messageField = within(publishDialog).getByLabelText("Message") await user.clear(messageField) await user.type(messageField, "Informative message") - await user.click( - within(publishDialog).getByLabelText("Promote to default version"), - ) await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), ) @@ -132,6 +130,9 @@ test("Do not mark as active if promote is not checked", async () => { const nameField = within(publishDialog).getByLabelText("Version name") await user.clear(nameField) await user.type(nameField, "v1.0") + await user.click( + within(publishDialog).getByLabelText(Language.defaultCheckboxLabel), + ) await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), ) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 36bad25e7ef83..078e14e406cec 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -4,7 +4,7 @@ import { useOrganizationId } from "hooks/useOrganizationId" import { usePermissions } from "hooks/usePermissions" import { FC } from "react" import { Helmet } from "react-helmet-async" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "utils/page" import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService" import { useTemplateVersionData } from "./data" @@ -15,6 +15,7 @@ type Params = { } export const TemplateVersionEditorPage: FC = () => { + const navigate = useNavigate() const { version: versionName, template: templateName } = useParams() as Params const orgId = useOrganizationId() const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { @@ -72,8 +73,15 @@ export const TemplateVersionEditorPage: FC = () => { isAskingPublishParameters={editorState.matches( "askPublishParameters", )} - publishingError={editorState.context.publishingError} isPublishing={editorState.matches("publishingVersion")} + publishingError={editorState.context.publishingError} + publishedVersion={editorState.context.lastSuccessfulPublishedVersion} + publishedVersionIsDefault={ + editorState.context.lastSuccessfulPublishIsDefault + } + onCreateWorkspace={() => { + navigate(`/templates/${templateName}/workspace`) + }} disablePreview={editorState.hasTag("loading")} disableUpdate={ editorState.hasTag("loading") || diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 349857550abf5..9ddc58ff2954b 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1407,6 +1407,315 @@ export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ }, ] +export const MockWorkspaceExtendedBuildLogs: TypesGen.ProvisionerJobLog[] = [ + { + id: 938494, + created_at: "2023-08-25T19:07:43.331Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Setting up", + output: "", + }, + { + id: 938495, + created_at: "2023-08-25T19:07:43.331Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Parsing template parameters", + output: "", + }, + { + id: 938496, + created_at: "2023-08-25T19:07:43.339Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938497, + created_at: "2023-08-25T19:07:44.15Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing the backend...", + }, + { + id: 938498, + created_at: "2023-08-25T19:07:44.215Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Initializing provider plugins...", + }, + { + id: 938499, + created_at: "2023-08-25T19:07:44.216Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding coder/coder versions matching "~> 0.11.0"...', + }, + { + id: 938500, + created_at: "2023-08-25T19:07:44.668Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: '- Finding kreuzwerker/docker versions matching "~> 3.0.1"...', + }, + { + id: 938501, + created_at: "2023-08-25T19:07:44.722Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Using coder/coder v0.11.1 from the shared cache directory", + }, + { + id: 938502, + created_at: "2023-08-25T19:07:44.857Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "- Using kreuzwerker/docker v3.0.2 from the shared cache directory", + }, + { + id: 938503, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "Terraform has created a lock file .terraform.lock.hcl to record the provider", + }, + { + id: 938504, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "selections it made above. Include this file in your version control repository", + }, + { + id: 938505, + created_at: "2023-08-25T19:07:45.081Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "so that Terraform can guarantee to make the same selections by default when", + }, + { + id: 938506, + created_at: "2023-08-25T19:07:45.082Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: 'you run "terraform init" in the future.', + }, + { + id: 938507, + created_at: "2023-08-25T19:07:45.083Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "Terraform has been successfully initialized!", + }, + { + id: 938508, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + 'You may now begin working with Terraform. Try running "terraform plan" to see', + }, + { + id: 938509, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "any changes that are required for your infrastructure. All Terraform commands", + }, + { + id: 938510, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "should now work.", + }, + { + id: 938511, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "If you ever set or change modules or backend configuration for Terraform,", + }, + { + id: 938512, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: + "rerun this command to reinitialize your working directory. If you forget, other", + }, + { + id: 938513, + created_at: "2023-08-25T19:07:45.084Z", + log_source: "provisioner", + log_level: "debug", + stage: "Detecting persistent resources", + output: "commands will detect it and remind you to do so if necessary.", + }, + { + id: 938514, + created_at: "2023-08-25T19:07:45.143Z", + log_source: "provisioner", + log_level: "info", + stage: "Detecting persistent resources", + output: "Terraform 1.1.9", + }, + { + id: 938515, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "Warning: Argument is deprecated", + }, + { + id: 938516, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938517, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: " 15: feature_use_managed_variables = true", + }, + { + id: 938518, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938519, + created_at: "2023-08-25T19:07:46.297Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: + "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", + }, + { + id: 938520, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "Error: ephemeral parameter requires the default property", + }, + { + id: 938521, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: + 'on devcontainer-on-docker.tf line 27, in data "coder_parameter" "another_one":', + }, + { + id: 938522, + created_at: "2023-08-25T19:07:46.3Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: ' 27: data "coder_parameter" "another_one" {', + }, + { + id: 938523, + created_at: "2023-08-25T19:07:46.301Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938524, + created_at: "2023-08-25T19:07:46.301Z", + log_source: "provisioner", + log_level: "error", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938525, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "Warning: Argument is deprecated", + }, + { + id: 938526, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938527, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: " 15: feature_use_managed_variables = true", + }, + { + id: 938528, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: "", + }, + { + id: 938529, + created_at: "2023-08-25T19:07:46.303Z", + log_source: "provisioner", + log_level: "warn", + stage: "Detecting persistent resources", + output: + "Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.", + }, + { + id: 938530, + created_at: "2023-08-25T19:07:46.311Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Cleaning Up", + output: "", + }, +] + export const MockCancellationMessage = { message: "Job successfully canceled", } diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 4c4623f5bc374..990479b9c2475 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -24,13 +24,15 @@ export interface TemplateVersionEditorMachineContext { buildLogs?: ProvisionerJobLog[] tarReader?: TarReader publishingError?: unknown + lastSuccessfulPublishedVersion?: TemplateVersion + lastSuccessfulPublishIsDefault?: boolean missingVariables?: TemplateVersionVariable[] missingVariableValues?: VariableValue[] } export const templateVersionEditorMachine = createMachine( { - /** @xstate-layout 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 */ + /** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhqgamAE6wCWA9gHYCiEpy5RAdKZfaTlqQF6tQDEASQByggCqCAggBlBALWoBtAAwBdRKAzkyyCpQ0gAHogC0AFgDsypgDYAnAFZlFpzefKbADgA0IAJ6IAIzKngDMTIF2FmYONoFW9s4Avkm+qJi4BMRkVLT0jCwQWGD8AMIAStSSYtQA+vjU5QDKggDywirqSCBaOnoGxggmoTYOTKEATIEOThNxDmajvgEIgTFjDhaeNpPTZpbRKWno2HhghCR6eQzMpEUlAAoAqgBCsk0AEp0GvexUA6Y5mNlKEzBNPJ5Ap4JjCbEt-EEJsozEw7MoHKEQhYJqFPBYbBYjiB0qcspdcnQboVivwACKCJoAWQZTVqzzeDI+tRekmEwka326v10-26gxMcM8TGcoRGc0CNjMyg8yyCkzsqOUkWcnjMEJhRJJmXO2SulIKOFgAGsHgBXABGXFgAAsHjgiDg0GBUCQyrzStRpGzXu8vmofto-voxaZAvFxnZ9mFonY7NswarVuCLEwLPjQlCPGsJodUsSTsaLjkaObmJabQ6na73Z7vdkyu0AGKCcqM4Mcz6CzSRkXR0CDOM5ta6ya4jHbZR2TNx1O5iHTHEWQLgzyGitnKtm-LMDCN0guviHqj8CBUMAsSgAN3IVvvp8d5+dl9NVCHPRH-QxggSrWOioSOHCdhzCEMzLuCGoFqMkJmGsgRynuGQHj+NbHkw75Nt+5KUPwxBEAUpIAGaMGgeFnhelBQFelB-sKgHjkEcbStESKbGYaZFoEy6LlKcJePE4IzDYEwYaSJpEdcBQAMY4JQilgFwDGCJQDxkVARBwLALy2qQWAQDed4Ps+r5MMpqnqUZJkQCxAGiuxQyhA4UpmLiLhePYaEjMuFgFqibjOPmqbKNJZZGlh8m1kwtrYOQOAQGI7rmZQ96sFZ95JVgKVpe6zl9K5RimFETAljioI4mmdgjBYy7QhsepWE4DVQShMmVthCnMIp+l4HwDmmZl2VPi+96DWAZyjU54ZCi5Y7lcBDgTNKeITGm+LYutNjNRMrV4uii7gRM+w9XF1b9UwADueCKV+DHzdI5BQLA-CSLStLck8gjSL90itAA4iVUYAggjg5o4UxJutkzbEFDgakionbImMKeVdZI3QlD3IE9I3GaZb0ffwLz-YDtS0u0SiLcOpUrYMvlMJJowwgqiZpsumPSoWnjIrEMTBTjcl47hBNEy9JMQGTn2lP6gb1I0LTtODo6QyYeKoidaZxBd0JxMuUnWCjFipiMITeeBYtMbdUvPVAr3vQrlTVHUDTNG0HQM-+TNa3ENi5vEnjQ6m20o4dgS2GC6L1W4upmHbfUJRR3rS4x2HjZZU1MOnhPOkxGtsatUkorqmxWLKFuC4JCIIHDwcuAj60uKCiwp-FuEF5nTE5zlee90X2GKIEXSMxDQHBDsuZah5cbBHYAWZk3uYzJEDVxlJdhdxLVIYGRmDIPg7ocI6cBMAVqV8Iy55kAxp9EOfxSfbeWW59ZsW40eB9HxgJ8z44AvrAK+hVb730vEAkBCBB7KVHJ0EuZVBhhVRAFTwvFI5TFXrVJg3l9Qll1CEGwe9f7kX-oA5+wDX7UhKE0agYhajMiaC0YQIN6iSHKFIN4nsZBPGoE0JBzNEAEgiDuWUippgW28qvdaG0rBrB3iEKKhIYr7h-hSXCh9yDHyfi-S+dwaSK2EAGIMzDWHsPwJw7h0heHSH4YIv2rFkFBEklVUI0ipgNSsPsVeJZg5RCxmhWYkRQikM0VSYe5Q4DkFtEQNSb8LKD2sjAZA0TYCxPiXAIRkNixjDQpCDwgTtz4hNk4WwDgFTKgxHxLcYSiSUHIBAOABhv7izIUQCMAcgImC3MHGUcog4gQOg3VMYx7DREWDEaEkJorHEwhonCVJWDsE4DwPgXSp5uQlCFPpUIkTInsBmBucYPARGiFFSYJYPAFnCUsgohiwCbM1j0gs8jqlgjRKmbcy4PIbTxEnI6BYYIODubdesdoPwujdB6L0Pp4BLW6ds7cGow6eVlOta2YIRkrG3MiTUGIsQ4jxASMFCV8KfkItWZ5pdBgeSlM4GYwtrZOF8ScuM4QkTgX2ASZUaZ6nzNkvbBKtk1IaSgFpHS719KwEMrLGlLihhKgZSUuuQIwg4tcZVYSWp4hSW2GEMluF8qFXSp0xFWzVrDEcNKSY6JDYzxxA4Q64R1ptxBEcw5RqqQzWGjLRyCrhGrEWGzDxVgwjeXWpCHwJzoSuqOkCKEnlwQkLUQs9pESCiO2Jo5eWgbIZwg2nHeeSIrAesOv0o5BIwR6lxLvNNQrU49wzk7Ji+agJeE5QSfyTgUYwjMKvLUGwbl2FGAU7qDberdz-jogBejqEtItS8ty20Y6JkWDCFw0zjkrCxuEFCCxLBxDzF6yd10Ol4QofOkBYCb4MTvrKqBVCQHtrcrKDU66pIlgWJ5HdiAcRKjQQsD1wRtoCvLOm4VWir3QJoY819q0wioo6mjLY2JFg4MVHg6Y4FZzbn7d6goUSYlxISQhicHiEJxCVCMewiotSrwKbYXYwQPAoTxCkFIQA */ predictableActionArguments: true, id: "templateVersionEditor", schema: { @@ -68,7 +70,7 @@ export const templateVersionEditorMachine = createMachine( data: WorkspaceResource[] } publishingVersion: { - data: void + data: { isActiveVersion: boolean } } loadMissingVariables: { data: TemplateVersionVariable[] @@ -108,7 +110,7 @@ export const templateVersionEditorMachine = createMachine( publishingVersion: { tags: "loading", - entry: ["clearPublishingError"], + entry: ["clearPublishingError", "clearLastSuccessfulPublishedVersion"], invoke: { id: "publishingVersion", src: "publishingVersion", @@ -119,6 +121,7 @@ export const templateVersionEditorMachine = createMachine( }, onDone: { + actions: ["assignLastSuccessfulPublishedVersion"], target: ["idle"], }, }, @@ -256,6 +259,12 @@ export const templateVersionEditorMachine = createMachine( assignBuild: assign({ version: (_, event) => event.data, }), + assignLastSuccessfulPublishedVersion: assign({ + lastSuccessfulPublishedVersion: (ctx) => ctx.version, + lastSuccessfulPublishIsDefault: (_, event) => + event.data.isActiveVersion, + version: () => undefined, + }), addBuildLog: assign({ buildLogs: (context, event) => { const previousLogs = context.buildLogs ?? [] @@ -285,6 +294,9 @@ export const templateVersionEditorMachine = createMachine( publishingError: (_, event) => event.data, }), clearPublishingError: assign({ publishingError: (_) => undefined }), + clearLastSuccessfulPublishedVersion: assign({ + lastSuccessfulPublishedVersion: (_) => undefined, + }), assignMissingVariables: assign({ missingVariables: (_, event) => event.data, }), @@ -420,6 +432,8 @@ export const templateVersionEditorMachine = createMachine( }) : Promise.resolve(), ]) + + return { isActiveVersion } }, loadMissingVariables: ({ version }) => { if (!version) { From a2be2f983864ef166dd1925cdc380d42a2bb20a4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 28 Aug 2023 11:13:19 -0700 Subject: [PATCH 34/40] fix: avoid derp-map updates endpoint leak (#9390) --- coderd/workspaceagents.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index c127b2342d4a3..d921bbfa72bd7 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -878,13 +878,15 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { }) return } - nconn := websocket.NetConn(ctx, ws, websocket.MessageBinary) + ctx, nconn := websocketNetConn(ctx, ws, websocket.MessageBinary) defer nconn.Close() // Slurp all packets from the connection into io.Discard so pongs get sent - // by the websocket package. + // by the websocket package. We don't do any reads ourselves so this is + // necessary. go func() { _, _ = io.Copy(io.Discard, nconn) + _ = nconn.Close() }() go func(ctx context.Context) { @@ -899,13 +901,11 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { return } - // We don't need a context that times out here because the ping will - // eventually go through. If the context times out, then other - // websocket read operations will receive an error, obfuscating the - // actual problem. + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) err := ws.Ping(ctx) + cancel() if err != nil { - _ = ws.Close(websocket.StatusInternalError, err.Error()) + _ = nconn.Close() return } } @@ -920,7 +920,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { if lastDERPMap == nil || !tailnet.CompareDERPMaps(lastDERPMap, derpMap) { err := json.NewEncoder(nconn).Encode(derpMap) if err != nil { - _ = ws.Close(websocket.StatusInternalError, err.Error()) + _ = nconn.Close() return } lastDERPMap = derpMap From d138ed73148af4163ab65b33553493f4172f6a3f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 28 Aug 2023 15:14:17 -0300 Subject: [PATCH 35/40] fix(coderd): send updated workspace data adter ws connection (#9392) --- coderd/workspaces.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 707c85200488b..9384d2b7ecb00 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1028,6 +1028,9 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { _ = sendEvent(ctx, codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypePing, }) + // Send updated workspace info after connection is established. This avoids + // missing updates if the client connects after an update. + sendUpdate(ctx, nil) for { select { From 2167fe16d6278844fcae928dc4d70ad0af1d0ff0 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 28 Aug 2023 15:24:01 -0300 Subject: [PATCH 36/40] chore: remove e2e workaround (#9393) --- site/e2e/helpers.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 1cc442ce72c70..756c307ff59c9 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -39,17 +39,12 @@ export const createWorkspace = async ( await expect(page).toHaveURL("/@admin/" + name) - // FIXME: workaround for https://github.com/coder/coder/issues/8566 - const reloadTimer = setInterval(async () => { - await page.reload() - }, 3000) await page.waitForSelector( "span[data-testid='build-status'] >> text=Running", { state: "visible", }, ) - clearInterval(reloadTimer) return name } From fea8813f13e412e1aca6dfe89c0f178456d0ee0e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 28 Aug 2023 13:33:40 -0500 Subject: [PATCH 37/40] chore: drop 'template plan' unused command (#9386) --- cli/templateplan.go | 18 ------------------ cli/templates.go | 1 - cli/testdata/coder_templates_--help.golden | 1 - docs/cli/templates.md | 1 - docs/cli/templates_plan.md | 11 ----------- docs/manifest.json | 5 ----- 6 files changed, 37 deletions(-) delete mode 100644 cli/templateplan.go delete mode 100644 docs/cli/templates_plan.md diff --git a/cli/templateplan.go b/cli/templateplan.go deleted file mode 100644 index 76710a046d36a..0000000000000 --- a/cli/templateplan.go +++ /dev/null @@ -1,18 +0,0 @@ -package cli - -import ( - "github.com/coder/coder/v2/cli/clibase" -) - -func (*RootCmd) templatePlan() *clibase.Cmd { - return &clibase.Cmd{ - Use: "plan ", - Middleware: clibase.Chain( - clibase.RequireNArgs(1), - ), - Short: "Plan a template push from the current directory", - Handler: func(inv *clibase.Invocation) error { - return nil - }, - } -} diff --git a/cli/templates.go b/cli/templates.go index 7f5f16a4558c9..7ded6a7e5ee2b 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -37,7 +37,6 @@ func (r *RootCmd) templates() *clibase.Cmd { r.templateEdit(), r.templateInit(), r.templateList(), - r.templatePlan(), r.templatePush(), r.templateVersions(), r.templateDelete(), diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index 0bcc6c7978df7..352695e26fb57 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -24,7 +24,6 @@ Templates are written in standard Terraform and describe the infrastructure for edit Edit the metadata of a template by name. init Get started with a templated template. list List all the templates available for the organization - plan Plan a template push from the current directory pull Download the latest version of a template to a path. push Push a new template version from the current directory or as specified by flag diff --git a/docs/cli/templates.md b/docs/cli/templates.md index ed459f2d70d82..4426625363ed7 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -40,7 +40,6 @@ Templates are written in standard Terraform and describe the infrastructure for | [edit](./templates_edit.md) | Edit the metadata of a template by name. | | [init](./templates_init.md) | Get started with a templated template. | | [list](./templates_list.md) | List all the templates available for the organization | -| [plan](./templates_plan.md) | Plan a template push from the current directory | | [pull](./templates_pull.md) | Download the latest version of a template to a path. | | [push](./templates_push.md) | Push a new template version from the current directory or as specified by flag | | [versions](./templates_versions.md) | Manage different versions of the specified template | diff --git a/docs/cli/templates_plan.md b/docs/cli/templates_plan.md deleted file mode 100644 index 06ed7d56c507d..0000000000000 --- a/docs/cli/templates_plan.md +++ /dev/null @@ -1,11 +0,0 @@ - - -# templates plan - -Plan a template push from the current directory - -## Usage - -```console -coder templates plan -``` diff --git a/docs/manifest.json b/docs/manifest.json index 38c6103286346..8c18bb26ad9f5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -794,11 +794,6 @@ "description": "List all the templates available for the organization", "path": "cli/templates_list.md" }, - { - "title": "templates plan", - "description": "Plan a template push from the current directory", - "path": "cli/templates_plan.md" - }, { "title": "templates pull", "description": "Download the latest version of a template to a path.", From 487bdc2e08e75e4343bf66b7915421c902e2344b Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 28 Aug 2023 22:46:42 +0300 Subject: [PATCH 38/40] fix(coderd): allow `workspaceAgentLogs` follow to return on non-latest-build (#9382) --- coderd/workspaceagents.go | 25 +++++++++- coderd/workspaceagents_test.go | 85 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index d921bbfa72bd7..7a3b25eee96fc 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -23,6 +23,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" "golang.org/x/exp/maps" + "golang.org/x/exp/slices" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -481,6 +482,15 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { return } + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace by agent id.", + Detail: err.Error(), + }) + return + } + api.WebsocketWaitMutex.Lock() api.WebsocketWaitGroup.Add(1) api.WebsocketWaitMutex.Unlock() @@ -556,7 +566,8 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { go func() { defer close(bufferedLogs) - for { + keepGoing := true + for keepGoing { select { case <-ctx.Done(): return @@ -565,6 +576,18 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { t.Reset(recheckInterval) } + agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + if xerrors.Is(err, context.Canceled) { + return + } + logger.Warn(ctx, "failed to get workspace agents in latest build", slog.Error(err)) + continue + } + // If the agent is no longer in the latest build, we can stop after + // checking once. + keepGoing = slices.ContainsFunc(agents, func(agent database.WorkspaceAgent) bool { return agent.ID == workspaceAgent.ID }) + logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{ AgentID: workspaceAgent.ID, CreatedAfter: lastSentLogID, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 43694e95e67a9..2e51687afafa6 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -242,6 +242,91 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) { require.Equal(t, "testing", logChunk[0].Output) require.Equal(t, "testing2", logChunk[1].Output) }) + t.Run("Close logs on outdated build", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken) + err := agentClient.PatchLogs(ctx, agentsdk.PatchLogs{ + Logs: []agentsdk.Log{ + { + CreatedAt: database.Now(), + Output: "testing", + }, + }, + }) + require.NoError(t, err) + + logs, closer, err := client.WorkspaceAgentLogsAfter(ctx, build.Resources[0].Agents[0].ID, 0, true) + require.NoError(t, err) + defer func() { + _ = closer.Close() + }() + + first := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + assert.Fail(t, "context done while waiting in goroutine") + case <-logs: + close(first) + } + }() + select { + case <-ctx.Done(): + require.FailNow(t, "context done while waiting for first log") + case <-first: + } + + _ = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart) + + // Send a new log message to trigger a re-check. + err = agentClient.PatchLogs(ctx, agentsdk.PatchLogs{ + Logs: []agentsdk.Log{ + { + CreatedAt: database.Now(), + Output: "testing2", + }, + }, + }) + require.NoError(t, err) + + select { + case <-ctx.Done(): + require.FailNow(t, "context done while waiting for logs close") + case <-logs: + } + }) t.Run("PublishesOnOverflow", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) From be47cc58ffb504e630e6b6fef9b7b523f939d476 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 28 Aug 2023 23:12:45 +0300 Subject: [PATCH 39/40] fix(enterprise/coderd): use `websocketNetConn` in `workspaceProxyCoordinate` to bind context (#9395) --- enterprise/coderd/workspaceproxycoordinate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/workspaceproxycoordinate.go b/enterprise/coderd/workspaceproxycoordinate.go index 03e3f132e70f4..ec454d73a870a 100644 --- a/enterprise/coderd/workspaceproxycoordinate.go +++ b/enterprise/coderd/workspaceproxycoordinate.go @@ -68,7 +68,7 @@ func (api *API) workspaceProxyCoordinate(rw http.ResponseWriter, r *http.Request id := uuid.New() sub := (*api.AGPL.TailnetCoordinator.Load()).ServeMultiAgent(id) - nc := websocket.NetConn(ctx, conn, websocket.MessageText) + ctx, nc := websocketNetConn(ctx, conn, websocket.MessageText) defer nc.Close() err = tailnet.ServeWorkspaceProxy(ctx, nc, sub) From eb686843275605b4cc7153e9b418f7350098662b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 28 Aug 2023 17:55:09 -0500 Subject: [PATCH 40/40] docs: add v2.1.4 changelog (#9398) * docs: add v2.1.4 changelog * fmt * reorder * clarify --- docs/changelogs/README.md | 6 +++--- docs/changelogs/v2.1.4.md | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 docs/changelogs/v2.1.4.md diff --git a/docs/changelogs/README.md b/docs/changelogs/README.md index b0ffdbd4218b0..7e2493d7a3c3e 100644 --- a/docs/changelogs/README.md +++ b/docs/changelogs/README.md @@ -12,8 +12,8 @@ Run this command to generate release notes: export CODER_IGNORE_MISSING_COMMIT_METADATA=1 export BRANCH=main ./scripts/release/generate_release_notes.sh \ - --old-version=v2.1.3 \ - --new-version=v2.1.4 \ + --old-version=v2.1.4 \ + --new-version=v2.1.5 \ --ref=$(git rev-parse --short "${ref:-origin/$BRANCH}") \ - > ./docs/changelogs/v2.1.4.md + > ./docs/changelogs/v2.1.5.md ``` diff --git a/docs/changelogs/v2.1.4.md b/docs/changelogs/v2.1.4.md new file mode 100644 index 0000000000000..f2abe83d2fc10 --- /dev/null +++ b/docs/changelogs/v2.1.4.md @@ -0,0 +1,41 @@ +## Changelog + +### Features + +- Add `template_active_version_id` to workspaces (#9226) (@kylecarbs) +- Show entity name in DeleteDialog (#9347) (@ammario) +- Improve template publishing flow (#9346) (@aslilac) + +### Bug fixes + +- Fixed 2 bugs contributing to a memory leak in `coderd` (#9364): + - Allow `workspaceAgentLogs` follow to return on non-latest-build (#9382) + (@mafredri) + - Avoid derp-map updates endpoint leak (#9390) (@deansheather) +- Send updated workspace data after ws connection (#9392) (@BrunoQuaresma) +- Fix `coder template pull` on Windows (#9327) (@spikecurtis) +- Truncate websocket close error (#9360) (@kylecarbs) +- Add `--max-ttl`` to template create (#9319) (@ammario) +- Remove rate limits from agent metadata (#9308) (@ammario) +- Use `websocketNetConn` in `workspaceProxyCoordinate` to bind context (#9395) + (@mafredri) +- Fox default ephemeral parameter value on parameters page (#9314) + (@BrunoQuaresma) +- Render variable width unicode characters in terminal (#9259) (@ammario) +- Use WebGL renderer for terminal (#9320) (@ammario) +- 80425c32b fix(site): workaround: reload page every 3sec (#9387) (@mtojek) +- Make right panel scrollable on template editor (#9344) (@BrunoQuaresma) +- Use more reasonable restart limit for systemd service (#9355) (@bpmct) + +Compare: +[`v2.1.3...v2.1.4`](https://github.com/coder/coder/compare/v2.1.3...v2.1.4) + +## Container image + +- `docker pull ghcr.io/coder/coder:v2.1.4` + +## Install/upgrade + +Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or +[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a +release asset below.