Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd17dd5d

Browse files
authored
feat: add filtering by initiator to provisioner job listing in the CLI (#20137)
Relates tocoder/internal#934This PR provides a mechanism to filter provisioner jobs according to whoinitiated the job.This will be used to find pending prebuild jobs when prebuilds haveoverwhelmed the provisioner job queue. They can then be canceled.If prebuilds are overwhelming provisioners, the following steps will betaken:```bash# pause prebuild reconciliation to limit provisioner queue pollution:coder prebuilds pause # cancel pending provisioner jobs to clear the queuecoder provisioner jobs list --initiator="prebuilds" --status="pending" | jq ... | xargs -n1 -I{} coder provisioner jobs cancel {}# push a fixed template and wait for the import to completecoder templates push ... # push a fixed template# resume prebuild reconciliationcoder prebuilds resume```This interface differs somewhat from what was specified in the issue,but still provides a mechanism that addresses the issue. The originalproposal was made by myself and this simpler implementation makes sense.I might add a `--search` parameter in a follow-up if there is appetitefor it.Potential follow ups:* Support for this usage: `coder provisioner jobs list --search"initiator:prebuilds status:pending"`* Adding the same parameters to `coder provisioner jobs cancel` as aconvenience feature so that operators don't have to pipe through `jq`and `xargs`
1 parentc1357d4 commitd17dd5d

23 files changed

+395
-39
lines changed

‎cli/provisionerjobs.go‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
4343
cliui.TableFormat([]provisionerJobRow{}, []string{"created at","id","type","template display name","status","queue","tags"}),
4444
cliui.JSONFormat(),
4545
)
46-
status []string
47-
limitint64
46+
status []string
47+
limitint64
48+
initiatorstring
4849
)
4950

5051
cmd:=&serpent.Command{
@@ -65,9 +66,20 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
6566
returnxerrors.Errorf("current organization: %w",err)
6667
}
6768

69+
varinitiatorID*uuid.UUID
70+
71+
ifinitiator!="" {
72+
user,err:=client.User(ctx,initiator)
73+
iferr!=nil {
74+
returnxerrors.Errorf("initiator not found: %s",initiator)
75+
}
76+
initiatorID=&user.ID
77+
}
78+
6879
jobs,err:=client.OrganizationProvisionerJobs(ctx,org.ID,&codersdk.OrganizationProvisionerJobsOptions{
69-
Status: slice.StringEnums[codersdk.ProvisionerJobStatus](status),
70-
Limit:int(limit),
80+
Status: slice.StringEnums[codersdk.ProvisionerJobStatus](status),
81+
Limit:int(limit),
82+
InitiatorID:initiatorID,
7183
})
7284
iferr!=nil {
7385
returnxerrors.Errorf("list provisioner jobs: %w",err)
@@ -122,6 +134,13 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
122134
Default:"50",
123135
Value:serpent.Int64Of(&limit),
124136
},
137+
{
138+
Flag:"initiator",
139+
FlagShorthand:"i",
140+
Env:"CODER_PROVISIONER_JOB_LIST_INITIATOR",
141+
Description:"Filter by initiator (user ID or username).",
142+
Value:serpent.StringOf(&initiator),
143+
},
125144
}...)
126145

127146
orgContext.AttachOptions(cmd)

‎cli/provisionerjobs_test.go‎

Lines changed: 168 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"encoding/json"
77
"fmt"
8+
"strings"
89
"testing"
910
"time"
1011

@@ -26,33 +27,32 @@ import (
2627
funcTestProvisionerJobs(t*testing.T) {
2728
t.Parallel()
2829

29-
db,ps:=dbtestutil.NewDB(t)
30-
client,_,coderdAPI:=coderdtest.NewWithAPI(t,&coderdtest.Options{
31-
IncludeProvisionerDaemon:false,
32-
Database:db,
33-
Pubsub:ps,
34-
})
35-
owner:=coderdtest.CreateFirstUser(t,client)
36-
templateAdminClient,templateAdmin:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID,rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
37-
memberClient,member:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID)
38-
39-
// These CLI tests are related to provisioner job CRUD operations and as such
40-
// do not require the overhead of starting a provisioner. Other provisioner job
41-
// functionalities (acquisition etc.) are tested elsewhere.
42-
template:=dbgen.Template(t,db, database.Template{
43-
OrganizationID:owner.OrganizationID,
44-
CreatedBy:owner.UserID,
45-
AllowUserCancelWorkspaceJobs:true,
46-
})
47-
version:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
48-
OrganizationID:owner.OrganizationID,
49-
CreatedBy:owner.UserID,
50-
TemplateID: uuid.NullUUID{UUID:template.ID,Valid:true},
51-
})
52-
5330
t.Run("Cancel",func(t*testing.T) {
5431
t.Parallel()
5532

33+
db,ps:=dbtestutil.NewDB(t)
34+
client,_,coderdAPI:=coderdtest.NewWithAPI(t,&coderdtest.Options{
35+
IncludeProvisionerDaemon:false,
36+
Database:db,
37+
Pubsub:ps,
38+
})
39+
owner:=coderdtest.CreateFirstUser(t,client)
40+
templateAdminClient,templateAdmin:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID,rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
41+
memberClient,member:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID)
42+
43+
// These CLI tests are related to provisioner job CRUD operations and as such
44+
// do not require the overhead of starting a provisioner. Other provisioner job
45+
// functionalities (acquisition etc.) are tested elsewhere.
46+
template:=dbgen.Template(t,db, database.Template{
47+
OrganizationID:owner.OrganizationID,
48+
CreatedBy:owner.UserID,
49+
AllowUserCancelWorkspaceJobs:true,
50+
})
51+
version:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
52+
OrganizationID:owner.OrganizationID,
53+
CreatedBy:owner.UserID,
54+
TemplateID: uuid.NullUUID{UUID:template.ID,Valid:true},
55+
})
5656
// Test helper to create a provisioner job of a given type with a given input.
5757
prepareJob:=func(t*testing.T,jobType database.ProvisionerJobType,input json.RawMessage) database.ProvisionerJob {
5858
t.Helper()
@@ -178,4 +178,148 @@ func TestProvisionerJobs(t *testing.T) {
178178
})
179179
}
180180
})
181+
182+
t.Run("List",func(t*testing.T) {
183+
t.Parallel()
184+
185+
db,ps:=dbtestutil.NewDB(t)
186+
client,_,coderdAPI:=coderdtest.NewWithAPI(t,&coderdtest.Options{
187+
IncludeProvisionerDaemon:false,
188+
Database:db,
189+
Pubsub:ps,
190+
})
191+
owner:=coderdtest.CreateFirstUser(t,client)
192+
_,member:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID)
193+
194+
// These CLI tests are related to provisioner job CRUD operations and as such
195+
// do not require the overhead of starting a provisioner. Other provisioner job
196+
// functionalities (acquisition etc.) are tested elsewhere.
197+
template:=dbgen.Template(t,db, database.Template{
198+
OrganizationID:owner.OrganizationID,
199+
CreatedBy:owner.UserID,
200+
AllowUserCancelWorkspaceJobs:true,
201+
})
202+
version:=dbgen.TemplateVersion(t,db, database.TemplateVersion{
203+
OrganizationID:owner.OrganizationID,
204+
CreatedBy:owner.UserID,
205+
TemplateID: uuid.NullUUID{UUID:template.ID,Valid:true},
206+
})
207+
// Create some test jobs
208+
job1:=dbgen.ProvisionerJob(t,db,coderdAPI.Pubsub, database.ProvisionerJob{
209+
OrganizationID:owner.OrganizationID,
210+
InitiatorID:owner.UserID,
211+
Type:database.ProvisionerJobTypeTemplateVersionImport,
212+
Input: []byte(`{"template_version_id":"`+version.ID.String()+`"}`),
213+
Tags: database.StringMap{provisionersdk.TagScope:provisionersdk.ScopeOrganization},
214+
})
215+
216+
job2:=dbgen.ProvisionerJob(t,db,coderdAPI.Pubsub, database.ProvisionerJob{
217+
OrganizationID:owner.OrganizationID,
218+
InitiatorID:member.ID,
219+
Type:database.ProvisionerJobTypeWorkspaceBuild,
220+
Input: []byte(`{"workspace_build_id":"`+uuid.New().String()+`"}`),
221+
Tags: database.StringMap{provisionersdk.TagScope:provisionersdk.ScopeOrganization},
222+
})
223+
// Test basic list command
224+
t.Run("Basic",func(t*testing.T) {
225+
t.Parallel()
226+
227+
inv,root:=clitest.New(t,"provisioner","jobs","list")
228+
clitest.SetupConfig(t,client,root)
229+
varbuf bytes.Buffer
230+
inv.Stdout=&buf
231+
err:=inv.Run()
232+
require.NoError(t,err)
233+
234+
// Should contain both jobs
235+
output:=buf.String()
236+
assert.Contains(t,output,job1.ID.String())
237+
assert.Contains(t,output,job2.ID.String())
238+
})
239+
240+
// Test list with JSON output
241+
t.Run("JSON",func(t*testing.T) {
242+
t.Parallel()
243+
244+
inv,root:=clitest.New(t,"provisioner","jobs","list","--output","json")
245+
clitest.SetupConfig(t,client,root)
246+
varbuf bytes.Buffer
247+
inv.Stdout=&buf
248+
err:=inv.Run()
249+
require.NoError(t,err)
250+
251+
// Parse JSON output
252+
varjobs []codersdk.ProvisionerJob
253+
err=json.Unmarshal(buf.Bytes(),&jobs)
254+
require.NoError(t,err)
255+
256+
// Should contain both jobs
257+
jobIDs:=make([]uuid.UUID,len(jobs))
258+
fori,job:=rangejobs {
259+
jobIDs[i]=job.ID
260+
}
261+
assert.Contains(t,jobIDs,job1.ID)
262+
assert.Contains(t,jobIDs,job2.ID)
263+
})
264+
265+
// Test list with limit
266+
t.Run("Limit",func(t*testing.T) {
267+
t.Parallel()
268+
269+
inv,root:=clitest.New(t,"provisioner","jobs","list","--limit","1")
270+
clitest.SetupConfig(t,client,root)
271+
varbuf bytes.Buffer
272+
inv.Stdout=&buf
273+
err:=inv.Run()
274+
require.NoError(t,err)
275+
276+
// Should contain at most 1 job
277+
output:=buf.String()
278+
jobCount:=0
279+
ifstrings.Contains(output,job1.ID.String()) {
280+
jobCount++
281+
}
282+
ifstrings.Contains(output,job2.ID.String()) {
283+
jobCount++
284+
}
285+
assert.LessOrEqual(t,jobCount,1)
286+
})
287+
288+
// Test list with initiator filter
289+
t.Run("InitiatorFilter",func(t*testing.T) {
290+
t.Parallel()
291+
292+
// Get owner user details to access username
293+
ctx:=testutil.Context(t,testutil.WaitShort)
294+
ownerUser,err:=client.User(ctx,owner.UserID.String())
295+
require.NoError(t,err)
296+
297+
// Test filtering by initiator (using username)
298+
inv,root:=clitest.New(t,"provisioner","jobs","list","--initiator",ownerUser.Username)
299+
clitest.SetupConfig(t,client,root)
300+
varbuf bytes.Buffer
301+
inv.Stdout=&buf
302+
err=inv.Run()
303+
require.NoError(t,err)
304+
305+
// Should only contain job1 (initiated by owner)
306+
output:=buf.String()
307+
assert.Contains(t,output,job1.ID.String())
308+
assert.NotContains(t,output,job2.ID.String())
309+
})
310+
311+
// Test list with invalid user
312+
t.Run("InvalidUser",func(t*testing.T) {
313+
t.Parallel()
314+
315+
// Test with non-existent user
316+
inv,root:=clitest.New(t,"provisioner","jobs","list","--initiator","nonexistent-user")
317+
clitest.SetupConfig(t,client,root)
318+
varbuf bytes.Buffer
319+
inv.Stdout=&buf
320+
err:=inv.Run()
321+
require.Error(t,err)
322+
assert.Contains(t,err.Error(),"initiator not found: nonexistent-user")
323+
})
324+
})
181325
}

