mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: provide endpoint to lock/unlock workspace (#8239)
This commit is contained in:
+2
-1
@@ -51,6 +51,7 @@
|
||||
"autostart_schedule": "CRON_TZ=US/Central 30 9 * * 1-5",
|
||||
"ttl_ms": 28800000,
|
||||
"last_used_at": "[timestamp]",
|
||||
"deleting_at": null
|
||||
"deleting_at": null,
|
||||
"locked_at": null
|
||||
}
|
||||
]
|
||||
|
||||
Generated
+60
@@ -5558,6 +5558,53 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/lock": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Update workspace lock by id.",
|
||||
"operationId": "update-workspace-lock-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Lock or unlock a workspace",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateWorkspaceLock"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -9039,6 +9086,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceLock": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lock": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9196,6 +9251,11 @@ 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"
|
||||
},
|
||||
|
||||
Generated
+54
@@ -4901,6 +4901,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/lock": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Update workspace lock by id.",
|
||||
"operationId": "update-workspace-lock-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Lock or unlock a workspace",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateWorkspaceLock"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -8151,6 +8192,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceLock": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lock": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8288,6 +8337,11 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -735,6 +735,7 @@ func New(options *Options) *API {
|
||||
})
|
||||
r.Get("/watch", api.watchWorkspace)
|
||||
r.Put("/extend", api.putExtendWorkspace)
|
||||
r.Put("/lock", api.putWorkspaceLock)
|
||||
})
|
||||
})
|
||||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||
|
||||
@@ -143,13 +143,14 @@ var (
|
||||
DisplayName: "Provisioner Daemon",
|
||||
Site: rbac.Permissions(map[string][]rbac.Action{
|
||||
// TODO: Add ProvisionerJob resource type.
|
||||
rbac.ResourceFile.Type: {rbac.ActionRead},
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceUser.Type: {rbac.ActionRead},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceFile.Type: {rbac.ActionRead},
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceUser.Type: {rbac.ActionRead},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
@@ -165,9 +166,10 @@ var (
|
||||
Name: "autostart",
|
||||
DisplayName: "Autostart Daemon",
|
||||
Site: rbac.Permissions(map[string][]rbac.Action{
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
@@ -213,6 +215,7 @@ var (
|
||||
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspaceExecution.Type: {rbac.ActionCreate},
|
||||
rbac.ResourceWorkspaceProxy.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
}),
|
||||
@@ -1998,7 +2001,7 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
|
||||
action = rbac.ActionDelete
|
||||
}
|
||||
|
||||
if err = q.authorizeContext(ctx, action, w); err != nil {
|
||||
if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil {
|
||||
return database.WorkspaceBuild{}, err
|
||||
}
|
||||
|
||||
@@ -2530,6 +2533,13 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceLockedAt(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) (database.Workspace, error) {
|
||||
return q.db.GetWorkspaceByID(ctx, arg.ID)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)
|
||||
|
||||
@@ -1196,7 +1196,7 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
}).Asserts(w, rbac.ActionUpdate)
|
||||
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
@@ -1204,7 +1204,7 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionDelete,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
}).Asserts(w, rbac.ActionDelete)
|
||||
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionDelete), rbac.ActionDelete)
|
||||
}))
|
||||
s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
|
||||
@@ -5197,6 +5197,26 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateWorkspaceLockedAt(_ context.Context, arg database.UpdateWorkspaceLockedAtParams) 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.LockedAt = arg.LockedAt
|
||||
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()
|
||||
|
||||
@@ -1531,6 +1531,13 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas
|
||||
return err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateWorkspaceLockedAt(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateWorkspaceLockedAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedAt").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
start := time.Now()
|
||||
proxy, err := m.s.UpdateWorkspaceProxy(ctx, arg)
|
||||
|
||||
@@ -3165,6 +3165,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceLockedAt mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceLockedAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateWorkspaceLockedAt", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateWorkspaceLockedAt indicates an expected call of UpdateWorkspaceLockedAt.
|
||||
func (mr *MockStoreMockRecorder) UpdateWorkspaceLockedAt(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLockedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLockedAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceProxy mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceProxy(arg0 context.Context, arg1 database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+2
-1
@@ -816,7 +816,8 @@ CREATE TABLE workspaces (
|
||||
name character varying(64) NOT NULL,
|
||||
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
|
||||
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
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
ALTER TABLE workspaces DROP COLUMN locked_at;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
ALTER TABLE workspaces ADD COLUMN locked_at timestamptz NULL;
|
||||
COMMIT;
|
||||
@@ -145,6 +145,11 @@ 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()
|
||||
}
|
||||
|
||||
return rbac.ResourceWorkspaceExecution.
|
||||
WithID(w.ID).
|
||||
InOrg(w.OrganizationID).
|
||||
@@ -152,12 +157,40 @@ 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()
|
||||
}
|
||||
|
||||
return rbac.ResourceWorkspaceApplicationConnect.
|
||||
WithID(w.ID).
|
||||
InOrg(w.OrganizationID).
|
||||
WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
|
||||
// If a workspace is locked it cannot be built.
|
||||
// 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 &&
|
||||
transition != WorkspaceTransitionDelete {
|
||||
return w.LockedRBAC()
|
||||
}
|
||||
|
||||
return rbac.ResourceWorkspaceBuild.
|
||||
WithID(w.ID).
|
||||
InOrg(w.OrganizationID).
|
||||
WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (w Workspace) LockedRBAC() rbac.Object {
|
||||
return rbac.ResourceWorkspaceLocked.
|
||||
WithID(w.ID).
|
||||
InOrg(w.OrganizationID).
|
||||
WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (m OrganizationMember) RBACObject() rbac.Object {
|
||||
return rbac.ResourceOrganizationMember.
|
||||
WithID(m.UserID).
|
||||
|
||||
@@ -235,6 +235,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
&i.Count,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1687,6 +1687,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"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
|
||||
@@ -254,6 +254,7 @@ type sqlcQuerier interface {
|
||||
UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error)
|
||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
|
||||
UpdateWorkspaceLockedAt(ctx context.Context, arg UpdateWorkspaceLockedAtParams) 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
|
||||
|
||||
@@ -8147,7 +8147,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
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
@@ -8190,13 +8190,14 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
@@ -8220,13 +8221,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
@@ -8257,13 +8259,14 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
@@ -8313,13 +8316,14 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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, COUNT(*) OVER () as count
|
||||
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, COUNT(*) OVER () as count
|
||||
FROM
|
||||
workspaces
|
||||
JOIN
|
||||
@@ -8529,6 +8533,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"`
|
||||
Count int64 `db:"count" json:"count"`
|
||||
}
|
||||
|
||||
@@ -8565,6 +8570,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
&i.Count,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -8582,7 +8588,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.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
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN
|
||||
@@ -8651,6 +8657,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -8680,7 +8687,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
|
||||
($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
|
||||
`
|
||||
|
||||
type InsertWorkspaceParams struct {
|
||||
@@ -8722,6 +8729,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -8734,7 +8742,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
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
|
||||
`
|
||||
|
||||
type UpdateWorkspaceParams struct {
|
||||
@@ -8757,6 +8765,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -8818,6 +8827,25 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceLockedAt = `-- name: UpdateWorkspaceLockedAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
locked_at = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateWorkspaceLockedAtParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceLockedAt(ctx context.Context, arg UpdateWorkspaceLockedAtParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceLockedAt, arg.ID, arg.LockedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
|
||||
@@ -453,3 +453,11 @@ WHERE
|
||||
workspace_builds.transition = 'start'::workspace_transition
|
||||
)
|
||||
) AND workspaces.deleted = 'false';
|
||||
|
||||
-- name: UpdateWorkspaceLockedAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
locked_at = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
@@ -28,6 +28,21 @@ var (
|
||||
Type: "workspace",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceBuild refers to permissions necessary to
|
||||
// insert a workspace build job.
|
||||
// create/delete = ?
|
||||
// read = read workspace builds
|
||||
// update = insert/update workspace builds.
|
||||
ResourceWorkspaceBuild = Object{
|
||||
Type: "workspace_build",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceLocked is returned if a workspace is locked.
|
||||
// It grants restricted permissions on workspace builds.
|
||||
ResourceWorkspaceLocked = Object{
|
||||
Type: "workspace_locked",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceProxy CRUD. Org
|
||||
// create/delete = make or delete proxies
|
||||
// read = read proxy urls
|
||||
|
||||
@@ -25,7 +25,9 @@ func AllResources() []Object {
|
||||
ResourceWildcard,
|
||||
ResourceWorkspace,
|
||||
ResourceWorkspaceApplicationConnect,
|
||||
ResourceWorkspaceBuild,
|
||||
ResourceWorkspaceExecution,
|
||||
ResourceWorkspaceLocked,
|
||||
ResourceWorkspaceProxy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
opts = &RoleOptions{}
|
||||
}
|
||||
|
||||
var ownerAndAdminExceptions []Object
|
||||
ownerAndAdminExceptions := []Object{ResourceWorkspaceLocked}
|
||||
if opts.NoOwnerWorkspaceExec {
|
||||
ownerAndAdminExceptions = append(ownerAndAdminExceptions,
|
||||
ResourceWorkspaceExecution,
|
||||
@@ -152,7 +152,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceProvisionerDaemon.Type: {ActionRead},
|
||||
}),
|
||||
Org: map[string][]Permission{},
|
||||
User: allPermsExcept(),
|
||||
User: allPermsExcept(ResourceWorkspaceLocked),
|
||||
}.withCachedRegoValue()
|
||||
|
||||
auditorRole := Role{
|
||||
@@ -234,7 +234,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
Site: []Permission{},
|
||||
Org: map[string][]Permission{
|
||||
// Org admins should not have workspace exec perms.
|
||||
organizationID: allPermsExcept(ResourceWorkspaceExecution),
|
||||
organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceLocked),
|
||||
},
|
||||
User: []Permission{},
|
||||
}
|
||||
|
||||
@@ -318,6 +318,24 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceLocked",
|
||||
Actions: rbac.AllActions(),
|
||||
Resource: rbac.ResourceWorkspaceLocked.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {},
|
||||
false: {memberMe, orgAdmin, userAdmin, otherOrgAdmin, otherOrgMember, orgMemberMe, owner, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceBuild",
|
||||
Actions: rbac.AllActions(),
|
||||
Resource: rbac.ResourceWorkspaceBuild.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {userAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, memberMe},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
|
||||
@@ -350,6 +350,11 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
var buildErr wsbuilder.BuildError
|
||||
if xerrors.As(err, &buildErr) {
|
||||
var authErr dbauthz.NotAuthorizedError
|
||||
if xerrors.As(err, &authErr) {
|
||||
buildErr.Status = http.StatusUnauthorized
|
||||
}
|
||||
|
||||
if buildErr.Status == http.StatusInternalServerError {
|
||||
api.Logger.Error(ctx, "workspace build error", slog.Error(buildErr.Wrapped))
|
||||
}
|
||||
|
||||
+62
-4
@@ -454,10 +454,6 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
}, nil)
|
||||
var bldErr wsbuilder.BuildError
|
||||
if xerrors.As(err, &bldErr) {
|
||||
if bldErr.Status == http.StatusInternalServerError {
|
||||
api.Logger.Error(ctx, "workspace build error", slog.Error(bldErr.Wrapped))
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
|
||||
Message: bldErr.Message,
|
||||
Detail: bldErr.Error(),
|
||||
@@ -755,6 +751,61 @@ 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
|
||||
// @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"
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /workspaces/{workspace}/lock [put]
|
||||
func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
||||
var req codersdk.UpdateWorkspaceLock
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusOK
|
||||
resp := codersdk.Response{}
|
||||
|
||||
// If the workspace is already in the desired state do nothing!
|
||||
if workspace.LockedAt.Valid == req.Lock {
|
||||
httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{
|
||||
Message: "Nothing to do!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lockedAt := sql.NullTime{
|
||||
Valid: req.Lock,
|
||||
}
|
||||
if req.Lock {
|
||||
lockedAt.Time = database.Now()
|
||||
}
|
||||
|
||||
err := api.Database.UpdateWorkspaceLockedAt(ctx, database.UpdateWorkspaceLockedAtParams{
|
||||
ID: workspace.ID,
|
||||
LockedAt: lockedAt,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating workspace locked status.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO should we kick off a build to stop the workspace if it's started
|
||||
// from this endpoint? I'm leaning no to keep things simple and kick
|
||||
// the responsibility back to the client.
|
||||
httpapi.Write(ctx, rw, code, resp)
|
||||
}
|
||||
|
||||
// @Summary Extend workspace deadline by ID
|
||||
// @ID extend-workspace-deadline-by-id
|
||||
// @Security CoderSessionToken
|
||||
@@ -1054,10 +1105,16 @@ func convertWorkspace(
|
||||
autostartSchedule = &workspace.AutostartSchedule.String
|
||||
}
|
||||
|
||||
var lockedAt *time.Time
|
||||
if workspace.LockedAt.Valid {
|
||||
lockedAt = &workspace.LockedAt.Time
|
||||
}
|
||||
|
||||
var (
|
||||
ttlMillis = convertWorkspaceTTLMillis(workspace.Ttl)
|
||||
deletingAt = calculateDeletingAt(workspace, template, workspaceBuild)
|
||||
)
|
||||
|
||||
return codersdk.Workspace{
|
||||
ID: workspace.ID,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
@@ -1077,6 +1134,7 @@ func convertWorkspace(
|
||||
TTLMillis: ttlMillis,
|
||||
LastUsedAt: workspace.LastUsedAt,
|
||||
DeletingAt: deletingAt,
|
||||
LockedAt: lockedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2375,3 +2375,79 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
|
||||
}
|
||||
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
||||
}
|
||||
|
||||
func TestWorkspaceLock(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)
|
||||
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
|
||||
Lock: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
workspace, err = client.Workspace(ctx, workspace.ID)
|
||||
require.NoError(t, err, "fetch provisioned workspace")
|
||||
require.NotNil(t, workspace.LockedAt)
|
||||
require.WithinRange(t, *workspace.LockedAt, time.Now().Add(-time.Second*10), time.Now())
|
||||
|
||||
err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
|
||||
Lock: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
workspace, err = client.Workspace(ctx, workspace.ID)
|
||||
require.NoError(t, err, "fetch provisioned workspace")
|
||||
require.Nil(t, workspace.LockedAt)
|
||||
})
|
||||
|
||||
t.Run("CannotStart", 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)
|
||||
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
|
||||
Lock: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be able to stop a workspace while it is locked.
|
||||
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Should not be able to start a workspace while it is locked.
|
||||
_, 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,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ type Workspace struct {
|
||||
// DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.
|
||||
// Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.
|
||||
DeletingAt *time.Time `json:"deleting_at" format:"date-time"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type WorkspacesRequest struct {
|
||||
@@ -276,6 +281,25 @@ 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"`
|
||||
}
|
||||
|
||||
// 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())
|
||||
res, err := c.Request(ctx, http.MethodPut, path, req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace lock: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type WorkspaceFilter struct {
|
||||
// Owner can be "me" or a username
|
||||
Owner string `json:"owner,omitempty" typescript:"-"`
|
||||
|
||||
@@ -18,7 +18,7 @@ We track the following resources:
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>inactivity_ttl</td><td>true</td></tr><tr><td>locked_ttl</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>locked_at</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
|
||||
+37
-20
@@ -4299,6 +4299,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| ---------- | ------ | -------- | ------------ | ----------- |
|
||||
| `schedule` | string | false | | |
|
||||
|
||||
## codersdk.UpdateWorkspaceLock
|
||||
|
||||
```json
|
||||
{
|
||||
"lock": true
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------ | ------- | -------- | ------------ | ----------- |
|
||||
| `lock` | boolean | false | | |
|
||||
|
||||
## codersdk.UpdateWorkspaceRequest
|
||||
|
||||
```json
|
||||
@@ -4578,6 +4592,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"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,
|
||||
@@ -4595,26 +4610,27 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
|
||||
### 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. |
|
||||
| `id` | string | false | | |
|
||||
| `last_used_at` | string | false | | |
|
||||
| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `outdated` | boolean | false | | |
|
||||
| `owner_id` | string | false | | |
|
||||
| `owner_name` | string | false | | |
|
||||
| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
|
||||
| `template_display_name` | string | false | | |
|
||||
| `template_icon` | string | false | | |
|
||||
| `template_id` | string | false | | |
|
||||
| `template_name` | string | false | | |
|
||||
| `ttl_ms` | integer | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `autostart_schedule` | string | false | | |
|
||||
| `created_at` | string | false | | |
|
||||
| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. |
|
||||
| `id` | string | false | | |
|
||||
| `last_used_at` | string | false | | |
|
||||
| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
|
||||
| `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 | | |
|
||||
|
||||
## codersdk.WorkspaceAgent
|
||||
|
||||
@@ -5619,6 +5635,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"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,
|
||||
|
||||
@@ -174,6 +174,7 @@ 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,
|
||||
@@ -353,6 +354,7 @@ 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,
|
||||
@@ -552,6 +554,7 @@ 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,
|
||||
@@ -732,6 +735,7 @@ 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,
|
||||
@@ -881,6 +885,60 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update workspace lock by id.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PUT /workspaces/{workspace}/lock`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"lock": 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 |
|
||||
|
||||
### 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
|
||||
|
||||
@@ -117,6 +117,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"autostart_schedule": ActionTrack,
|
||||
"ttl": ActionTrack,
|
||||
"last_used_at": ActionIgnore,
|
||||
"locked_at": ActionTrack,
|
||||
},
|
||||
&database.WorkspaceBuild{}: {
|
||||
"id": ActionIgnore,
|
||||
|
||||
@@ -998,6 +998,11 @@ export interface UpdateWorkspaceAutostartRequest {
|
||||
readonly schedule?: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
export interface UpdateWorkspaceLock {
|
||||
readonly lock: boolean
|
||||
}
|
||||
|
||||
// From codersdk/workspaceproxy.go
|
||||
export interface UpdateWorkspaceProxyResponse {
|
||||
readonly proxy: WorkspaceProxy
|
||||
@@ -1075,6 +1080,7 @@ export interface Workspace {
|
||||
readonly ttl_ms?: number
|
||||
readonly last_used_at: string
|
||||
readonly deleting_at?: string
|
||||
readonly locked_at?: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
||||
Reference in New Issue
Block a user