mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add frontend for locked workspaces (#8655)
- Fix workspaces query for locked workspaces.
This commit is contained in:
Generated
+1
-1
@@ -6097,7 +6097,7 @@ const docTemplate = `{
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
"$ref": "#/definitions/codersdk.Workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -5379,7 +5379,7 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
"$ref": "#/definitions/codersdk.Workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
// Lock the workspace if it has breached the template's
|
||||
// threshold for inactivity.
|
||||
if reason == database.BuildReasonAutolock {
|
||||
err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{
|
||||
ws, err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{
|
||||
ID: ws.ID,
|
||||
LockedAt: sql.NullTime{
|
||||
Time: database.Now(),
|
||||
|
||||
@@ -2587,11 +2587,11 @@ 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) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
|
||||
func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) {
|
||||
return q.db.GetWorkspaceByID(ctx, arg.ID)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg)
|
||||
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
|
||||
@@ -5250,9 +5250,9 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
|
||||
func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
return database.Workspace{}, err
|
||||
}
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
@@ -5274,7 +5274,7 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat
|
||||
}
|
||||
}
|
||||
if template.ID == uuid.Nil {
|
||||
return xerrors.Errorf("unable to find workspace template")
|
||||
return database.Workspace{}, xerrors.Errorf("unable to find workspace template")
|
||||
}
|
||||
if template.LockedTTL > 0 {
|
||||
workspace.DeletingAt = sql.NullTime{
|
||||
@@ -5284,9 +5284,9 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat
|
||||
}
|
||||
}
|
||||
q.workspaces[index] = workspace
|
||||
return nil
|
||||
return workspace, nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
@@ -5730,6 +5730,16 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
}
|
||||
}
|
||||
|
||||
// We omit locked workspaces by default.
|
||||
if arg.LockedAt.IsZero() && workspace.LockedAt.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter out workspaces that are locked after the timestamp.
|
||||
if !arg.LockedAt.IsZero() && workspace.LockedAt.Time.Before(arg.LockedAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(arg.TemplateIDs) > 0 {
|
||||
match := false
|
||||
for _, id := range arg.TemplateIDs {
|
||||
|
||||
@@ -1572,11 +1572,11 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas
|
||||
return err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
|
||||
func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg)
|
||||
ws, r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedDeletingAt").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
return ws, r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
|
||||
@@ -3307,11 +3307,12 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{
|
||||
}
|
||||
|
||||
// UpdateWorkspaceLockedDeletingAt mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) error {
|
||||
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].(error)
|
||||
return ret0
|
||||
ret0, _ := ret[0].(database.Workspace)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateWorkspaceLockedDeletingAt indicates an expected call of UpdateWorkspaceLockedDeletingAt.
|
||||
|
||||
@@ -217,6 +217,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
arg.Name,
|
||||
arg.HasAgent,
|
||||
arg.AgentInactiveDisconnectTimeoutSeconds,
|
||||
arg.LockedAt,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
|
||||
@@ -277,7 +277,7 @@ type sqlcQuerier interface {
|
||||
UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error
|
||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
|
||||
UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) 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
|
||||
|
||||
@@ -9001,6 +9001,14 @@ WHERE
|
||||
) > 0
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by locked workspaces. By default we do not return locked
|
||||
-- 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
|
||||
ELSE
|
||||
locked_at IS NULL
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
@@ -9012,11 +9020,11 @@ ORDER BY
|
||||
LOWER(workspaces.name) ASC
|
||||
LIMIT
|
||||
CASE
|
||||
WHEN $11 :: integer > 0 THEN
|
||||
$11
|
||||
WHEN $12 :: integer > 0 THEN
|
||||
$12
|
||||
END
|
||||
OFFSET
|
||||
$10
|
||||
$11
|
||||
`
|
||||
|
||||
type GetWorkspacesParams struct {
|
||||
@@ -9029,6 +9037,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"`
|
||||
Offset int32 `db:"offset_" json:"offset_"`
|
||||
Limit int32 `db:"limit_" json:"limit_"`
|
||||
}
|
||||
@@ -9064,6 +9073,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
arg.Name,
|
||||
arg.HasAgent,
|
||||
arg.AgentInactiveDisconnectTimeoutSeconds,
|
||||
arg.LockedAt,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
@@ -9366,7 +9376,7 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :exec
|
||||
const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :one
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
@@ -9383,6 +9393,7 @@ 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
|
||||
`
|
||||
|
||||
type UpdateWorkspaceLockedDeletingAtParams struct {
|
||||
@@ -9390,9 +9401,25 @@ type UpdateWorkspaceLockedDeletingAtParams struct {
|
||||
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt)
|
||||
return err
|
||||
func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt)
|
||||
var i Workspace
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.LockedAt,
|
||||
&i.DeletingAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
|
||||
|
||||
@@ -259,6 +259,14 @@ WHERE
|
||||
) > 0
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by locked workspaces. By default we do not return locked
|
||||
-- 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
|
||||
ELSE
|
||||
locked_at IS NULL
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
@@ -474,7 +482,7 @@ WHERE
|
||||
)
|
||||
) AND workspaces.deleted = 'false';
|
||||
|
||||
-- name: UpdateWorkspaceLockedDeletingAt :exec
|
||||
-- name: UpdateWorkspaceLockedDeletingAt :one
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
@@ -490,7 +498,8 @@ FROM
|
||||
WHERE
|
||||
workspaces.template_id = templates.id
|
||||
AND
|
||||
workspaces.id = $1;
|
||||
workspaces.id = $1
|
||||
RETURNING workspaces.*;
|
||||
|
||||
-- name: UpdateWorkspacesDeletingAtByTemplateID :exec
|
||||
UPDATE
|
||||
|
||||
@@ -114,9 +114,15 @@ 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")
|
||||
|
||||
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
|
||||
// are omitted by default.
|
||||
if filter.LockedAt.IsZero() {
|
||||
filter.LockedAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
parser.ErrorExcessParams(values)
|
||||
|
||||
+17
-9
@@ -768,7 +768,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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
|
||||
// @Success 200 {object} codersdk.Workspace
|
||||
// @Router /workspaces/{workspace}/lock [put]
|
||||
func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@@ -779,9 +779,6 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
|
||||
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{
|
||||
@@ -797,7 +794,7 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
|
||||
lockedAt.Time = database.Now()
|
||||
}
|
||||
|
||||
err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{
|
||||
workspace, err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{
|
||||
ID: workspace.ID,
|
||||
LockedAt: lockedAt,
|
||||
})
|
||||
@@ -809,10 +806,21 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace resources.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
|
||||
workspace,
|
||||
data.builds[0],
|
||||
data.templates[0],
|
||||
findUser(workspace.OwnerID, data.users),
|
||||
))
|
||||
}
|
||||
|
||||
// @Summary Extend workspace deadline by ID
|
||||
|
||||
@@ -1407,6 +1407,46 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
// and template.InactivityTTL should be 0
|
||||
assert.Len(t, res.Workspaces, 0)
|
||||
})
|
||||
|
||||
t.Run("LockedAt", 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{
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
// update template with inactivity ttl
|
||||
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)
|
||||
|
||||
// Create another workspace to validate that we do not return unlocked workspaces.
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID)
|
||||
|
||||
err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{
|
||||
Lock: 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")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
require.NotNil(t, res.Workspaces[0].LockedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOffsetLimit(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user