Skip to content

Commit fe50c08

Browse files
committed
feat: add status option to provisioners list command
1 parent 65e074e commit fe50c08

18 files changed

+249
-20
lines changed

cli/provisioners.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"golang.org/x/xerrors"
77

88
"github.com/coder/coder/v2/cli/cliui"
9+
"github.com/coder/coder/v2/coderd/util/slice"
910
"github.com/coder/coder/v2/codersdk"
1011
"github.com/coder/serpent"
1112
)
@@ -41,6 +42,7 @@ func (r *RootCmd) provisionerList() *serpent.Command {
4142
)
4243
limit int64
4344
offline bool
45+
status []string
4446
)
4547

4648
cmd := &serpent.Command{
@@ -62,6 +64,7 @@ func (r *RootCmd) provisionerList() *serpent.Command {
6264
daemons, err := client.OrganizationProvisionerDaemons(ctx, org.ID, &codersdk.OrganizationProvisionerDaemonsOptions{
6365
Limit: int(limit),
6466
Offline: offline,
67+
Status: slice.StringEnums[codersdk.ProvisionerDaemonStatus](status),
6568
})
6669
if err != nil {
6770
return xerrors.Errorf("list provisioner daemons: %w", err)
@@ -102,9 +105,17 @@ func (r *RootCmd) provisionerList() *serpent.Command {
102105
},
103106
{
104107
Flag: "show-offline",
108+
Env: "CODER_PROVISIONER_SHOW_OFFLINE",
105109
Description: "Show offline provisioners.",
106110
Value: serpent.BoolOf(&offline),
107111
},
112+
{
113+
Flag: "status",
114+
FlagShorthand: "s",
115+
Env: "CODER_PROVISIONER_LIST_STATUS",
116+
Description: "Filter by provisioner status.",
117+
Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerDaemonStatusEnums())...),
118+
},
108119
}...)
109120

110121
orgContext.AttachOptions(cmd)

cli/provisioners_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func TestProvisioners_Golden(t *testing.T) {
198198
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
199199
})
200200

201-
t.Run("list with offline provisioners", func(t *testing.T) {
201+
t.Run("list with offline provisioner daemons", func(t *testing.T) {
202202
t.Parallel()
203203

204204
var got bytes.Buffer
@@ -215,6 +215,23 @@ func TestProvisioners_Golden(t *testing.T) {
215215
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
216216
})
217217

218+
t.Run("list provisioner daemons by status", func(t *testing.T) {
219+
t.Parallel()
220+
221+
var got bytes.Buffer
222+
inv, root := clitest.New(t,
223+
"provisioners",
224+
"list",
225+
"--status=idle,offline,busy",
226+
)
227+
inv.Stdout = &got
228+
clitest.SetupConfig(t, templateAdminClient, root)
229+
err := inv.Run()
230+
require.NoError(t, err)
231+
232+
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
233+
})
234+
218235
// Test jobs list with template admin as members are currently
219236
// unable to access provisioner jobs. In the future (with RBAC
220237
// changes), we may allow them to view _their_ jobs.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
2+
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
3+
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
4+
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
2+
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
3+
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
4+
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
2+
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
3+
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
4+
====[timestamp]===== ====[timestamp]===== built-in provisioner-2 v0.0.0 offline map[owner: scope:organization]
5+
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]

cli/testdata/coder_provisioner_list_--help.golden

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ OPTIONS:
2020
-o, --output table|json (default: table)
2121
Output format.
2222

23-
--show-offline bool
23+
--show-offline bool, $CODER_PROVISIONER_SHOW_OFFLINE
2424
Show offline provisioners.
2525

26+
-s, --status [offline|idle|busy], $CODER_PROVISIONER_LIST_STATUS
27+
Filter by provisioner status.
28+
2629
———
2730
Run `coder --help` for a list of global options.

coderd/database/querier_test.go

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -479,12 +479,12 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
479479
require.Equal(t, database.ProvisionerDaemonStatusIdle, daemons[1].Status)
480480
})
481481

482-
t.Run("Excludes offline daemons", func(t *testing.T) {
482+
t.Run("ExcludeOffline", func(t *testing.T) {
483483
t.Parallel()
484484
db, _ := dbtestutil.NewDB(t)
485485
org := dbgen.Organization(t, db, database.Organization{})
486486

487-
_ = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
487+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
488488
Name: "offline-daemon",
489489
OrganizationID: org.ID,
490490
CreatedAt: dbtime.Now().Add(-time.Hour),
@@ -514,12 +514,12 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
514514
require.Equal(t, database.ProvisionerDaemonStatusIdle, daemons[0].Status)
515515
})
516516

517-
t.Run("Includes offline daemons", func(t *testing.T) {
517+
t.Run("IncludeOffline", func(t *testing.T) {
518518
t.Parallel()
519519
db, _ := dbtestutil.NewDB(t)
520520
org := dbgen.Organization(t, db, database.Organization{})
521521

522-
_ = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
522+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
523523
Name: "offline-daemon",
524524
OrganizationID: org.ID,
525525
CreatedAt: dbtime.Now().Add(-time.Hour),
@@ -528,14 +528,14 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
528528
Time: dbtime.Now().Add(-time.Hour),
529529
},
530530
})
531-
_ = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
531+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
532532
Name: "foo-daemon",
533533
OrganizationID: org.ID,
534534
Tags: database.StringMap{
535535
"foo": "bar",
536536
},
537537
})
538-
_ = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
538+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
539539
Name: "bar-daemon",
540540
OrganizationID: org.ID,
541541
CreatedAt: dbtime.Now().Add(-(30 * time.Minute)),
@@ -561,6 +561,80 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
561561
require.Equal(t, 2, statusCounts[database.ProvisionerDaemonStatusIdle])
562562
require.Equal(t, 1, statusCounts[database.ProvisionerDaemonStatusOffline])
563563
})
564+
565+
t.Run("MatchesStatuses", func(t *testing.T) {
566+
t.Parallel()
567+
db, _ := dbtestutil.NewDB(t)
568+
org := dbgen.Organization(t, db, database.Organization{})
569+
570+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
571+
Name: "offline-daemon",
572+
OrganizationID: org.ID,
573+
CreatedAt: dbtime.Now().Add(-time.Hour),
574+
LastSeenAt: sql.NullTime{
575+
Valid: true,
576+
Time: dbtime.Now().Add(-time.Hour),
577+
},
578+
})
579+
580+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
581+
Name: "foo-daemon",
582+
OrganizationID: org.ID,
583+
CreatedAt: dbtime.Now().Add(-(30 * time.Minute)),
584+
LastSeenAt: sql.NullTime{
585+
Valid: true,
586+
Time: dbtime.Now().Add(-(30 * time.Minute)),
587+
},
588+
})
589+
590+
type testCase struct {
591+
name string
592+
statuses []database.ProvisionerDaemonStatus
593+
expectedNum int
594+
}
595+
596+
tests := []testCase{
597+
{
598+
name: "Get idle and offline",
599+
statuses: []database.ProvisionerDaemonStatus{
600+
database.ProvisionerDaemonStatusOffline,
601+
database.ProvisionerDaemonStatusIdle,
602+
},
603+
expectedNum: 2,
604+
},
605+
{
606+
name: "Get offline",
607+
statuses: []database.ProvisionerDaemonStatus{
608+
database.ProvisionerDaemonStatusOffline,
609+
},
610+
expectedNum: 1,
611+
},
612+
{
613+
name: "Get all - empty statuses",
614+
statuses: []database.ProvisionerDaemonStatus{},
615+
expectedNum: 2,
616+
},
617+
{
618+
name: "Get all - nil statuses",
619+
statuses: nil,
620+
expectedNum: 2,
621+
},
622+
}
623+
624+
for _, tc := range tests {
625+
//nolint:tparallel,paralleltest
626+
t.Run(tc.name, func(t *testing.T) {
627+
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
628+
OrganizationID: org.ID,
629+
StaleIntervalMS: 45 * time.Minute.Milliseconds(),
630+
Offline: sql.NullBool{Bool: true, Valid: true},
631+
Statuses: tc.statuses,
632+
})
633+
require.NoError(t, err)
634+
require.Len(t, daemons, tc.expectedNum)
635+
})
636+
}
637+
})
564638
}
565639

566640
func TestGetWorkspaceAgentUsageStats(t *testing.T) {

coderd/database/queries.sql.go

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

coderd/database/queries/provisionerdaemons.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ WHERE
110110
pd.organization_id = @organization_id::uuid
111111
AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pd.id = ANY(@ids::uuid[]))
112112
AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, @tags::tagset))
113+
-- Filter by status array if any status values are provided
114+
AND (COALESCE(array_length(@statuses::provisioner_daemon_status[], 1), 0) = 0 OR
115+
(CASE
116+
WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval)
117+
THEN 'offline'::provisioner_daemon_status
118+
ELSE CASE
119+
WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
120+
ELSE 'idle'::provisioner_daemon_status
121+
END
122+
END) = ANY(@statuses::provisioner_daemon_status[]))
113123
-- Include offline daemons only if offline is set to true
114124
AND (
115125
COALESCE(sqlc.narg('offline')::bool, false) = true

coderd/database/sdk2db/sdk2db.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Package sdk2db provides common conversion routines from codersdk types to database types
2+
package sdk2db
3+
4+
import (
5+
"github.com/coder/coder/v2/coderd/database"
6+
"github.com/coder/coder/v2/coderd/database/db2sdk"
7+
"github.com/coder/coder/v2/codersdk"
8+
)
9+
10+
func ProvisionerDaemonStatus(status codersdk.ProvisionerDaemonStatus) database.ProvisionerDaemonStatus {
11+
return database.ProvisionerDaemonStatus(status)
12+
}
13+
14+
func ProvisionerDaemonStatuses(params []codersdk.ProvisionerDaemonStatus) []database.ProvisionerDaemonStatus {
15+
return db2sdk.List(params, ProvisionerDaemonStatus)
16+
}

0 commit comments

Comments
 (0)