Skip to content

Commit b96ac67

Browse files
authored
chore: add organization search query to workspaces (coder#14474)
* chore: add organization search query to workspaces
1 parent 54fe082 commit b96ac67

File tree

7 files changed

+109
-78
lines changed

7 files changed

+109
-78
lines changed

coderd/database/dbmem/dbmem.go

+6
Original file line numberDiff line numberDiff line change
@@ -9972,6 +9972,12 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
99729972
}
99739973
}
99749974

9975+
if arg.OrganizationID != uuid.Nil {
9976+
if workspace.OrganizationID != arg.OrganizationID {
9977+
continue
9978+
}
9979+
}
9980+
99759981
if arg.OwnerUsername != "" {
99769982
owner, err := q.getUserByIDNoLock(workspace.OwnerID)
99779983
if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) {

coderd/database/modelqueries.go

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
247247
arg.Deleted,
248248
arg.Status,
249249
arg.OwnerID,
250+
arg.OrganizationID,
250251
pq.Array(arg.HasParam),
251252
arg.OwnerUsername,
252253
arg.TemplateName,

coderd/database/queries.sql.go

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

coderd/database/queries/workspaces.sql

+6
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ WHERE
195195
workspaces.owner_id = @owner_id
196196
ELSE true
197197
END
198+
-- Filter by organization_id
199+
AND CASE
200+
WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
201+
workspaces.organization_id = @organization_id
202+
ELSE true
203+
END
198204
-- Filter by build parameter
199205
-- @has_param will match any build that includes the parameter.
200206
AND CASE WHEN array_length(@has_param :: text[], 1) > 0 THEN

coderd/searchquery/search.go

+26-48
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G
3939
Email: parser.String(values, "", "email"),
4040
DateFrom: parser.Time(values, time.Time{}, "date_from", dateLayout),
4141
DateTo: parser.Time(values, time.Time{}, "date_to", dateLayout),
42+
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
4243
ResourceType: string(httpapi.ParseCustom(parser, values, "", "resource_type", httpapi.ParseEnum[database.ResourceType])),
4344
Action: string(httpapi.ParseCustom(parser, values, "", "action", httpapi.ParseEnum[database.AuditAction])),
4445
BuildReason: string(httpapi.ParseCustom(parser, values, "", "build_reason", httpapi.ParseEnum[database.BuildReason])),
@@ -47,27 +48,6 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G
4748
filter.DateTo = filter.DateTo.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
4849
}
4950

50-
// Convert the "organization" parameter to an organization uuid. This can require
51-
// a database lookup.
52-
organizationArg := parser.String(values, "", "organization")
53-
if organizationArg != "" {
54-
organizationID, err := uuid.Parse(organizationArg)
55-
if err == nil {
56-
filter.OrganizationID = organizationID
57-
} else {
58-
// Organization could be a name
59-
organization, err := db.GetOrganizationByName(ctx, organizationArg)
60-
if err != nil {
61-
parser.Errors = append(parser.Errors, codersdk.ValidationError{
62-
Field: "organization",
63-
Detail: fmt.Sprintf("Organization %q either does not exist, or you are unauthorized to view it", organizationArg),
64-
})
65-
} else {
66-
filter.OrganizationID = organization.ID
67-
}
68-
}
69-
}
70-
7151
parser.ErrorExcessParams(values)
7252
return filter, parser.Errors
7353
}
@@ -95,7 +75,7 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) {
9575
return filter, parser.Errors
9676
}
9777

98-
func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectTimeout time.Duration) (database.GetWorkspacesParams, []codersdk.ValidationError) {
78+
func Workspaces(ctx context.Context, db database.Store, query string, page codersdk.Pagination, agentInactiveDisconnectTimeout time.Duration) (database.GetWorkspacesParams, []codersdk.ValidationError) {
9979
filter := database.GetWorkspacesParams{
10080
AgentInactiveDisconnectTimeoutSeconds: int64(agentInactiveDisconnectTimeout.Seconds()),
10181

@@ -145,6 +125,7 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
145125
// which will return all workspaces.
146126
Valid: values.Has("outdated"),
147127
}
128+
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
148129

149130
type paramMatch struct {
150131
name string
@@ -198,32 +179,12 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G
198179

199180
parser := httpapi.NewQueryParamParser()
200181
filter := database.GetTemplatesWithFilterParams{
201-
Deleted: parser.Boolean(values, false, "deleted"),
202-
ExactName: parser.String(values, "", "exact_name"),
203-
FuzzyName: parser.String(values, "", "name"),
204-
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
205-
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
206-
}
207-
208-
// Convert the "organization" parameter to an organization uuid. This can require
209-
// a database lookup.
210-
organizationArg := parser.String(values, "", "organization")
211-
if organizationArg != "" {
212-
organizationID, err := uuid.Parse(organizationArg)
213-
if err == nil {
214-
filter.OrganizationID = organizationID
215-
} else {
216-
// Organization could be a name
217-
organization, err := db.GetOrganizationByName(ctx, organizationArg)
218-
if err != nil {
219-
parser.Errors = append(parser.Errors, codersdk.ValidationError{
220-
Field: "organization",
221-
Detail: fmt.Sprintf("Organization %q either does not exist, or you are unauthorized to view it", organizationArg),
222-
})
223-
} else {
224-
filter.OrganizationID = organization.ID
225-
}
226-
}
182+
Deleted: parser.Boolean(values, false, "deleted"),
183+
ExactName: parser.String(values, "", "exact_name"),
184+
FuzzyName: parser.String(values, "", "name"),
185+
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
186+
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
187+
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
227188
}
228189

229190
parser.ErrorExcessParams(values)
@@ -271,6 +232,23 @@ func searchTerms(query string, defaultKey func(term string, values url.Values) e
271232
return searchValues, nil
272233
}
273234

235+
func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string) uuid.UUID {
236+
return httpapi.ParseCustom(parser, vals, uuid.Nil, queryParam, func(v string) (uuid.UUID, error) {
237+
if v == "" {
238+
return uuid.Nil, nil
239+
}
240+
organizationID, err := uuid.Parse(v)
241+
if err == nil {
242+
return organizationID, nil
243+
}
244+
organization, err := db.GetOrganizationByName(ctx, v)
245+
if err != nil {
246+
return uuid.Nil, xerrors.Errorf("organization %q either does not exist, or you are unauthorized to view it", v)
247+
}
248+
return organization.ID, nil
249+
})
250+
}
251+
274252
// splitQueryParameterByDelimiter takes a query string and splits it into the individual elements
275253
// of the query. Each element is separated by a delimiter. All quoted strings are
276254
// kept as a single element.

coderd/searchquery/search_test.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/stretchr/testify/require"
1414

1515
"github.com/coder/coder/v2/coderd/database"
16+
"github.com/coder/coder/v2/coderd/database/dbgen"
1617
"github.com/coder/coder/v2/coderd/database/dbmem"
1718
"github.com/coder/coder/v2/coderd/searchquery"
1819
"github.com/coder/coder/v2/codersdk"
@@ -25,6 +26,7 @@ func TestSearchWorkspace(t *testing.T) {
2526
Query string
2627
Expected database.GetWorkspacesParams
2728
ExpectedErrorContains string
29+
Setup func(t *testing.T, db database.Store)
2830
}{
2931
{
3032
Name: "Empty",
@@ -195,6 +197,31 @@ func TestSearchWorkspace(t *testing.T) {
195197
ParamValues: []string{"bar"},
196198
},
197199
},
200+
{
201+
Name: "Organization",
202+
Query: `organization:4fe722f0-49bc-4a90-a3eb-4ac439bfce20`,
203+
Setup: func(t *testing.T, db database.Store) {
204+
dbgen.Organization(t, db, database.Organization{
205+
ID: uuid.MustParse("4fe722f0-49bc-4a90-a3eb-4ac439bfce20"),
206+
})
207+
},
208+
Expected: database.GetWorkspacesParams{
209+
OrganizationID: uuid.MustParse("4fe722f0-49bc-4a90-a3eb-4ac439bfce20"),
210+
},
211+
},
212+
{
213+
Name: "OrganizationByName",
214+
Query: `organization:foobar`,
215+
Setup: func(t *testing.T, db database.Store) {
216+
dbgen.Organization(t, db, database.Organization{
217+
ID: uuid.MustParse("08eb6715-02f8-45c5-b86d-03786fcfbb4e"),
218+
Name: "foobar",
219+
})
220+
},
221+
Expected: database.GetWorkspacesParams{
222+
OrganizationID: uuid.MustParse("08eb6715-02f8-45c5-b86d-03786fcfbb4e"),
223+
},
224+
},
198225

199226
// Failures
200227
{
@@ -243,7 +270,12 @@ func TestSearchWorkspace(t *testing.T) {
243270
c := c
244271
t.Run(c.Name, func(t *testing.T) {
245272
t.Parallel()
246-
values, errs := searchquery.Workspaces(c.Query, codersdk.Pagination{}, 0)
273+
// TODO: Replace this with the mock database.
274+
db := dbmem.New()
275+
if c.Setup != nil {
276+
c.Setup(t, db)
277+
}
278+
values, errs := searchquery.Workspaces(context.Background(), db, c.Query, codersdk.Pagination{}, 0)
247279
if c.ExpectedErrorContains != "" {
248280
assert.True(t, len(errs) > 0, "expect some errors")
249281
var s strings.Builder
@@ -270,7 +302,7 @@ func TestSearchWorkspace(t *testing.T) {
270302

271303
query := ``
272304
timeout := 1337 * time.Second
273-
values, errs := searchquery.Workspaces(query, codersdk.Pagination{}, timeout)
305+
values, errs := searchquery.Workspaces(context.Background(), dbmem.New(), query, codersdk.Pagination{}, timeout)
274306
require.Empty(t, errs)
275307
require.Equal(t, int64(timeout.Seconds()), values.AgentInactiveDisconnectTimeoutSeconds)
276308
})

0 commit comments

Comments
 (0)