Skip to content

Commit a3ccc19

Browse files
committed
endpoint
1 parent 62455e7 commit a3ccc19

File tree

8 files changed

+318
-12
lines changed

8 files changed

+318
-12
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,17 @@ func isDeprecated(template database.Template) bool {
13891389
return template.Deprecated != ""
13901390
}
13911391

1392+
func (q *FakeQuerier) getWorkspaceBuildParametersNoLock(workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
1393+
params := make([]database.WorkspaceBuildParameter, 0)
1394+
for _, param := range q.workspaceBuildParameters {
1395+
if param.WorkspaceBuildID != workspaceBuildID {
1396+
continue
1397+
}
1398+
params = append(params, param)
1399+
}
1400+
return params, nil
1401+
}
1402+
13921403
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
13931404
return xerrors.New("AcquireLock must only be called within a transaction")
13941405
}
@@ -7898,14 +7909,7 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu
78987909
q.mutex.RLock()
78997910
defer q.mutex.RUnlock()
79007911

7901-
params := make([]database.WorkspaceBuildParameter, 0)
7902-
for _, param := range q.workspaceBuildParameters {
7903-
if param.WorkspaceBuildID != workspaceBuildID {
7904-
continue
7905-
}
7906-
params = append(params, param)
7907-
}
7908-
return params, nil
7912+
return q.getWorkspaceBuildParametersNoLock(workspaceBuildID)
79097913
}
79107914

79117915
func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
@@ -13233,6 +13237,18 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
1323313237
continue
1323413238
}
1323513239
}
13240+
13241+
if arg.HasAITask.Valid {
13242+
tv, err := q.getTemplateVersionByIDNoLock(ctx, template.ActiveVersionID)
13243+
if err != nil {
13244+
return nil, xerrors.Errorf("get template version: %w", err)
13245+
}
13246+
tvHasAITask := tv.HasAITask.Valid && tv.HasAITask.Bool
13247+
if tvHasAITask != arg.HasAITask.Bool {
13248+
continue
13249+
}
13250+
}
13251+
1323613252
templates = append(templates, template)
1323713253
}
1323813254
if len(templates) > 0 {
@@ -13562,6 +13578,43 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
1356213578
}
1356313579
}
1356413580

13581+
if arg.HasAITask.Valid {
13582+
hasAITask, err := func() (bool, error) {
13583+
build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID)
13584+
if err != nil {
13585+
return false, xerrors.Errorf("get latest build: %w", err)
13586+
}
13587+
if build.HasAITask.Valid {
13588+
return build.HasAITask.Bool, nil
13589+
}
13590+
// If the build has a nil AI task, check if the job is in progress
13591+
// and if it has a non-empty ai_task_prompt parameter
13592+
job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID)
13593+
if err != nil {
13594+
return false, xerrors.Errorf("get provisioner job: %w", err)
13595+
}
13596+
if job.CompletedAt.Valid {
13597+
return false, nil
13598+
}
13599+
parameters, err := q.getWorkspaceBuildParametersNoLock(build.ID)
13600+
if err != nil {
13601+
return false, xerrors.Errorf("get workspace build parameters: %w", err)
13602+
}
13603+
for _, param := range parameters {
13604+
if param.Name == "ai_task_prompt" && param.Value != "" {
13605+
return true, nil
13606+
}
13607+
}
13608+
return false, nil
13609+
}()
13610+
if err != nil {
13611+
return nil, xerrors.Errorf("get hasAITask: %w", err)
13612+
}
13613+
if hasAITask != arg.HasAITask.Bool {
13614+
continue
13615+
}
13616+
}
13617+
1356513618
// If the filter exists, ensure the object is authorized.
1356613619
if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil {
1356713620
continue

coderd/rbac/regosql/compile_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ internal.member_2(input.object.org_owner, {"3bf82434-e40b-44ae-b3d8-d0115bba9bad
236236
neq(input.object.owner, "");
237237
"806dd721-775f-4c85-9ce3-63fbbd975954" = input.object.owner`,
238238
},
239-
ExpectedSQL: p(p("organization_id :: text != ''") + " AND " +
240-
p("organization_id :: text = ANY(ARRAY ['3bf82434-e40b-44ae-b3d8-d0115bba9bad','5630fda3-26ab-462c-9014-a88a62d7a415','c304877a-bc0d-4e9b-9623-a38eae412929'])") + " AND " +
239+
ExpectedSQL: p(p("t.organization_id :: text != ''") + " AND " +
240+
p("t.organization_id :: text = ANY(ARRAY ['3bf82434-e40b-44ae-b3d8-d0115bba9bad','5630fda3-26ab-462c-9014-a88a62d7a415','c304877a-bc0d-4e9b-9623-a38eae412929'])") + " AND " +
241241
p("false") + " AND " +
242242
p("false")),
243243
VariableConverter: regosql.TemplateConverter(),

coderd/rbac/regosql/configs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func userACLMatcher(m sqltypes.VariableMatcher) sqltypes.VariableMatcher {
2525
func TemplateConverter() *sqltypes.VariableConverter {
2626
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
2727
resourceIDMatcher(),
28-
organizationOwnerMatcher(),
28+
sqltypes.StringVarMatcher("t.organization_id :: text", []string{"input", "object", "org_owner"}),
2929
// Templates have no user owner, only owner by an organization.
3030
sqltypes.AlwaysFalse(userOwnerMatcher()),
3131
)

coderd/searchquery/search.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
146146
// which will return all workspaces.
147147
Valid: values.Has("outdated"),
148148
}
149+
filter.HasAITask = parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task")
149150
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
150151

151152
type paramMatch struct {
@@ -206,6 +207,7 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G
206207
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
207208
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
208209
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
210+
HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"),
209211
}
210212

211213
parser.ErrorExcessParams(values)

coderd/searchquery/search_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,36 @@ func TestSearchWorkspace(t *testing.T) {
222222
OrganizationID: uuid.MustParse("08eb6715-02f8-45c5-b86d-03786fcfbb4e"),
223223
},
224224
},
225+
{
226+
Name: "HasAITaskTrue",
227+
Query: "has-ai-task:true",
228+
Expected: database.GetWorkspacesParams{
229+
HasAITask: sql.NullBool{
230+
Bool: true,
231+
Valid: true,
232+
},
233+
},
234+
},
235+
{
236+
Name: "HasAITaskFalse",
237+
Query: "has-ai-task:false",
238+
Expected: database.GetWorkspacesParams{
239+
HasAITask: sql.NullBool{
240+
Bool: false,
241+
Valid: true,
242+
},
243+
},
244+
},
245+
{
246+
Name: "HasAITaskMissing",
247+
Query: "",
248+
Expected: database.GetWorkspacesParams{
249+
HasAITask: sql.NullBool{
250+
Bool: false,
251+
Valid: false,
252+
},
253+
},
254+
},
225255

226256
// Failures
227257
{
@@ -559,6 +589,36 @@ func TestSearchTemplates(t *testing.T) {
559589
FuzzyName: "foobar",
560590
},
561591
},
592+
{
593+
Name: "HasAITaskTrue",
594+
Query: "has-ai-task:true",
595+
Expected: database.GetTemplatesWithFilterParams{
596+
HasAITask: sql.NullBool{
597+
Bool: true,
598+
Valid: true,
599+
},
600+
},
601+
},
602+
{
603+
Name: "HasAITaskFalse",
604+
Query: "has-ai-task:false",
605+
Expected: database.GetTemplatesWithFilterParams{
606+
HasAITask: sql.NullBool{
607+
Bool: false,
608+
Valid: true,
609+
},
610+
},
611+
},
612+
{
613+
Name: "HasAITaskMissing",
614+
Query: "",
615+
Expected: database.GetTemplatesWithFilterParams{
616+
HasAITask: sql.NullBool{
617+
Bool: false,
618+
Valid: false,
619+
},
620+
},
621+
},
562622
}
563623

564624
for _, c := range testCases {

coderd/templates_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd_test
22

33
import (
44
"context"
5+
"database/sql"
56
"net/http"
67
"sync/atomic"
78
"testing"
@@ -16,6 +17,7 @@ import (
1617
"github.com/coder/coder/v2/coderd/coderdtest"
1718
"github.com/coder/coder/v2/coderd/database"
1819
"github.com/coder/coder/v2/coderd/database/dbauthz"
20+
"github.com/coder/coder/v2/coderd/database/dbgen"
1921
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2022
"github.com/coder/coder/v2/coderd/database/dbtime"
2123
"github.com/coder/coder/v2/coderd/notifications"
@@ -1809,3 +1811,66 @@ func TestTemplateNotifications(t *testing.T) {
18091811
})
18101812
})
18111813
}
1814+
1815+
func TestTemplateFilterHasAITask(t *testing.T) {
1816+
t.Parallel()
1817+
1818+
db, pubsub := dbtestutil.NewDB(t)
1819+
client := coderdtest.New(t, &coderdtest.Options{
1820+
Database: db,
1821+
Pubsub: pubsub,
1822+
IncludeProvisionerDaemon: true,
1823+
})
1824+
user := coderdtest.CreateFirstUser(t, client)
1825+
1826+
jobWithAITask := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1827+
OrganizationID: user.OrganizationID,
1828+
InitiatorID: user.UserID,
1829+
Tags: database.StringMap{},
1830+
Type: database.ProvisionerJobTypeTemplateVersionImport,
1831+
})
1832+
jobWithoutAITask := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1833+
OrganizationID: user.OrganizationID,
1834+
InitiatorID: user.UserID,
1835+
Tags: database.StringMap{},
1836+
Type: database.ProvisionerJobTypeTemplateVersionImport,
1837+
})
1838+
versionWithAITask := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1839+
OrganizationID: user.OrganizationID,
1840+
CreatedBy: user.UserID,
1841+
HasAITask: sql.NullBool{Bool: true, Valid: true},
1842+
JobID: jobWithAITask.ID,
1843+
})
1844+
versionWithoutAITask := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1845+
OrganizationID: user.OrganizationID,
1846+
CreatedBy: user.UserID,
1847+
HasAITask: sql.NullBool{Bool: false, Valid: true},
1848+
JobID: jobWithoutAITask.ID,
1849+
})
1850+
templateWithAITask := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithAITask.ID)
1851+
templateWithoutAITask := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithoutAITask.ID)
1852+
1853+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1854+
defer cancel()
1855+
1856+
// Test filtering
1857+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
1858+
SearchQuery: "has-ai-task:true",
1859+
})
1860+
require.NoError(t, err)
1861+
require.Len(t, templates, 1)
1862+
require.Equal(t, templateWithAITask.ID, templates[0].ID)
1863+
1864+
templates, err = client.Templates(ctx, codersdk.TemplateFilter{
1865+
SearchQuery: "has-ai-task:false",
1866+
})
1867+
require.NoError(t, err)
1868+
require.Len(t, templates, 1)
1869+
require.Equal(t, templateWithoutAITask.ID, templates[0].ID)
1870+
1871+
templates, err = client.Templates(ctx, codersdk.TemplateFilter{})
1872+
require.NoError(t, err)
1873+
require.Len(t, templates, 2)
1874+
require.Contains(t, templates, templateWithAITask)
1875+
require.Contains(t, templates, templateWithoutAITask)
1876+
}

coderd/workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
136136
// @Security CoderSessionToken
137137
// @Produce json
138138
// @Tags Workspaces
139-
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before."
139+
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task."
140140
// @Param limit query int false "Page limit"
141141
// @Param offset query int false "Page offset"
142142
// @Success 200 {object} codersdk.WorkspacesResponse

0 commit comments

Comments
 (0)