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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 27 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 8 additions & 3 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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

Lines changed: 21 additions & 1 deletion
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

Lines changed: 50 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy