mirror of https://github.com/coder/coder.git
chore: allow search by build params in workspace search filter (#12694)
* chore: workspace search filter allow search by params * has_param will return all workspaces with the param existance * exact matching
This commit is contained in:
parent
b4fd819f0d
commit
c674128105
|
@ -8739,6 +8739,55 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(arg.HasParam) > 0 || len(arg.ParamNames) > 0 {
|
||||||
|
build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("get latest build: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]database.WorkspaceBuildParameter, 0)
|
||||||
|
for _, param := range q.workspaceBuildParameters {
|
||||||
|
if param.WorkspaceBuildID != build.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params = append(params, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerErr error
|
||||||
|
index := slices.IndexFunc(params, func(buildParam database.WorkspaceBuildParameter) bool {
|
||||||
|
// If hasParam matches, then we are done. This is a good match.
|
||||||
|
if slices.ContainsFunc(arg.HasParam, func(name string) bool {
|
||||||
|
return strings.EqualFold(buildParam.Name, name)
|
||||||
|
}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check name + value
|
||||||
|
match := false
|
||||||
|
for i := range arg.ParamNames {
|
||||||
|
matchName := arg.ParamNames[i]
|
||||||
|
if !strings.EqualFold(matchName, buildParam.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matchValue := arg.ParamValues[i]
|
||||||
|
if !strings.EqualFold(matchValue, buildParam.Value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
if innerErr != nil {
|
||||||
|
return nil, xerrors.Errorf("error searching workspace build params: %w", innerErr)
|
||||||
|
}
|
||||||
|
if index < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if arg.OwnerUsername != "" {
|
if arg.OwnerUsername != "" {
|
||||||
owner, err := q.getUserByIDNoLock(workspace.OwnerID)
|
owner, err := q.getUserByIDNoLock(workspace.OwnerID)
|
||||||
if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) {
|
if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) {
|
||||||
|
|
|
@ -213,9 +213,12 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||||
// The name comment is for metric tracking
|
// The name comment is for metric tracking
|
||||||
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filtered)
|
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filtered)
|
||||||
rows, err := q.db.QueryContext(ctx, query,
|
rows, err := q.db.QueryContext(ctx, query,
|
||||||
|
pq.Array(arg.ParamNames),
|
||||||
|
pq.Array(arg.ParamValues),
|
||||||
arg.Deleted,
|
arg.Deleted,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.OwnerID,
|
arg.OwnerID,
|
||||||
|
pq.Array(arg.HasParam),
|
||||||
arg.OwnerUsername,
|
arg.OwnerUsername,
|
||||||
arg.TemplateName,
|
arg.TemplateName,
|
||||||
pq.Array(arg.TemplateIDs),
|
pq.Array(arg.TemplateIDs),
|
||||||
|
|
|
@ -277,6 +277,9 @@ type sqlcQuerier interface {
|
||||||
GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error)
|
GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error)
|
||||||
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
|
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
|
||||||
GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error)
|
GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error)
|
||||||
|
// build_params is used to filter by build parameters if present.
|
||||||
|
// It has to be a CTE because the set returning function 'unnest' cannot
|
||||||
|
// be used in a WHERE clause.
|
||||||
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error)
|
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error)
|
||||||
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error)
|
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error)
|
||||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||||
|
|
|
@ -12162,7 +12162,13 @@ func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Conte
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaces = `-- name: GetWorkspaces :many
|
const getWorkspaces = `-- name: GetWorkspaces :many
|
||||||
WITH filtered_workspaces AS (
|
WITH
|
||||||
|
build_params AS (
|
||||||
|
SELECT
|
||||||
|
LOWER(unnest($1 :: text[])) AS name,
|
||||||
|
LOWER(unnest($2 :: text[])) AS value
|
||||||
|
),
|
||||||
|
filtered_workspaces AS (
|
||||||
SELECT
|
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.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,
|
||||||
COALESCE(template.name, 'unknown') as template_name,
|
COALESCE(template.name, 'unknown') as template_name,
|
||||||
|
@ -12181,6 +12187,7 @@ ON
|
||||||
workspaces.owner_id = users.id
|
workspaces.owner_id = users.id
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT
|
SELECT
|
||||||
|
workspace_builds.id,
|
||||||
workspace_builds.transition,
|
workspace_builds.transition,
|
||||||
workspace_builds.template_version_id,
|
workspace_builds.template_version_id,
|
||||||
template_versions.name AS template_version_name,
|
template_versions.name AS template_version_name,
|
||||||
|
@ -12218,32 +12225,32 @@ LEFT JOIN LATERAL (
|
||||||
) template ON true
|
) template ON true
|
||||||
WHERE
|
WHERE
|
||||||
-- Optionally include deleted workspaces
|
-- Optionally include deleted workspaces
|
||||||
workspaces.deleted = $1
|
workspaces.deleted = $3
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $2 :: text != '' THEN
|
WHEN $4 :: text != '' THEN
|
||||||
CASE
|
CASE
|
||||||
-- Some workspace specific status refer to the transition
|
-- Some workspace specific status refer to the transition
|
||||||
-- type. By default, the standard provisioner job status
|
-- type. By default, the standard provisioner job status
|
||||||
-- search strings are supported.
|
-- search strings are supported.
|
||||||
-- 'running' states
|
-- 'running' states
|
||||||
WHEN $2 = 'starting' THEN
|
WHEN $4 = 'starting' THEN
|
||||||
latest_build.job_status = 'running'::provisioner_job_status AND
|
latest_build.job_status = 'running'::provisioner_job_status AND
|
||||||
latest_build.transition = 'start'::workspace_transition
|
latest_build.transition = 'start'::workspace_transition
|
||||||
WHEN $2 = 'stopping' THEN
|
WHEN $4 = 'stopping' THEN
|
||||||
latest_build.job_status = 'running'::provisioner_job_status AND
|
latest_build.job_status = 'running'::provisioner_job_status AND
|
||||||
latest_build.transition = 'stop'::workspace_transition
|
latest_build.transition = 'stop'::workspace_transition
|
||||||
WHEN $2 = 'deleting' THEN
|
WHEN $4 = 'deleting' THEN
|
||||||
latest_build.job_status = 'running' AND
|
latest_build.job_status = 'running' AND
|
||||||
latest_build.transition = 'delete'::workspace_transition
|
latest_build.transition = 'delete'::workspace_transition
|
||||||
|
|
||||||
-- 'succeeded' states
|
-- 'succeeded' states
|
||||||
WHEN $2 = 'deleted' THEN
|
WHEN $4 = 'deleted' THEN
|
||||||
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
||||||
latest_build.transition = 'delete'::workspace_transition
|
latest_build.transition = 'delete'::workspace_transition
|
||||||
WHEN $2 = 'stopped' THEN
|
WHEN $4 = 'stopped' THEN
|
||||||
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
||||||
latest_build.transition = 'stop'::workspace_transition
|
latest_build.transition = 'stop'::workspace_transition
|
||||||
WHEN $2 = 'started' THEN
|
WHEN $4 = 'started' THEN
|
||||||
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
||||||
latest_build.transition = 'start'::workspace_transition
|
latest_build.transition = 'start'::workspace_transition
|
||||||
|
|
||||||
|
@ -12251,13 +12258,13 @@ WHERE
|
||||||
-- differ. A workspace is "running" if the job is "succeeded" and
|
-- differ. A workspace is "running" if the job is "succeeded" and
|
||||||
-- the transition is "start". This is because a workspace starts
|
-- the transition is "start". This is because a workspace starts
|
||||||
-- running when a job is complete.
|
-- running when a job is complete.
|
||||||
WHEN $2 = 'running' THEN
|
WHEN $4 = 'running' THEN
|
||||||
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
latest_build.job_status = 'succeeded'::provisioner_job_status AND
|
||||||
latest_build.transition = 'start'::workspace_transition
|
latest_build.transition = 'start'::workspace_transition
|
||||||
|
|
||||||
WHEN $2 != '' THEN
|
WHEN $4 != '' THEN
|
||||||
-- By default just match the job status exactly
|
-- By default just match the job status exactly
|
||||||
latest_build.job_status = $2::provisioner_job_status
|
latest_build.job_status = $4::provisioner_job_status
|
||||||
ELSE
|
ELSE
|
||||||
true
|
true
|
||||||
END
|
END
|
||||||
|
@ -12265,46 +12272,80 @@ WHERE
|
||||||
END
|
END
|
||||||
-- Filter by owner_id
|
-- Filter by owner_id
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||||
workspaces.owner_id = $3
|
workspaces.owner_id = $5
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
|
-- Filter by build parameter
|
||||||
|
-- @has_param will match any build that includes the parameter.
|
||||||
|
AND CASE WHEN array_length($6 :: text[], 1) > 0 THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
workspace_build_parameters
|
||||||
|
WHERE
|
||||||
|
workspace_build_parameters.workspace_build_id = latest_build.id AND
|
||||||
|
-- ILIKE is case insensitive
|
||||||
|
workspace_build_parameters.name ILIKE ANY($6)
|
||||||
|
)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
-- @param_value will match param name an value.
|
||||||
|
-- requires 2 arrays, @param_names and @param_values to be passed in.
|
||||||
|
-- Array index must match between the 2 arrays for name=value
|
||||||
|
AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
workspace_build_parameters
|
||||||
|
INNER JOIN
|
||||||
|
build_params
|
||||||
|
ON
|
||||||
|
LOWER(workspace_build_parameters.name) = build_params.name AND
|
||||||
|
LOWER(workspace_build_parameters.value) = build_params.value AND
|
||||||
|
workspace_build_parameters.workspace_build_id = latest_build.id
|
||||||
|
)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
|
||||||
-- Filter by owner_name
|
-- Filter by owner_name
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $4 :: text != '' THEN
|
WHEN $7 :: text != '' THEN
|
||||||
workspaces.owner_id = (SELECT id FROM users WHERE lower(username) = lower($4) AND deleted = false)
|
workspaces.owner_id = (SELECT id FROM users WHERE lower(username) = lower($7) AND deleted = false)
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by template_name
|
-- Filter by template_name
|
||||||
-- There can be more than 1 template with the same name across organizations.
|
-- There can be more than 1 template with the same name across organizations.
|
||||||
-- Use the organization filter to restrict to 1 org if needed.
|
-- Use the organization filter to restrict to 1 org if needed.
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $5 :: text != '' THEN
|
WHEN $8 :: text != '' THEN
|
||||||
workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($5) AND deleted = false)
|
workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($8) AND deleted = false)
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by template_ids
|
-- Filter by template_ids
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN array_length($6 :: uuid[], 1) > 0 THEN
|
WHEN array_length($9 :: uuid[], 1) > 0 THEN
|
||||||
workspaces.template_id = ANY($6)
|
workspaces.template_id = ANY($9)
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by workspace_ids
|
-- Filter by workspace_ids
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN array_length($7 :: uuid[], 1) > 0 THEN
|
WHEN array_length($10 :: uuid[], 1) > 0 THEN
|
||||||
workspaces.id = ANY($7)
|
workspaces.id = ANY($10)
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by name, matching on substring
|
-- Filter by name, matching on substring
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $8 :: text != '' THEN
|
WHEN $11 :: text != '' THEN
|
||||||
workspaces.name ILIKE '%' || $8 || '%'
|
workspaces.name ILIKE '%' || $11 || '%'
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by agent status
|
-- Filter by agent status
|
||||||
-- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents.
|
-- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents.
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $9 :: text != '' THEN
|
WHEN $12 :: text != '' THEN
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM
|
FROM
|
||||||
|
@ -12316,7 +12357,7 @@ WHERE
|
||||||
WHERE
|
WHERE
|
||||||
workspace_resources.job_id = latest_build.provisioner_job_id AND
|
workspace_resources.job_id = latest_build.provisioner_job_id AND
|
||||||
latest_build.transition = 'start'::workspace_transition AND
|
latest_build.transition = 'start'::workspace_transition AND
|
||||||
$9 = (
|
$12 = (
|
||||||
CASE
|
CASE
|
||||||
WHEN workspace_agents.first_connected_at IS NULL THEN
|
WHEN workspace_agents.first_connected_at IS NULL THEN
|
||||||
CASE
|
CASE
|
||||||
|
@ -12327,7 +12368,7 @@ WHERE
|
||||||
END
|
END
|
||||||
WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN
|
WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN
|
||||||
'disconnected'
|
'disconnected'
|
||||||
WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $10 :: bigint THEN
|
WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $13 :: bigint THEN
|
||||||
'disconnected'
|
'disconnected'
|
||||||
WHEN workspace_agents.last_connected_at IS NOT NULL THEN
|
WHEN workspace_agents.last_connected_at IS NOT NULL THEN
|
||||||
'connected'
|
'connected'
|
||||||
|
@ -12340,24 +12381,24 @@ WHERE
|
||||||
END
|
END
|
||||||
-- Filter by dormant workspaces.
|
-- Filter by dormant workspaces.
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $11 :: boolean != 'false' THEN
|
WHEN $14 :: boolean != 'false' THEN
|
||||||
dormant_at IS NOT NULL
|
dormant_at IS NOT NULL
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by last_used
|
-- Filter by last_used
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $12 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
|
WHEN $15 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
|
||||||
workspaces.last_used_at <= $12
|
workspaces.last_used_at <= $15
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $13 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
|
WHEN $16 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
|
||||||
workspaces.last_used_at >= $13
|
workspaces.last_used_at >= $16
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $14 :: boolean IS NOT NULL THEN
|
WHEN $17 :: boolean IS NOT NULL THEN
|
||||||
(latest_build.template_version_id = template.active_version_id) = $14 :: boolean
|
(latest_build.template_version_id = template.active_version_id) = $17 :: boolean
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||||
|
@ -12369,7 +12410,7 @@ WHERE
|
||||||
filtered_workspaces fw
|
filtered_workspaces fw
|
||||||
ORDER BY
|
ORDER BY
|
||||||
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
|
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
|
||||||
CASE WHEN owner_id = $15 AND favorite THEN 0 ELSE 1 END ASC,
|
CASE WHEN owner_id = $18 AND favorite THEN 0 ELSE 1 END ASC,
|
||||||
(latest_build_completed_at IS NOT NULL AND
|
(latest_build_completed_at IS NOT NULL AND
|
||||||
latest_build_canceled_at IS NULL AND
|
latest_build_canceled_at IS NULL AND
|
||||||
latest_build_error IS NULL AND
|
latest_build_error IS NULL AND
|
||||||
|
@ -12378,11 +12419,11 @@ WHERE
|
||||||
LOWER(name) ASC
|
LOWER(name) ASC
|
||||||
LIMIT
|
LIMIT
|
||||||
CASE
|
CASE
|
||||||
WHEN $17 :: integer > 0 THEN
|
WHEN $20 :: integer > 0 THEN
|
||||||
$17
|
$20
|
||||||
END
|
END
|
||||||
OFFSET
|
OFFSET
|
||||||
$16
|
$19
|
||||||
), filtered_workspaces_order_with_summary AS (
|
), filtered_workspaces_order_with_summary AS (
|
||||||
SELECT
|
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
|
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
|
||||||
|
@ -12417,7 +12458,7 @@ WHERE
|
||||||
'', -- latest_build_error
|
'', -- latest_build_error
|
||||||
'start'::workspace_transition -- latest_build_transition
|
'start'::workspace_transition -- latest_build_transition
|
||||||
WHERE
|
WHERE
|
||||||
$18 :: boolean = true
|
$21 :: boolean = true
|
||||||
), total_count AS (
|
), total_count AS (
|
||||||
SELECT
|
SELECT
|
||||||
count(*) AS count
|
count(*) AS count
|
||||||
|
@ -12434,9 +12475,12 @@ CROSS JOIN
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetWorkspacesParams struct {
|
type GetWorkspacesParams struct {
|
||||||
|
ParamNames []string `db:"param_names" json:"param_names"`
|
||||||
|
ParamValues []string `db:"param_values" json:"param_values"`
|
||||||
Deleted bool `db:"deleted" json:"deleted"`
|
Deleted bool `db:"deleted" json:"deleted"`
|
||||||
Status string `db:"status" json:"status"`
|
Status string `db:"status" json:"status"`
|
||||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||||
|
HasParam []string `db:"has_param" json:"has_param"`
|
||||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||||
TemplateName string `db:"template_name" json:"template_name"`
|
TemplateName string `db:"template_name" json:"template_name"`
|
||||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||||
|
@ -12481,11 +12525,17 @@ type GetWorkspacesRow struct {
|
||||||
Count int64 `db:"count" json:"count"`
|
Count int64 `db:"count" json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build_params is used to filter by build parameters if present.
|
||||||
|
// It has to be a CTE because the set returning function 'unnest' cannot
|
||||||
|
// be used in a WHERE clause.
|
||||||
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) {
|
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getWorkspaces,
|
rows, err := q.db.QueryContext(ctx, getWorkspaces,
|
||||||
|
pq.Array(arg.ParamNames),
|
||||||
|
pq.Array(arg.ParamValues),
|
||||||
arg.Deleted,
|
arg.Deleted,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.OwnerID,
|
arg.OwnerID,
|
||||||
|
pq.Array(arg.HasParam),
|
||||||
arg.OwnerUsername,
|
arg.OwnerUsername,
|
||||||
arg.TemplateName,
|
arg.TemplateName,
|
||||||
pq.Array(arg.TemplateIDs),
|
pq.Array(arg.TemplateIDs),
|
||||||
|
|
|
@ -77,7 +77,16 @@ WHERE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- name: GetWorkspaces :many
|
-- name: GetWorkspaces :many
|
||||||
WITH filtered_workspaces AS (
|
WITH
|
||||||
|
-- build_params is used to filter by build parameters if present.
|
||||||
|
-- It has to be a CTE because the set returning function 'unnest' cannot
|
||||||
|
-- be used in a WHERE clause.
|
||||||
|
build_params AS (
|
||||||
|
SELECT
|
||||||
|
LOWER(unnest(@param_names :: text[])) AS name,
|
||||||
|
LOWER(unnest(@param_values :: text[])) AS value
|
||||||
|
),
|
||||||
|
filtered_workspaces AS (
|
||||||
SELECT
|
SELECT
|
||||||
workspaces.*,
|
workspaces.*,
|
||||||
COALESCE(template.name, 'unknown') as template_name,
|
COALESCE(template.name, 'unknown') as template_name,
|
||||||
|
@ -96,6 +105,7 @@ ON
|
||||||
workspaces.owner_id = users.id
|
workspaces.owner_id = users.id
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT
|
SELECT
|
||||||
|
workspace_builds.id,
|
||||||
workspace_builds.transition,
|
workspace_builds.transition,
|
||||||
workspace_builds.template_version_id,
|
workspace_builds.template_version_id,
|
||||||
template_versions.name AS template_version_name,
|
template_versions.name AS template_version_name,
|
||||||
|
@ -184,6 +194,40 @@ WHERE
|
||||||
workspaces.owner_id = @owner_id
|
workspaces.owner_id = @owner_id
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
|
-- Filter by build parameter
|
||||||
|
-- @has_param will match any build that includes the parameter.
|
||||||
|
AND CASE WHEN array_length(@has_param :: text[], 1) > 0 THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
workspace_build_parameters
|
||||||
|
WHERE
|
||||||
|
workspace_build_parameters.workspace_build_id = latest_build.id AND
|
||||||
|
-- ILIKE is case insensitive
|
||||||
|
workspace_build_parameters.name ILIKE ANY(@has_param)
|
||||||
|
)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
-- @param_value will match param name an value.
|
||||||
|
-- requires 2 arrays, @param_names and @param_values to be passed in.
|
||||||
|
-- Array index must match between the 2 arrays for name=value
|
||||||
|
AND CASE WHEN array_length(@param_names :: text[], 1) > 0 THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
workspace_build_parameters
|
||||||
|
INNER JOIN
|
||||||
|
build_params
|
||||||
|
ON
|
||||||
|
LOWER(workspace_build_parameters.name) = build_params.name AND
|
||||||
|
LOWER(workspace_build_parameters.value) = build_params.value AND
|
||||||
|
workspace_build_parameters.workspace_build_id = latest_build.id
|
||||||
|
)
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
|
||||||
-- Filter by owner_name
|
-- Filter by owner_name
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN @owner_username :: text != '' THEN
|
WHEN @owner_username :: text != '' THEN
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -268,18 +269,18 @@ func ParseCustomList[T any](parser *QueryParamParser, vals url.Values, def []T,
|
||||||
allTerms = append(allTerms, terms...)
|
allTerms = append(allTerms, terms...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var badValues []string
|
var badErrors error
|
||||||
var output []T
|
var output []T
|
||||||
for _, s := range allTerms {
|
for _, s := range allTerms {
|
||||||
good, err := parseFunc(s)
|
good, err := parseFunc(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badValues = append(badValues, s)
|
badErrors = errors.Join(badErrors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
output = append(output, good)
|
output = append(output, good)
|
||||||
}
|
}
|
||||||
if len(badValues) > 0 {
|
if badErrors != nil {
|
||||||
return []T{}, xerrors.Errorf("%s", strings.Join(badValues, ","))
|
return []T{}, badErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
|
|
|
@ -388,7 +388,7 @@ func TestParseQueryParams(t *testing.T) {
|
||||||
Value: "6c8ef17d-5dd8-4b92-bac9-41944f90f237,bogus",
|
Value: "6c8ef17d-5dd8-4b92-bac9-41944f90f237,bogus",
|
||||||
Expected: []uuid.UUID{},
|
Expected: []uuid.UUID{},
|
||||||
Default: []uuid.UUID{},
|
Default: []uuid.UUID{},
|
||||||
ExpectedErrorContains: "bogus",
|
ExpectedErrorContains: "invalid UUID length",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
QueryParam: "multiple_keys",
|
QueryParam: "multiple_keys",
|
||||||
|
|
|
@ -121,6 +121,40 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
|
||||||
Valid: values.Has("outdated"),
|
Valid: values.Has("outdated"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type paramMatch struct {
|
||||||
|
name string
|
||||||
|
value *string
|
||||||
|
}
|
||||||
|
// parameter matching takes the form of:
|
||||||
|
// `param:<name>[=<value>]`
|
||||||
|
// If the value is omitted, then we match on the presence of the parameter.
|
||||||
|
// If the value is provided, then we match on the parameter and value.
|
||||||
|
params := httpapi.ParseCustomList(parser, values, []paramMatch{}, "param", func(v string) (paramMatch, error) {
|
||||||
|
// Ignore excess spaces
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
parts := strings.Split(v, "=")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
// Only match on the presence of the parameter
|
||||||
|
return paramMatch{name: parts[0], value: nil}, nil
|
||||||
|
}
|
||||||
|
if len(parts) == 2 {
|
||||||
|
if parts[1] == "" {
|
||||||
|
return paramMatch{}, xerrors.Errorf("query element %q has an empty value. omit the '=' to match just on the parameter name", v)
|
||||||
|
}
|
||||||
|
// Match on the parameter and value
|
||||||
|
return paramMatch{name: parts[0], value: &parts[1]}, nil
|
||||||
|
}
|
||||||
|
return paramMatch{}, xerrors.Errorf("query element %q can only contain 1 '='", v)
|
||||||
|
})
|
||||||
|
for _, p := range params {
|
||||||
|
if p.value == nil {
|
||||||
|
filter.HasParam = append(filter.HasParam, p.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filter.ParamNames = append(filter.ParamNames, p.name)
|
||||||
|
filter.ParamValues = append(filter.ParamValues, *p.value)
|
||||||
|
}
|
||||||
|
|
||||||
parser.ErrorExcessParams(values)
|
parser.ErrorExcessParams(values)
|
||||||
return filter, parser.Errors
|
return filter, parser.Errors
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,74 @@ func TestSearchWorkspace(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "ParamName",
|
||||||
|
Query: "param:foo",
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
HasParam: []string{"foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "MultipleParamNames",
|
||||||
|
Query: "param:foo param:bar param:baz",
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
HasParam: []string{"foo", "bar", "baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ParamValue",
|
||||||
|
Query: "param:foo=bar",
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
ParamNames: []string{"foo"},
|
||||||
|
ParamValues: []string{"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "QuotedParamValue",
|
||||||
|
Query: `param:"image=ghcr.io/coder/coder-preview:main"`,
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
ParamNames: []string{"image"},
|
||||||
|
ParamValues: []string{"ghcr.io/coder/coder-preview:main"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "MultipleParamValues",
|
||||||
|
Query: "param:foo=bar param:fuzz=buzz",
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
ParamNames: []string{"foo", "fuzz"},
|
||||||
|
ParamValues: []string{"bar", "buzz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "MixedParams",
|
||||||
|
Query: "param:dot param:foo=bar param:fuzz=buzz param:tot",
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
HasParam: []string{"dot", "tot"},
|
||||||
|
ParamNames: []string{"foo", "fuzz"},
|
||||||
|
ParamValues: []string{"bar", "buzz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ParamSpaces",
|
||||||
|
Query: `param:" dot " param:" foo=bar "`,
|
||||||
|
Expected: database.GetWorkspacesParams{
|
||||||
|
HasParam: []string{"dot"},
|
||||||
|
ParamNames: []string{"foo"},
|
||||||
|
ParamValues: []string{"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Failures
|
// Failures
|
||||||
|
{
|
||||||
|
Name: "ParamExcessValue",
|
||||||
|
Query: "param:foo=bar=baz",
|
||||||
|
ExpectedErrorContains: "can only contain 1 '='",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ParamNoValue",
|
||||||
|
Query: "param:foo=",
|
||||||
|
ExpectedErrorContains: "omit the '=' to match",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "NoPrefix",
|
Name: "NoPrefix",
|
||||||
Query: `:foo`,
|
Query: `:foo`,
|
||||||
|
@ -163,6 +230,11 @@ func TestSearchWorkspace(t *testing.T) {
|
||||||
Query: `foo:bar`,
|
Query: `foo:bar`,
|
||||||
ExpectedErrorContains: `"foo" is not a valid query param`,
|
ExpectedErrorContains: `"foo" is not a valid query param`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "ParamExtraColons",
|
||||||
|
Query: "param:foo:value",
|
||||||
|
ExpectedErrorContains: "can only contain 1 ':'",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range testCases {
|
for _, c := range testCases {
|
||||||
|
@ -182,6 +254,10 @@ func TestSearchWorkspace(t *testing.T) {
|
||||||
// nil slice vs 0 len slice is equivalent for our purposes.
|
// nil slice vs 0 len slice is equivalent for our purposes.
|
||||||
c.Expected.WorkspaceIds = values.WorkspaceIds
|
c.Expected.WorkspaceIds = values.WorkspaceIds
|
||||||
}
|
}
|
||||||
|
if len(c.Expected.HasParam) == len(values.HasParam) {
|
||||||
|
// nil slice vs 0 len slice is equivalent for our purposes.
|
||||||
|
c.Expected.HasParam = values.HasParam
|
||||||
|
}
|
||||||
assert.Len(t, errs, 0, "expected no error")
|
assert.Len(t, errs, 0, "expected no error")
|
||||||
assert.Equal(t, c.Expected, values, "expected values")
|
assert.Equal(t, c.Expected, values, "expected values")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1322,6 +1322,20 @@ func TestWorkspaceFilter(t *testing.T) {
|
||||||
func TestWorkspaceFilterManual(t *testing.T) {
|
func TestWorkspaceFilterManual(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
expectIDs := func(t *testing.T, exp []codersdk.Workspace, got []codersdk.Workspace) {
|
||||||
|
t.Helper()
|
||||||
|
expIDs := make([]uuid.UUID, 0, len(exp))
|
||||||
|
for _, e := range exp {
|
||||||
|
expIDs = append(expIDs, e.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotIDs := make([]uuid.UUID, 0, len(got))
|
||||||
|
for _, g := range got {
|
||||||
|
gotIDs = append(gotIDs, g.ID)
|
||||||
|
}
|
||||||
|
require.ElementsMatchf(t, expIDs, gotIDs, "expected IDs")
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("Name", func(t *testing.T) {
|
t.Run("Name", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
@ -1593,7 +1607,6 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||||
return workspaces.Count == 1
|
return workspaces.Count == 1
|
||||||
}, testutil.IntervalMedium, "agent status timeout")
|
}, testutil.IntervalMedium, "agent status timeout")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Dormant", func(t *testing.T) {
|
t.Run("Dormant", func(t *testing.T) {
|
||||||
// this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
|
// this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
@ -1640,7 +1653,6 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||||
require.Equal(t, dormantWorkspace.ID, res.Workspaces[0].ID)
|
require.Equal(t, dormantWorkspace.ID, res.Workspaces[0].ID)
|
||||||
require.NotNil(t, res.Workspaces[0].DormantAt)
|
require.NotNil(t, res.Workspaces[0].DormantAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("LastUsed", func(t *testing.T) {
|
t.Run("LastUsed", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -1747,6 +1759,172 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||||
require.Len(t, res.Workspaces, 1)
|
require.Len(t, res.Workspaces, 1)
|
||||||
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
||||||
})
|
})
|
||||||
|
t.Run("Params", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
paramOneName = "one"
|
||||||
|
paramTwoName = "two"
|
||||||
|
paramThreeName = "three"
|
||||||
|
paramOptional = "optional"
|
||||||
|
)
|
||||||
|
|
||||||
|
makeParameters := func(extra ...*proto.RichParameter) *echo.Responses {
|
||||||
|
return &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionPlan: []*proto.Response{
|
||||||
|
{
|
||||||
|
Type: &proto.Response_Plan{
|
||||||
|
Plan: &proto.PlanComplete{
|
||||||
|
Parameters: append([]*proto.RichParameter{
|
||||||
|
{Name: paramOneName, Description: "", Mutable: true, Type: "string"},
|
||||||
|
{Name: paramTwoName, DisplayName: "", Description: "", Mutable: true, Type: "string"},
|
||||||
|
{Name: paramThreeName, Description: "", Mutable: true, Type: "string"},
|
||||||
|
}, extra...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProvisionApply: echo.ApplyComplete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, makeParameters(&proto.RichParameter{Name: paramOptional, Description: "", Mutable: true, Type: "string"}))
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
noOptionalVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, makeParameters(), func(request *codersdk.CreateTemplateVersionRequest) {
|
||||||
|
request.TemplateID = template.ID
|
||||||
|
})
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, noOptionalVersion.ID)
|
||||||
|
|
||||||
|
// foo :: one=foo, two=bar, one=baz, optional=optional
|
||||||
|
foo := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
||||||
|
request.TemplateVersionID = version.ID
|
||||||
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||||
|
{
|
||||||
|
Name: paramOneName,
|
||||||
|
Value: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramTwoName,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramThreeName,
|
||||||
|
Value: "baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramOptional,
|
||||||
|
Value: "optional",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// bar :: one=foo, two=bar, three=baz, optional=optional
|
||||||
|
bar := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
||||||
|
request.TemplateVersionID = version.ID
|
||||||
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||||
|
{
|
||||||
|
Name: paramOneName,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramTwoName,
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramThreeName,
|
||||||
|
Value: "baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramOptional,
|
||||||
|
Value: "optional",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// baz :: one=baz, two=baz, three=baz
|
||||||
|
baz := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
||||||
|
request.TemplateVersionID = noOptionalVersion.ID
|
||||||
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||||
|
{
|
||||||
|
Name: paramOneName,
|
||||||
|
Value: "unique",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramTwoName,
|
||||||
|
Value: "baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: paramThreeName,
|
||||||
|
Value: "baz",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
//nolint:tparallel,paralleltest
|
||||||
|
t.Run("has_param", func(t *testing.T) {
|
||||||
|
// Checks the existence of a param value
|
||||||
|
// all match
|
||||||
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:%s", paramOneName),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
||||||
|
|
||||||
|
// Some match
|
||||||
|
optional, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:%s", paramOptional),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo, bar}, optional.Workspaces)
|
||||||
|
|
||||||
|
// None match
|
||||||
|
none, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: "param:not-a-param",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, none.Workspaces, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
//nolint:tparallel,paralleltest
|
||||||
|
t.Run("exact_param", func(t *testing.T) {
|
||||||
|
// All match
|
||||||
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramThreeName, "baz"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
||||||
|
|
||||||
|
// Two match
|
||||||
|
two, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramTwoName, "bar"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo, bar}, two.Workspaces)
|
||||||
|
|
||||||
|
// Only 1 matches
|
||||||
|
one, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramOneName, "foo"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo}, one.Workspaces)
|
||||||
|
})
|
||||||
|
|
||||||
|
//nolint:tparallel,paralleltest
|
||||||
|
t.Run("exact_param_and_has", func(t *testing.T) {
|
||||||
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
|
FilterQuery: fmt.Sprintf("param:not=athing param:%s=%s param:%s=%s", paramOptional, "optional", paramOneName, "unique"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOffsetLimit(t *testing.T) {
|
func TestOffsetLimit(t *testing.T) {
|
||||||
|
|
Loading…
Reference in New Issue