‎cli/testdata/coder_list_--output_json.golden‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"queue_position": 0,
4646
"queue_size": 0,
4747
"organization_id": "===========[first org ID]===========",
48+
"initiator_id": "==========[first user ID]===========",
4849
"input": {
4950
"workspace_build_id": "========[workspace build ID]========"
5051
},

‎cli/testdata/coder_provisioner_jobs_list_--help.golden‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ OPTIONS:
1111
-O, --org string, $CODER_ORGANIZATION
1212
Select which organization (uuid or name) to use.
1313

14-
-c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|worker name|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|logs overflowed|organization|queue] (default: created at,id,type,template display name,status,queue,tags)
14+
-c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|worker name|file id|tags|queue position|queue size|organization id|initiator id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|logs overflowed|organization|queue] (default: created at,id,type,template display name,status,queue,tags)
1515
Columns to display in table output.
1616

17+
-i, --initiator string, $CODER_PROVISIONER_JOB_LIST_INITIATOR
18+
Filter by initiator (user ID or username).
19+
1720
-l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50)
1821
Limit the number of jobs returned.
1922

‎cli/testdata/coder_provisioner_jobs_list_--output_json.golden‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"queue_position": 0,
1616
"queue_size": 0,
1717
"organization_id": "===========[first org ID]===========",
18+
"initiator_id": "==========[first user ID]===========",
1819
"input": {
1920
"template_version_id": "============[version ID]============"
2021
},
@@ -45,6 +46,7 @@
4546
"queue_position": 0,
4647
"queue_size": 0,
4748
"organization_id": "===========[first org ID]===========",
49+
"initiator_id": "==========[first user ID]===========",
4850
"input": {
4951
"workspace_build_id": "========[workspace build ID]========"
5052
},

‎coderd/apidoc/docs.go‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/dbauthz/dbauthz_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2484,10 +2484,12 @@ func (s *MethodTestSuite) TestExtraMethods() {
24842484

24852485
ds,err:=db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(context.Background(), database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{
24862486
OrganizationID:org.ID,
2487+
InitiatorID:uuid.Nil,
24872488
})
24882489
s.NoError(err,"get provisioner jobs by org")
24892490
check.Args(database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{
24902491
OrganizationID:org.ID,
2492+
InitiatorID:uuid.Nil,
24912493
}).Asserts(j1,policy.ActionRead,j2,policy.ActionRead).Returns(ds)
24922494
}))
24932495
}

‎coderd/database/queries.sql.go‎

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp