Skip to content

Commit d6ba0df

Browse files
authored
feat: add "updated" search param to workspaces (#11714)
* feat: add "updated" search param to workspaces * rego -> sql needs to specify which <table>.organization_id
1 parent 081fbef commit d6ba0df

File tree

10 files changed

+159
-25
lines changed

10 files changed

+159
-25
lines changed

coderd/database/dbmem/dbmem.go

+17
Original file line numberDiff line numberDiff line change
@@ -7534,6 +7534,23 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
75347534
}
75357535
}
75367536

7537+
if arg.UsingActive.Valid {
7538+
build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID)
7539+
if err != nil {
7540+
return nil, xerrors.Errorf("get latest build: %w", err)
7541+
}
7542+
7543+
template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID)
7544+
if err != nil {
7545+
return nil, xerrors.Errorf("get template: %w", err)
7546+
}
7547+
7548+
updated := build.TemplateVersionID == template.ActiveVersionID
7549+
if arg.UsingActive.Bool != updated {
7550+
continue
7551+
}
7552+
}
7553+
75377554
if !arg.Deleted && workspace.Deleted {
75387555
continue
75397556
}

coderd/database/modelqueries.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ type workspaceQuerier interface {
198198
// This code is copied from `GetWorkspaces` and adds the authorized filter WHERE
199199
// clause.
200200
func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) {
201-
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL())
201+
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigWorkspaces())
202202
if err != nil {
203203
return nil, xerrors.Errorf("compile authorized filter: %w", err)
204204
}
@@ -225,6 +225,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
225225
arg.Dormant,
226226
arg.LastUsedBefore,
227227
arg.LastUsedAfter,
228+
arg.UsingActive,
228229
arg.Offset,
229230
arg.Limit,
230231
)

coderd/database/queries.sql.go

+27-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

+8-3
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ WHERE
7979
-- name: GetWorkspaces :many
8080
SELECT
8181
workspaces.*,
82-
COALESCE(template_name.template_name, 'unknown') as template_name,
82+
COALESCE(template.name, 'unknown') as template_name,
8383
latest_build.template_version_id,
8484
latest_build.template_version_name,
8585
COUNT(*) OVER () as count
@@ -120,12 +120,12 @@ LEFT JOIN LATERAL (
120120
) latest_build ON TRUE
121121
LEFT JOIN LATERAL (
122122
SELECT
123-
templates.name AS template_name
123+
*
124124
FROM
125125
templates
126126
WHERE
127127
templates.id = workspaces.template_id
128-
) template_name ON true
128+
) template ON true
129129
WHERE
130130
-- Optionally include deleted workspaces
131131
workspaces.deleted = @deleted
@@ -259,6 +259,11 @@ WHERE
259259
workspaces.last_used_at >= @last_used_after
260260
ELSE true
261261
END
262+
AND CASE
263+
WHEN sqlc.narg('using_active') :: boolean IS NOT NULL THEN
264+
(latest_build.template_version_id = template.active_version_id) = sqlc.narg('using_active') :: boolean
265+
ELSE true
266+
END
262267
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
263268
-- @authorize_filter
264269
ORDER BY

coderd/rbac/authz.go

+6
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,12 @@ func ConfigWithoutACL() regosql.ConvertConfig {
611611
}
612612
}
613613

614+
func ConfigWorkspaces() regosql.ConvertConfig {
615+
return regosql.ConvertConfig{
616+
VariableConverter: regosql.WorkspaceConverter(),
617+
}
618+
}
619+
614620
func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) {
615621
root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries)
616622
if err != nil {

coderd/rbac/regosql/configs.go

+14
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ func UserConverter() *sqltypes.VariableConverter {
5353
return matcher
5454
}
5555

56+
func WorkspaceConverter() *sqltypes.VariableConverter {
57+
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
58+
resourceIDMatcher(),
59+
sqltypes.StringVarMatcher("workspaces.organization_id :: text", []string{"input", "object", "org_owner"}),
60+
userOwnerMatcher(),
61+
)
62+
matcher.RegisterMatcher(
63+
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
64+
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
65+
)
66+
67+
return matcher
68+
}
69+
5670
// NoACLConverter should be used when the target SQL table does not contain
5771
// group or user ACL columns.
5872
func NoACLConverter() *sqltypes.VariableConverter {

coderd/searchquery/search.go

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package searchquery
22

33
import (
4+
"database/sql"
45
"fmt"
56
"net/url"
67
"strings"
@@ -110,6 +111,14 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
110111
filter.Dormant = parser.Boolean(values, false, "dormant")
111112
filter.LastUsedAfter = parser.Time3339Nano(values, time.Time{}, "last_used_after")
112113
filter.LastUsedBefore = parser.Time3339Nano(values, time.Time{}, "last_used_before")
114+
filter.UsingActive = sql.NullBool{
115+
// Invert the value of the query parameter to get the correct value.
116+
// UsingActive returns if the workspace is on the latest template active version.
117+
Bool: !parser.Boolean(values, true, "outdated"),
118+
// Only include this search term if it was provided. Otherwise default to omitting it
119+
// which will return all workspaces.
120+
Valid: values.Has("outdated"),
121+
}
113122

114123
parser.ErrorExcessParams(values)
115124
return filter, parser.Errors

coderd/searchquery/search_test.go

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package searchquery_test
22

33
import (
4+
"database/sql"
45
"fmt"
56
"strings"
67
"testing"
@@ -116,7 +117,26 @@ func TestSearchWorkspace(t *testing.T) {
116117
OwnerUsername: "foo",
117118
},
118119
},
119-
120+
{
121+
Name: "Outdated",
122+
Query: `outdated:true`,
123+
Expected: database.GetWorkspacesParams{
124+
UsingActive: sql.NullBool{
125+
Bool: false,
126+
Valid: true,
127+
},
128+
},
129+
},
130+
{
131+
Name: "Updated",
132+
Query: `outdated:false`,
133+
Expected: database.GetWorkspacesParams{
134+
UsingActive: sql.NullBool{
135+
Bool: true,
136+
Valid: true,
137+
},
138+
},
139+
},
120140
// Failures
121141
{
122142
Name: "NoPrefix",

coderd/workspaces_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,56 @@ func TestWorkspaceFilterManual(t *testing.T) {
16321632
require.Len(t, afterRes.Workspaces, 1)
16331633
require.Equal(t, after.ID, afterRes.Workspaces[0].ID)
16341634
})
1635+
t.Run("Updated", func(t *testing.T) {
1636+
t.Parallel()
1637+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1638+
user := coderdtest.CreateFirstUser(t, client)
1639+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
1640+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1641+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
1642+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
1643+
1644+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1645+
defer cancel()
1646+
1647+
// Workspace is up-to-date
1648+
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
1649+
FilterQuery: "outdated:false",
1650+
})
1651+
require.NoError(t, err)
1652+
require.Len(t, res.Workspaces, 1)
1653+
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
1654+
1655+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
1656+
FilterQuery: "outdated:true",
1657+
})
1658+
require.NoError(t, err)
1659+
require.Len(t, res.Workspaces, 0)
1660+
1661+
// Now make it out of date
1662+
newTv := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
1663+
request.TemplateID = template.ID
1664+
})
1665+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1666+
err = client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
1667+
ID: newTv.ID,
1668+
})
1669+
require.NoError(t, err)
1670+
1671+
// Check the query again
1672+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
1673+
FilterQuery: "outdated:false",
1674+
})
1675+
require.NoError(t, err)
1676+
require.Len(t, res.Workspaces, 0)
1677+
1678+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
1679+
FilterQuery: "outdated:true",
1680+
})
1681+
require.NoError(t, err)
1682+
require.Len(t, res.Workspaces, 1)
1683+
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
1684+
})
16351685
}
16361686

16371687
func TestOffsetLimit(t *testing.T) {

site/src/pages/WorkspacesPage/filter/filter.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const workspaceFilterQuery = {
2222
running: "status:running",
2323
failed: "status:failed",
2424
dormant: "dormant:true",
25+
outdated: "outdated:true",
2526
};
2627

2728
type FilterPreset = {
@@ -48,6 +49,10 @@ const PRESET_FILTERS: FilterPreset[] = [
4849
query: workspaceFilterQuery.failed,
4950
name: "Failed workspaces",
5051
},
52+
{
53+
query: workspaceFilterQuery.outdated,
54+
name: "Outdated workspaces",
55+
},
5156
];
5257

5358
// Defined outside component so that the array doesn't get reconstructed each render

0 commit comments

Comments
 (0)