mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: include agent metadata in workspace list responses (#27934)
Closes #27933. Related: #27897 (single-agent GET). Agent metadata is only readable via a per-agent watch stream, so reading it across N workspaces costs N+1 requests. This adds a batch read to the list endpoint: ```text GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status ``` - New `include_agent_metadata` search key, repeatable and key-scoped. It expands the response, it does not filter workspaces. - `GetWorkspaces` aggregates the requested keys as JSON behind a `CASE`: without opt-in the response is unchanged and the subquery never runs. Runs only for the returned page, inside the same authorized query. - Agents in the response gain `metadata` (`[]codersdk.WorkspaceAgentMetadata`, `omitempty`), mapped by the `workspace_agent_id` each element carries. The collection script is omitted; it can be long. - `codersdk.WorkspaceFilter` gains `IncludeAgentMetadata []string`. - No wildcard, no schema change, no migration. --- Authored by Coder Agents on behalf of @Emyrk.
This commit is contained in:
@@ -2869,6 +2869,88 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||
})
|
||||
|
||||
t.Run("IncludeAgentMetadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
build := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).WithAgent().Do()
|
||||
require.Len(t, build.Agents, 1)
|
||||
agentID := build.Agents[0].ID
|
||||
|
||||
//nolint:gocritic // This is a test; only the agent API writes metadata.
|
||||
ctx := dbauthz.AsSystemRestricted(context.Background())
|
||||
collectedAt := dbtime.Now()
|
||||
// Task_Status is mixed-case on purpose: requested keys are
|
||||
// lowercased by the search parser, and the query matches stored
|
||||
// keys case-insensitively.
|
||||
for i, key := range []string{"Task_Status", "cpu", "unrequested"} {
|
||||
err := db.InsertWorkspaceAgentMetadata(ctx, database.InsertWorkspaceAgentMetadataParams{
|
||||
WorkspaceAgentID: agentID,
|
||||
DisplayName: key,
|
||||
Key: key,
|
||||
Script: "echo",
|
||||
Timeout: int64(time.Second),
|
||||
Interval: int64(time.Second),
|
||||
// Reversed so the response order proves display_order
|
||||
// sorting rather than insertion order.
|
||||
DisplayOrder: int32(3 - i), //nolint:gosec // Tiny test constant.
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = db.UpdateWorkspaceAgentMetadata(ctx, database.UpdateWorkspaceAgentMetadataParams{
|
||||
WorkspaceAgentID: agentID,
|
||||
Key: []string{key},
|
||||
Value: []string{"value-" + key},
|
||||
Error: []string{""},
|
||||
CollectedAt: []time.Time{collectedAt},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
findAgent := func(res codersdk.WorkspacesResponse) codersdk.WorkspaceAgent {
|
||||
require.Len(t, res.Workspaces, 1)
|
||||
require.Len(t, res.Workspaces[0].LatestBuild.Resources, 1)
|
||||
require.Len(t, res.Workspaces[0].LatestBuild.Resources[0].Agents, 1)
|
||||
return res.Workspaces[0].LatestBuild.Resources[0].Agents[0]
|
||||
}
|
||||
|
||||
// Without the opt-in the response carries no metadata.
|
||||
res, err := client.Workspaces(reqCtx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, findAgent(res).Metadata)
|
||||
|
||||
// Opting in returns exactly the requested keys, ordered by
|
||||
// display_order, with their collected values.
|
||||
res, err = client.Workspaces(reqCtx, codersdk.WorkspaceFilter{
|
||||
IncludeAgentMetadata: []string{"task_status", "cpu"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
metadata := findAgent(res).Metadata
|
||||
require.Len(t, metadata, 2)
|
||||
require.Equal(t, "cpu", metadata[0].Description.Key)
|
||||
require.Equal(t, "value-cpu", metadata[0].Result.Value)
|
||||
// The collection script is deliberately not exposed on the list
|
||||
// endpoint; it can be long.
|
||||
require.Empty(t, metadata[0].Description.Script)
|
||||
require.Equal(t, "Task_Status", metadata[1].Description.Key)
|
||||
require.Equal(t, "value-Task_Status", metadata[1].Result.Value)
|
||||
require.WithinDuration(t, collectedAt, metadata[1].Result.CollectedAt, time.Second)
|
||||
|
||||
// Unknown keys are not an error; the metadata is just absent.
|
||||
res, err = client.Workspaces(reqCtx, codersdk.WorkspaceFilter{
|
||||
IncludeAgentMetadata: []string{"no_such_key"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, findAgent(res).Metadata)
|
||||
})
|
||||
|
||||
t.Run("HealthyFilter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user