Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/agentapi/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (a *AppsAPI) UpdateAppStatus(ctx context.Context, req *agentproto.UpdateApp
// We pass time.Time{} for nextAutostart since we don't have access to
// TemplateScheduleStore here. The activity bump logic handles this by
// defaulting to the template's activity_bump duration (typically 1 hour).
workspacestats.ActivityBumpWorkspace(ctx, a.Log, a.Database, ws.ID, time.Time{}, workspacestats.ActivityBumpReasonAppActivity)
workspacestats.ActivityBumpWorkspace(ctx, a.Log, a.Database, ws.ID, time.Time{}, workspacestats.ActivityBumpReasonApp(app.Slug))
}
// just return a blank response because it doesn't contain any settable fields at present.
return new(agentproto.UpdateAppStatusResponse), nil
Expand Down
53 changes: 53 additions & 0 deletions coderd/agentapi/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,59 @@ func TestWorkspaceAgentAppStatus(t *testing.T) {
require.Len(t, sent, 1)
})

t.Run("BumpsActivityWithAppSlugAsSource", func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitShort)
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
agent := database.WorkspaceAgent{
ID: uuid.UUID{2},
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
}

// No TaskID, so enqueueAITaskStateNotification short-circuits and
// no extra notification-related mocks are required.
workspace := database.Workspace{
ID: uuid.UUID{9},
}
cachedWs := &agentapi.CachedWorkspaceFields{}
cachedWs.UpdateValues(workspace)

api := &agentapi.AppsAPI{
AgentID: agent.ID,
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
return agent, nil
},
Database: mDB,
Log: testutil.Logger(t),
Workspace: cachedWs,
}

app := database.WorkspaceApp{
ID: uuid.UUID{8},
Slug: "my-custom-app",
}
mDB.EXPECT().GetWorkspaceAppByAgentIDAndSlug(gomock.Any(), database.GetWorkspaceAppByAgentIDAndSlugParams{
AgentID: agent.ID,
Slug: "my-custom-app",
}).Times(1).Return(app, nil)
// Zero-value previous status: ID == uuid.Nil, so shouldBump only
// triggers via the new state being Working.
mDB.EXPECT().GetLatestWorkspaceAppStatusByAppID(gomock.Any(), app.ID).Times(1).Return(database.WorkspaceAppStatus{}, nil)
mDB.EXPECT().InsertWorkspaceAppStatus(gomock.Any(), gomock.Any()).Times(1).Return(database.WorkspaceAppStatus{}, nil)
mDB.EXPECT().ActivityBumpWorkspace(gomock.Any(), gomock.Cond(func(arg database.ActivityBumpWorkspaceParams) bool {
return arg.WorkspaceID == workspace.ID && arg.Source == "app:my-custom-app"
})).Times(1).Return(nil)

_, err := api.UpdateAppStatus(ctx, &agentproto.UpdateAppStatusRequest{
Slug: "my-custom-app",
Message: "testing",
State: agentproto.UpdateAppStatusRequest_WORKING,
})
require.NoError(t, err)
})

t.Run("FailUnknownApp", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
Expand Down
4 changes: 4 additions & 0 deletions coderd/agentapi/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func TestUpdateStats(t *testing.T) {
dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{
WorkspaceID: workspace.ID,
NextAutostart: time.Time{}.UTC(),
Source: "ssh",
}).Return(nil)

// Workspace last used at gets bumped.
Expand Down Expand Up @@ -377,6 +378,7 @@ func TestUpdateStats(t *testing.T) {
dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{
WorkspaceID: workspace.ID,
NextAutostart: nextAutostart,
Source: "ssh",
}).Return(nil)

// Workspace last used at gets bumped.
Expand Down Expand Up @@ -489,6 +491,7 @@ func TestUpdateStats(t *testing.T) {
dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{
WorkspaceID: workspace.ID,
NextAutostart: time.Time{}.UTC(),
Source: "ssh",
}).Return(nil)

// Workspace last used at gets bumped.
Expand Down Expand Up @@ -624,6 +627,7 @@ func TestUpdateStats(t *testing.T) {
dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{
WorkspaceID: workspace.ID,
NextAutostart: time.Time{}.UTC(),
Source: "ssh",
}).Return(nil)

// Workspace last used at gets bumped.
Expand Down
9 changes: 9 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions coderd/database/migrations/000567_workspace_last_activity.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
DROP VIEW workspaces_expanded;

CREATE VIEW workspaces_expanded AS
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.dormant_at,
workspaces.deleting_at,
workspaces.automatic_updates,
workspaces.favorite,
workspaces.next_start_at,
workspaces.group_acl,
workspaces.user_acl,
visible_users.avatar_url AS owner_avatar_url,
visible_users.username AS owner_username,
visible_users.name AS owner_name,
organizations.name AS organization_name,
organizations.display_name AS organization_display_name,
organizations.icon AS organization_icon,
organizations.description AS organization_description,
templates.name AS template_name,
templates.display_name AS template_display_name,
templates.icon AS template_icon,
templates.description AS template_description,
tasks.id AS task_id,
COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(g.name, ''::text), 'avatar_url', COALESCE(g.avatar_url, ''::text))) AS jsonb_object_agg
FROM (jsonb_each(workspaces.group_acl) acl(key, value)
LEFT JOIN groups g ON ((g.id = (acl.key)::uuid)))), '{}'::jsonb) AS group_acl_display_info,
COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(vu.name, ''::text), 'avatar_url', COALESCE(vu.avatar_url, ''::text))) AS jsonb_object_agg
FROM (jsonb_each(workspaces.user_acl) acl(key, value)
LEFT JOIN visible_users vu ON ((vu.id = (acl.key)::uuid)))), '{}'::jsonb) AS user_acl_display_info
FROM ((((workspaces
JOIN visible_users ON ((workspaces.owner_id = visible_users.id)))
JOIN organizations ON ((workspaces.organization_id = organizations.id)))
JOIN templates ON ((workspaces.template_id = templates.id)))
LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id)));

COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.';

ALTER TABLE ONLY workspaces DROP COLUMN IF EXISTS last_activity_at;
ALTER TABLE ONLY workspaces DROP COLUMN IF EXISTS last_activity_source;
54 changes: 54 additions & 0 deletions coderd/database/migrations/000567_workspace_last_activity.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
ALTER TABLE ONLY workspaces ADD COLUMN IF NOT EXISTS last_activity_source text;
ALTER TABLE ONLY workspaces ADD COLUMN IF NOT EXISTS last_activity_at timestamp with time zone;

COMMENT ON COLUMN workspaces.last_activity_source IS 'Source of the last activity that bumped the workspace deadline (e.g. ssh, vscode, jetbrains, reconnecting_pty, app:<slug>, chat_heartbeat). NULL if the workspace has never had its deadline bumped by activity.';
COMMENT ON COLUMN workspaces.last_activity_at IS 'Timestamp of the last activity that bumped the workspace deadline. Distinct from last_used_at, which is updated by a broader, unrelated app/port-forward-traffic code path.';

DROP VIEW workspaces_expanded;

CREATE VIEW workspaces_expanded AS
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.dormant_at,
workspaces.deleting_at,
workspaces.automatic_updates,
workspaces.favorite,
workspaces.next_start_at,
workspaces.group_acl,
workspaces.user_acl,
visible_users.avatar_url AS owner_avatar_url,
visible_users.username AS owner_username,
visible_users.name AS owner_name,
organizations.name AS organization_name,
organizations.display_name AS organization_display_name,
organizations.icon AS organization_icon,
organizations.description AS organization_description,
templates.name AS template_name,
templates.display_name AS template_display_name,
templates.icon AS template_icon,
templates.description AS template_description,
tasks.id AS task_id,
COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(g.name, ''::text), 'avatar_url', COALESCE(g.avatar_url, ''::text))) AS jsonb_object_agg
FROM (jsonb_each(workspaces.group_acl) acl(key, value)
LEFT JOIN groups g ON ((g.id = (acl.key)::uuid)))), '{}'::jsonb) AS group_acl_display_info,
COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(vu.name, ''::text), 'avatar_url', COALESCE(vu.avatar_url, ''::text))) AS jsonb_object_agg
FROM (jsonb_each(workspaces.user_acl) acl(key, value)
LEFT JOIN visible_users vu ON ((vu.id = (acl.key)::uuid)))), '{}'::jsonb) AS user_acl_display_info,
workspaces.last_activity_source,
workspaces.last_activity_at
FROM ((((workspaces
JOIN visible_users ON ((workspaces.owner_id = visible_users.id)))
JOIN organizations ON ((workspaces.organization_id = organizations.id)))
JOIN templates ON ((workspaces.template_id = templates.id)))
LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id)));

COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.';
2 changes: 2 additions & 0 deletions coderd/database/modelqueries.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
&i.LastActivitySource,
&i.LastActivityAt,
&i.TemplateVersionID,
&i.TemplateVersionName,
&i.LatestBuildCompletedAt,
Expand Down
6 changes: 6 additions & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading