Skip to content

Commit 236f7cf

Browse files
committed
Allow partial app status updates
Since we will be sending status updates without messages on an interval, we need to prevent duplicates and preserve the previous message. Also, I think this makes the endpoint more in alignment with PATCH semantics.
1 parent 67c1d92 commit 236f7cf

File tree

9 files changed

+173
-1
lines changed

9 files changed

+173
-1
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,6 +1991,13 @@ func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature datab
19911991
return q.db.GetLatestCryptoKeyByFeature(ctx, feature)
19921992
}
19931993

1994+
func (q *querier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) {
1995+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
1996+
return database.WorkspaceAppStatus{}, err
1997+
}
1998+
return q.db.GetLatestWorkspaceAppStatusByAppID(ctx, appID)
1999+
}
2000+
19942001
func (q *querier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) {
19952002
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
19962003
return nil, err

coderd/database/dbmem/dbmem.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3777,6 +3777,22 @@ func (q *FakeQuerier) GetLatestCryptoKeyByFeature(_ context.Context, feature dat
37773777
return latestKey, nil
37783778
}
37793779

3780+
func (q *FakeQuerier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) {
3781+
q.mutex.RLock()
3782+
defer q.mutex.RUnlock()
3783+
3784+
var current *database.WorkspaceAppStatus = nil
3785+
for _, appStatus := range q.workspaceAppStatuses {
3786+
if appStatus.AppID == appID && (current == nil || appStatus.CreatedAt.After(current.CreatedAt)) {
3787+
current = &appStatus
3788+
}
3789+
}
3790+
if current == nil {
3791+
return database.WorkspaceAppStatus{}, sql.ErrNoRows
3792+
}
3793+
return *current, nil
3794+
}
3795+
37803796
func (q *FakeQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) {
37813797
q.mutex.RLock()
37823798
defer q.mutex.RUnlock()

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaceapps.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@ SELECT DISTINCT ON (workspace_id)
5858
FROM workspace_app_statuses
5959
WHERE workspace_id = ANY(@ids :: uuid[])
6060
ORDER BY workspace_id, created_at DESC;
61+
62+
-- name: GetLatestWorkspaceAppStatusByAppID :one
63+
SELECT * FROM workspace_app_statuses WHERE app_id = $1
64+
ORDER BY created_at DESC LIMIT 1;

coderd/workspaceagents.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,40 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
380380
return
381381
}
382382

383+
// nolint:gocritic // This is a system restricted operation.
384+
status, err := api.Database.GetLatestWorkspaceAppStatusByAppID(dbauthz.AsSystemRestricted(ctx), app.ID)
385+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
386+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
387+
Message: "Internal error fetching workspace app statuses.",
388+
Detail: err.Error(),
389+
})
390+
return
391+
}
392+
// Preserve the existing message, which allows partial updates where we change
393+
// the status but keep the message.
394+
if req.Message == "" {
395+
req.Message = status.Message
396+
// Only preserve the URI if there was no message. If there is a message and
397+
// a blank URI, we assume the intent is to remove the URI, not preserve it.
398+
if req.URI == "" {
399+
req.URI = status.Uri.String
400+
}
401+
}
402+
// Skip duplicate status updates.
403+
newState := database.WorkspaceAppStatusState(req.State)
404+
if status.State == newState && status.Message == req.Message && status.Uri.String == req.URI {
405+
httpapi.Write(ctx, rw, http.StatusOK, nil)
406+
return
407+
}
408+
383409
// nolint:gocritic // This is a system restricted operation.
384410
_, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
385411
ID: uuid.New(),
386412
CreatedAt: dbtime.Now(),
387413
WorkspaceID: workspace.ID,
388414
AgentID: workspaceAgent.ID,
389415
AppID: app.ID,
390-
State: database.WorkspaceAppStatusState(req.State),
416+
State: newState,
391417
Message: req.Message,
392418
Uri: sql.NullString{
393419
String: req.URI,

coderd/workspaceagents_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,81 @@ func TestWorkspaceAgentAppStatus(t *testing.T) {
387387
require.False(t, agent.Apps[0].Statuses[0].NeedsUserAttention)
388388
})
389389

390+
t.Run("Patch", func(t *testing.T) {
391+
t.Parallel()
392+
ctx := testutil.Context(t, testutil.WaitShort)
393+
394+
ws := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
395+
OrganizationID: user.OrganizationID,
396+
OwnerID: user2.ID,
397+
}).WithAgent(func(a []*proto.Agent) []*proto.Agent {
398+
a[0].Apps = []*proto.App{
399+
{
400+
Slug: "vscode",
401+
},
402+
}
403+
return a
404+
}).Do()
405+
406+
agentClient := agentsdk.New(client.URL)
407+
agentClient.SetSessionToken(ws.AgentToken)
408+
409+
err := agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
410+
AppSlug: "vscode",
411+
Message: "testing",
412+
URI: "https://example.com",
413+
State: codersdk.WorkspaceAppStatusStateComplete,
414+
})
415+
require.NoError(t, err)
416+
417+
// Duplicate should be a no-op.
418+
err = agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
419+
AppSlug: "vscode",
420+
Message: "testing",
421+
URI: "https://example.com",
422+
State: codersdk.WorkspaceAppStatusStateComplete,
423+
})
424+
require.NoError(t, err)
425+
426+
// If no message, both the old message and URI are preserved, and this ends
427+
// up being a duplicate as well.
428+
err = agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
429+
AppSlug: "vscode",
430+
State: codersdk.WorkspaceAppStatusStateComplete,
431+
})
432+
require.NoError(t, err)
433+
434+
// Same, but the status updated so it will not be a duplicate.
435+
err = agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
436+
AppSlug: "vscode",
437+
State: codersdk.WorkspaceAppStatusStateWorking,
438+
})
439+
require.NoError(t, err)
440+
441+
// Message update, so nothing is preserved.
442+
err = agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
443+
AppSlug: "vscode",
444+
Message: "updated",
445+
State: codersdk.WorkspaceAppStatusStateWorking,
446+
})
447+
require.NoError(t, err)
448+
449+
workspace, err := client.Workspace(ctx, ws.Workspace.ID)
450+
require.NoError(t, err)
451+
agent, err := client.WorkspaceAgent(ctx, workspace.LatestBuild.Resources[0].Agents[0].ID)
452+
require.NoError(t, err)
453+
require.Len(t, agent.Apps[0].Statuses, 3)
454+
require.Equal(t, agent.Apps[0].Statuses[0].State, codersdk.WorkspaceAppStatusStateComplete)
455+
require.Equal(t, agent.Apps[0].Statuses[0].Message, "testing")
456+
require.Equal(t, agent.Apps[0].Statuses[0].URI, "https://example.com")
457+
require.Equal(t, agent.Apps[0].Statuses[1].State, codersdk.WorkspaceAppStatusStateWorking)
458+
require.Equal(t, agent.Apps[0].Statuses[1].Message, "testing")
459+
require.Equal(t, agent.Apps[0].Statuses[1].URI, "https://example.com")
460+
require.Equal(t, agent.Apps[0].Statuses[2].State, codersdk.WorkspaceAppStatusStateWorking)
461+
require.Equal(t, agent.Apps[0].Statuses[2].Message, "updated")
462+
require.Equal(t, agent.Apps[0].Statuses[2].URI, "")
463+
})
464+
390465
t.Run("FailUnknownApp", func(t *testing.T) {
391466
t.Parallel()
392467
ctx := testutil.Context(t, testutil.WaitShort)

0 commit comments

Comments
 (0)