chore: add workspace id filter on api (#12483)

* chore: add workspace id filter on api
This commit is contained in:
Steven Masley 2024-03-11 11:37:15 -05:00 committed by GitHub
parent 8f40ee3465
commit e3051dff0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 84 additions and 17 deletions

View File

@ -8266,6 +8266,19 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
}
}
if len(arg.WorkspaceIds) > 0 {
match := false
for _, id := range arg.WorkspaceIds {
if workspace.ID == id {
match = true
break
}
}
if !match {
continue
}
}
// If the filter exists, ensure the object is authorized.
if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil {
continue

View File

@ -221,6 +221,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
arg.OwnerUsername,
arg.TemplateName,
pq.Array(arg.TemplateIDs),
pq.Array(arg.WorkspaceIds),
arg.Name,
arg.HasAgent,
arg.AgentInactiveDisconnectTimeoutSeconds,

View File

@ -11989,16 +11989,22 @@ WHERE
workspaces.template_id = ANY($6)
ELSE true
END
-- Filter by workspace_ids
AND CASE
WHEN array_length($7 :: uuid[], 1) > 0 THEN
workspaces.id = ANY($7)
ELSE true
END
-- Filter by name, matching on substring
AND CASE
WHEN $7 :: text != '' THEN
workspaces.name ILIKE '%' || $7 || '%'
WHEN $8 :: text != '' THEN
workspaces.name ILIKE '%' || $8 || '%'
ELSE true
END
-- Filter by agent status
-- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents.
AND CASE
WHEN $8 :: text != '' THEN
WHEN $9 :: text != '' THEN
(
SELECT COUNT(*)
FROM
@ -12010,7 +12016,7 @@ WHERE
WHERE
workspace_resources.job_id = latest_build.provisioner_job_id AND
latest_build.transition = 'start'::workspace_transition AND
$8 = (
$9 = (
CASE
WHEN workspace_agents.first_connected_at IS NULL THEN
CASE
@ -12021,7 +12027,7 @@ WHERE
END
WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN
'disconnected'
WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $9 :: bigint THEN
WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $10 :: bigint THEN
'disconnected'
WHEN workspace_agents.last_connected_at IS NOT NULL THEN
'connected'
@ -12034,24 +12040,24 @@ WHERE
END
-- Filter by dormant workspaces.
AND CASE
WHEN $10 :: boolean != 'false' THEN
WHEN $11 :: boolean != 'false' THEN
dormant_at IS NOT NULL
ELSE true
END
-- Filter by last_used
AND CASE
WHEN $11 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at <= $11
WHEN $12 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at <= $12
ELSE true
END
AND CASE
WHEN $12 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at >= $12
WHEN $13 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at >= $13
ELSE true
END
AND CASE
WHEN $13 :: boolean IS NOT NULL THEN
(latest_build.template_version_id = template.active_version_id) = $13 :: boolean
WHEN $14 :: boolean IS NOT NULL THEN
(latest_build.template_version_id = template.active_version_id) = $14 :: boolean
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
@ -12063,7 +12069,7 @@ WHERE
filtered_workspaces fw
ORDER BY
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
CASE WHEN owner_id = $14 AND favorite THEN 0 ELSE 1 END ASC,
CASE WHEN owner_id = $15 AND favorite THEN 0 ELSE 1 END ASC,
(latest_build_completed_at IS NOT NULL AND
latest_build_canceled_at IS NULL AND
latest_build_error IS NULL AND
@ -12072,11 +12078,11 @@ WHERE
LOWER(name) ASC
LIMIT
CASE
WHEN $16 :: integer > 0 THEN
$16
WHEN $17 :: integer > 0 THEN
$17
END
OFFSET
$15
$16
), filtered_workspaces_order_with_summary AS (
SELECT
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.template_name, fwo.template_version_id, fwo.template_version_name, fwo.username, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition
@ -12111,7 +12117,7 @@ WHERE
'', -- latest_build_error
'start'::workspace_transition -- latest_build_transition
WHERE
$17 :: boolean = true
$18 :: boolean = true
), total_count AS (
SELECT
count(*) AS count
@ -12134,6 +12140,7 @@ type GetWorkspacesParams struct {
OwnerUsername string `db:"owner_username" json:"owner_username"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
WorkspaceIds []uuid.UUID `db:"workspace_ids" json:"workspace_ids"`
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"`
@ -12182,6 +12189,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
arg.OwnerUsername,
arg.TemplateName,
pq.Array(arg.TemplateIDs),
pq.Array(arg.WorkspaceIds),
arg.Name,
arg.HasAgent,
arg.AgentInactiveDisconnectTimeoutSeconds,

View File

@ -204,6 +204,12 @@ WHERE
workspaces.template_id = ANY(@template_ids)
ELSE true
END
-- Filter by workspace_ids
AND CASE
WHEN array_length(@workspace_ids :: uuid[], 1) > 0 THEN
workspaces.id = ANY(@workspace_ids)
ELSE true
END
-- Filter by name, matching on substring
AND CASE
WHEN @name :: text != '' THEN

View File

@ -103,6 +103,7 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
}
parser := httpapi.NewQueryParamParser()
filter.WorkspaceIds = parser.UUIDs(values, []uuid.UUID{}, "id")
filter.OwnerUsername = parser.String(values, "", "owner")
filter.TemplateName = parser.String(values, "", "template")
filter.Name = parser.String(values, "", "name")

View File

@ -178,6 +178,10 @@ func TestSearchWorkspace(t *testing.T) {
}
assert.Contains(t, s.String(), c.ExpectedErrorContains)
} else {
if len(c.Expected.WorkspaceIds) == len(values.WorkspaceIds) {
// nil slice vs 0 len slice is equivalent for our purposes.
c.Expected.WorkspaceIds = values.WorkspaceIds
}
assert.Len(t, errs, 0, "expected no error")
assert.Equal(t, c.Expected, values, "expected values")
}

View File

@ -9,6 +9,7 @@ import (
"math"
"net/http"
"os"
"slices"
"strings"
"testing"
"time"
@ -1356,6 +1357,39 @@ func TestWorkspaceFilterManual(t *testing.T) {
require.NoError(t, err)
require.Len(t, res.Workspaces, 0)
})
t.Run("IDs", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
alpha := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
bravo := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// full match
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
FilterQuery: fmt.Sprintf("id:%s,%s", alpha.ID, bravo.ID),
})
require.NoError(t, err)
require.Len(t, res.Workspaces, 2)
require.True(t, slices.ContainsFunc(res.Workspaces, func(workspace codersdk.Workspace) bool {
return workspace.ID == alpha.ID
}), "alpha workspace")
require.True(t, slices.ContainsFunc(res.Workspaces, func(workspace codersdk.Workspace) bool {
return workspace.ID == alpha.ID
}), "bravo workspace")
// no match
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
FilterQuery: fmt.Sprintf("id:%s", uuid.NewString()),
})
require.NoError(t, err)
require.Len(t, res.Workspaces, 0)
})
t.Run("Template", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})