Skip to content

Commit 688bb5e

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 f1c742e commit 688bb5e

File tree

11 files changed

+203
-1
lines changed

11 files changed

+203
-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/dbauthz/dbauthz_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,6 +3801,15 @@ func (s *MethodTestSuite) TestSystemFunctions() {
38013801
LoginType: database.LoginTypeGithub,
38023802
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(l)
38033803
}))
3804+
s.Run("GetLatestWorkspaceAppStatusByAppID", s.Subtest(func(db database.Store, check *expects) {
3805+
ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{})
3806+
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()})
3807+
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID})
3808+
agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
3809+
app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agent.ID})
3810+
status := dbgen.WorkspaceAppStatus(s.T(), db, database.WorkspaceAppStatus{WorkspaceID: ws.ID, AgentID: agent.ID, AppID: app.ID})
3811+
check.Args(app.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(status)
3812+
}))
38043813
s.Run("GetLatestWorkspaceAppStatusesByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) {
38053814
check.Args([]uuid.UUID{}).Asserts(rbac.ResourceSystem, policy.ActionRead)
38063815
}))

coderd/database/dbgen/dbgen.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,24 @@ func WorkspaceAppStat(t testing.TB, db database.Store, orig database.WorkspaceAp
799799
return scheme
800800
}
801801

802+
func WorkspaceAppStatus(t testing.TB, db database.Store, orig database.WorkspaceAppStatus) database.WorkspaceAppStatus {
803+
status, err := db.InsertWorkspaceAppStatus(genCtx, database.InsertWorkspaceAppStatusParams{
804+
ID: takeFirst(orig.ID, uuid.New()),
805+
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
806+
WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()),
807+
AgentID: takeFirst(orig.AgentID, uuid.New()),
808+
AppID: takeFirst(orig.AppID, uuid.New()),
809+
Uri: sql.NullString{
810+
String: takeFirst(orig.Uri.String, ""),
811+
Valid: takeFirst(orig.Uri.Valid, false),
812+
},
813+
Message: takeFirst(orig.Message, ""),
814+
State: takeFirst(orig.State, database.WorkspaceAppStatusStateWorking),
815+
})
816+
require.NoError(t, err, "insert workspace agent status")
817+
return status
818+
}
819+
802820
func WorkspaceResource(t testing.TB, db database.Store, orig database.WorkspaceResource) database.WorkspaceResource {
803821
resource, err := db.InsertWorkspaceResource(genCtx, database.InsertWorkspaceResourceParams{
804822
ID: takeFirst(orig.ID, uuid.New()),

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: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,43 @@ 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+
// TODO: What if another status is added while we are doing these checks?
395+
// This would essentially revert the message to the previous one. We could
396+
// maybe do this in the query instead?
397+
if req.Message == "" {
398+
req.Message = status.Message
399+
// Only preserve the URI if there was no message. If there is a message and
400+
// a blank URI, we assume the intent is to remove the URI, not preserve it.
401+
if req.URI == "" {
402+
req.URI = status.Uri.String
403+
}
404+
}
405+
// Skip duplicate status updates.
406+
newState := database.WorkspaceAppStatusState(req.State)
407+
if status.State == newState && status.Message == req.Message && status.Uri.String == req.URI {
408+
httpapi.Write(ctx, rw, http.StatusOK, nil)
409+
return
410+
}
411+
383412
// nolint:gocritic // This is a system restricted operation.
384413
_, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
385414
ID: uuid.New(),
386415
CreatedAt: dbtime.Now(),
387416
WorkspaceID: workspace.ID,
388417
AgentID: workspaceAgent.ID,
389418
AppID: app.ID,
390-
State: database.WorkspaceAppStatusState(req.State),
419+
State: newState,
391420
Message: req.Message,
392421
Uri: sql.NullString{
393422
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy