mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add filtering options to provisioners list (#19378)
## Summary In this pull request we're adding support for additional filtering options to the `provisioners list` CLI command and the `/provisionerdaemons` API endpoint. Resolves: https://github.com/coder/coder/issues/18783 ### Changes #### Added CLI Options - `--show-offline`: When this option is provided, all provisioner daemons will be returned. This means that when `--show-offline` is not provided only `idle` and `busy` provisioner daemons will be returned. - `--status=<list_of_statuses>`: When this option is provided with a comma-separated list of valid statuses (`idle`, `busy`, or `offline`) only provisioner daemons that have these statuses will be returned. - `--max-age=<duration>`: When this option is provided with a valid duration value (e.g., `24h`, `30s`) only provisioner daemons with a `last_seen_at` timestamp within the provided max age will be returned. #### Query Params - `?offline=true`: Include offline provisioner daemons in the results. Offline provisioner daemons will be excluded if `?offline=false` or if offline is not provided. - `?status=<list_of_statuses>`: Include provisioner daemons with the specified statuses. - `?max_age=<duration>`: Include provisioner daemons with a `last_seen_at` timestamp within the max age duration. #### Frontend - Since offline provisioners will not be returned by default anymore (`--show-offline` has to be provided to see them), a checkbox was added to the provisioners list page to allow for offline provisioners to be displayed - A revamp of the provisioners page will be done in: https://github.com/coder/coder/issues/17156, this checkbox change was just added to maintain currently functionality with the backend updates Current provisioners page (without checkbox) <img width="1329" height="574" alt="Screenshot 2025-08-20 at 10 51 00 AM" src="https://github.com/user-attachments/assets/77b73650-0b62-44f0-a77f-acbe5710809f" /> Provisioners page with checkbox (unchecked) <img width="1314" height="626" alt="Screenshot 2025-08-20 at 10 48 40 AM" src="https://github.com/user-attachments/assets/7ba164ad-6d3f-417b-bd39-338c0161b145" /> Provisioner page with checkbox (checked) and URL updated with query parameters <img width="1306" height="597" alt="Screenshot 2025-08-20 at 10 50 14 AM" src="https://github.com/user-attachments/assets/e78d0986-bbf8-491b-9d56-b682973237a0" /> ### Show Offline vs Offline Status To list offline provisioner daemons, users can either: 1. Include the `--show-offline` option OR 2. Include `offline` in the list of values provided to the `--status` option
This commit is contained in:
+31
-2
@@ -2,10 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -39,7 +41,10 @@ func (r *RootCmd) provisionerList() *serpent.Command {
|
||||
cliui.TableFormat([]provisionerDaemonRow{}, []string{"created at", "last seen at", "key name", "name", "version", "status", "tags"}),
|
||||
cliui.JSONFormat(),
|
||||
)
|
||||
limit int64
|
||||
limit int64
|
||||
offline bool
|
||||
status []string
|
||||
maxAge time.Duration
|
||||
)
|
||||
|
||||
cmd := &serpent.Command{
|
||||
@@ -59,7 +64,10 @@ func (r *RootCmd) provisionerList() *serpent.Command {
|
||||
}
|
||||
|
||||
daemons, err := client.OrganizationProvisionerDaemons(ctx, org.ID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Limit: int(limit),
|
||||
Limit: int(limit),
|
||||
Offline: offline,
|
||||
Status: slice.StringEnums[codersdk.ProvisionerDaemonStatus](status),
|
||||
MaxAge: maxAge,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("list provisioner daemons: %w", err)
|
||||
@@ -98,6 +106,27 @@ func (r *RootCmd) provisionerList() *serpent.Command {
|
||||
Default: "50",
|
||||
Value: serpent.Int64Of(&limit),
|
||||
},
|
||||
{
|
||||
Flag: "show-offline",
|
||||
FlagShorthand: "f",
|
||||
Env: "CODER_PROVISIONER_SHOW_OFFLINE",
|
||||
Description: "Show offline provisioners.",
|
||||
Value: serpent.BoolOf(&offline),
|
||||
},
|
||||
{
|
||||
Flag: "status",
|
||||
FlagShorthand: "s",
|
||||
Env: "CODER_PROVISIONER_LIST_STATUS",
|
||||
Description: "Filter by provisioner status.",
|
||||
Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerDaemonStatusEnums())...),
|
||||
},
|
||||
{
|
||||
Flag: "max-age",
|
||||
FlagShorthand: "m",
|
||||
Env: "CODER_PROVISIONER_LIST_MAX_AGE",
|
||||
Description: "Filter provisioners by maximum age.",
|
||||
Value: serpent.DurationOf(&maxAge),
|
||||
},
|
||||
}...)
|
||||
|
||||
orgContext.AttachOptions(cmd)
|
||||
|
||||
@@ -197,6 +197,74 @@ func TestProvisioners_Golden(t *testing.T) {
|
||||
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
|
||||
})
|
||||
|
||||
t.Run("list with offline provisioner daemons", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var got bytes.Buffer
|
||||
inv, root := clitest.New(t,
|
||||
"provisioners",
|
||||
"list",
|
||||
"--show-offline",
|
||||
)
|
||||
inv.Stdout = &got
|
||||
clitest.SetupConfig(t, templateAdminClient, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
|
||||
})
|
||||
|
||||
t.Run("list provisioner daemons by status", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var got bytes.Buffer
|
||||
inv, root := clitest.New(t,
|
||||
"provisioners",
|
||||
"list",
|
||||
"--status=idle,offline,busy",
|
||||
)
|
||||
inv.Stdout = &got
|
||||
clitest.SetupConfig(t, templateAdminClient, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
|
||||
})
|
||||
|
||||
t.Run("list provisioner daemons without offline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var got bytes.Buffer
|
||||
inv, root := clitest.New(t,
|
||||
"provisioners",
|
||||
"list",
|
||||
"--status=idle,busy",
|
||||
)
|
||||
inv.Stdout = &got
|
||||
clitest.SetupConfig(t, templateAdminClient, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
|
||||
})
|
||||
|
||||
t.Run("list provisioner daemons by max age", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var got bytes.Buffer
|
||||
inv, root := clitest.New(t,
|
||||
"provisioners",
|
||||
"list",
|
||||
"--max-age=1h",
|
||||
)
|
||||
inv.Stdout = &got
|
||||
clitest.SetupConfig(t, templateAdminClient, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
|
||||
})
|
||||
|
||||
// Test jobs list with template admin as members are currently
|
||||
// unable to access provisioner jobs. In the future (with RBAC
|
||||
// changes), we may allow them to view _their_ jobs.
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
|
||||
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== default-provisioner v0.0.0-devel map[owner: scope:organization] built-in idle <nil> <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder
|
||||
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 map[foo:bar owner: scope:organization] built-in busy 00000000-0000-0000-bbbb-000000000002 running <nil> <nil> Coder
|
||||
00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 map[owner: scope:organization] built-in offline <nil> <nil> 00000000-0000-0000-bbbb-000000000003 succeeded Coder
|
||||
00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 map[owner: scope:organization] built-in idle <nil> <nil> <nil> <nil> Coder
|
||||
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
|
||||
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== default-provisioner v0.0.0-devel map[owner: scope:organization] built-in idle <nil> <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder
|
||||
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 map[foo:bar owner: scope:organization] built-in busy 00000000-0000-0000-bbbb-000000000002 running <nil> <nil> Coder
|
||||
00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 map[owner: scope:organization] built-in idle <nil> <nil> <nil> <nil> Coder
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
|
||||
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
|
||||
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-2 v0.0.0 offline map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
|
||||
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
|
||||
====[timestamp]===== ====[timestamp]===== built-in default-provisioner v0.0.0-devel idle map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-1 v0.0.0 busy map[foo:bar owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-2 v0.0.0 offline map[owner: scope:organization]
|
||||
====[timestamp]===== ====[timestamp]===== built-in provisioner-3 v0.0.0 idle map[owner: scope:organization]
|
||||
@@ -17,8 +17,17 @@ OPTIONS:
|
||||
-l, --limit int, $CODER_PROVISIONER_LIST_LIMIT (default: 50)
|
||||
Limit the number of provisioners returned.
|
||||
|
||||
-m, --max-age duration, $CODER_PROVISIONER_LIST_MAX_AGE
|
||||
Filter provisioners by maximum age.
|
||||
|
||||
-o, --output table|json (default: table)
|
||||
Output format.
|
||||
|
||||
-f, --show-offline bool, $CODER_PROVISIONER_SHOW_OFFLINE
|
||||
Show offline provisioners.
|
||||
|
||||
-s, --status [offline|idle|busy], $CODER_PROVISIONER_LIST_STATUS
|
||||
Filter by provisioner status.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
|
||||
@@ -397,6 +397,7 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
IDs: []uuid.UUID{matchingDaemon0.ID, matchingDaemon1.ID},
|
||||
Offline: sql.NullBool{Bool: true, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 2)
|
||||
@@ -430,6 +431,7 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
Tags: database.StringMap{"foo": "bar"},
|
||||
Offline: sql.NullBool{Bool: true, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
@@ -463,6 +465,7 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 45 * time.Minute.Milliseconds(),
|
||||
Offline: sql.NullBool{Bool: true, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 2)
|
||||
@@ -475,6 +478,230 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
|
||||
require.Equal(t, database.ProvisionerDaemonStatusOffline, daemons[0].Status)
|
||||
require.Equal(t, database.ProvisionerDaemonStatusIdle, daemons[1].Status)
|
||||
})
|
||||
|
||||
t.Run("ExcludeOffline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "offline-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-time.Hour),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-time.Hour),
|
||||
},
|
||||
})
|
||||
fooDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "foo-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
},
|
||||
})
|
||||
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 45 * time.Minute.Milliseconds(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
|
||||
require.Equal(t, fooDaemon.ID, daemons[0].ProvisionerDaemon.ID)
|
||||
require.Equal(t, database.ProvisionerDaemonStatusIdle, daemons[0].Status)
|
||||
})
|
||||
|
||||
t.Run("IncludeOffline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "offline-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-time.Hour),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-time.Hour),
|
||||
},
|
||||
})
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "foo-daemon",
|
||||
OrganizationID: org.ID,
|
||||
Tags: database.StringMap{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "bar-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
},
|
||||
})
|
||||
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 45 * time.Minute.Milliseconds(),
|
||||
Offline: sql.NullBool{Bool: true, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 3)
|
||||
|
||||
statusCounts := make(map[database.ProvisionerDaemonStatus]int)
|
||||
for _, daemon := range daemons {
|
||||
statusCounts[daemon.Status]++
|
||||
}
|
||||
|
||||
require.Equal(t, 2, statusCounts[database.ProvisionerDaemonStatusIdle])
|
||||
require.Equal(t, 1, statusCounts[database.ProvisionerDaemonStatusOffline])
|
||||
})
|
||||
|
||||
t.Run("MatchesStatuses", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "offline-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-time.Hour),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-time.Hour),
|
||||
},
|
||||
})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "foo-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-(30 * time.Minute)),
|
||||
},
|
||||
})
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
statuses []database.ProvisionerDaemonStatus
|
||||
expectedNum int
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "Get idle and offline",
|
||||
statuses: []database.ProvisionerDaemonStatus{
|
||||
database.ProvisionerDaemonStatusOffline,
|
||||
database.ProvisionerDaemonStatusIdle,
|
||||
},
|
||||
expectedNum: 2,
|
||||
},
|
||||
{
|
||||
name: "Get offline",
|
||||
statuses: []database.ProvisionerDaemonStatus{
|
||||
database.ProvisionerDaemonStatusOffline,
|
||||
},
|
||||
expectedNum: 1,
|
||||
},
|
||||
// Offline daemons should not be included without Offline param
|
||||
{
|
||||
name: "Get idle - empty statuses",
|
||||
statuses: []database.ProvisionerDaemonStatus{},
|
||||
expectedNum: 1,
|
||||
},
|
||||
{
|
||||
name: "Get idle - nil statuses",
|
||||
statuses: nil,
|
||||
expectedNum: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
//nolint:tparallel,paralleltest
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 45 * time.Minute.Milliseconds(),
|
||||
Statuses: tc.statuses,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, tc.expectedNum)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterByMaxAge", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "foo-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-(45 * time.Minute)),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-(45 * time.Minute)),
|
||||
},
|
||||
})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "bar-daemon",
|
||||
OrganizationID: org.ID,
|
||||
CreatedAt: dbtime.Now().Add(-(25 * time.Minute)),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: dbtime.Now().Add(-(25 * time.Minute)),
|
||||
},
|
||||
})
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
maxAge sql.NullInt64
|
||||
expectedNum int
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "Max age 1 hour",
|
||||
maxAge: sql.NullInt64{Int64: time.Hour.Milliseconds(), Valid: true},
|
||||
expectedNum: 2,
|
||||
},
|
||||
{
|
||||
name: "Max age 30 minutes",
|
||||
maxAge: sql.NullInt64{Int64: (30 * time.Minute).Milliseconds(), Valid: true},
|
||||
expectedNum: 1,
|
||||
},
|
||||
{
|
||||
name: "Max age 15 minutes",
|
||||
maxAge: sql.NullInt64{Int64: (15 * time.Minute).Milliseconds(), Valid: true},
|
||||
expectedNum: 0,
|
||||
},
|
||||
{
|
||||
name: "No max age",
|
||||
maxAge: sql.NullInt64{Valid: false},
|
||||
expectedNum: 2,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
//nolint:tparallel,paralleltest
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: 60 * time.Minute.Milliseconds(),
|
||||
MaxAgeMs: tc.maxAge,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, tc.expectedNum)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWorkspaceAgentUsageStats(t *testing.T) {
|
||||
|
||||
@@ -8263,13 +8263,13 @@ const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDa
|
||||
SELECT
|
||||
pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id,
|
||||
CASE
|
||||
WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($1::bigint || ' ms')::interval)
|
||||
THEN 'offline'
|
||||
ELSE CASE
|
||||
WHEN current_job.id IS NOT NULL THEN 'busy'
|
||||
ELSE 'idle'
|
||||
END
|
||||
END::provisioner_daemon_status AS status,
|
||||
WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
|
||||
WHEN (COALESCE($1::bool, false) = true
|
||||
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
||||
THEN 'offline'::provisioner_daemon_status
|
||||
ELSE 'idle'::provisioner_daemon_status
|
||||
END AS status,
|
||||
pk.name AS key_name,
|
||||
-- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them.
|
||||
current_job.id AS current_job_id,
|
||||
@@ -8336,21 +8336,56 @@ LEFT JOIN
|
||||
AND previous_template.organization_id = pd.organization_id
|
||||
)
|
||||
WHERE
|
||||
pd.organization_id = $2::uuid
|
||||
AND (COALESCE(array_length($3::uuid[], 1), 0) = 0 OR pd.id = ANY($3::uuid[]))
|
||||
AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $4::tagset))
|
||||
pd.organization_id = $4::uuid
|
||||
AND (COALESCE(array_length($5::uuid[], 1), 0) = 0 OR pd.id = ANY($5::uuid[]))
|
||||
AND ($6::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $6::tagset))
|
||||
-- Filter by max age if provided
|
||||
AND (
|
||||
$7::bigint IS NULL
|
||||
OR pd.last_seen_at IS NULL
|
||||
OR pd.last_seen_at >= (NOW() - ($7::bigint || ' ms')::interval)
|
||||
)
|
||||
AND (
|
||||
-- Always include online daemons
|
||||
(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - ($3::bigint || ' ms')::interval))
|
||||
-- Include offline daemons if offline param is true or 'offline' status is requested
|
||||
OR (
|
||||
(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
||||
AND (
|
||||
COALESCE($1::bool, false) = true
|
||||
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
|
||||
)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- Filter daemons by any statuses if provided
|
||||
COALESCE(array_length($2::provisioner_daemon_status[], 1), 0) = 0
|
||||
OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
||||
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
||||
OR (
|
||||
'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
||||
)
|
||||
OR (
|
||||
COALESCE($1::bool, false) = true
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
pd.created_at DESC
|
||||
LIMIT
|
||||
$5::int
|
||||
$8::int
|
||||
`
|
||||
|
||||
type GetProvisionerDaemonsWithStatusByOrganizationParams struct {
|
||||
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
IDs []uuid.UUID `db:"ids" json:"ids"`
|
||||
Tags StringMap `db:"tags" json:"tags"`
|
||||
Limit sql.NullInt32 `db:"limit" json:"limit"`
|
||||
Offline sql.NullBool `db:"offline" json:"offline"`
|
||||
Statuses []ProvisionerDaemonStatus `db:"statuses" json:"statuses"`
|
||||
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
IDs []uuid.UUID `db:"ids" json:"ids"`
|
||||
Tags StringMap `db:"tags" json:"tags"`
|
||||
MaxAgeMs sql.NullInt64 `db:"max_age_ms" json:"max_age_ms"`
|
||||
Limit sql.NullInt32 `db:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
type GetProvisionerDaemonsWithStatusByOrganizationRow struct {
|
||||
@@ -8373,10 +8408,13 @@ type GetProvisionerDaemonsWithStatusByOrganizationRow struct {
|
||||
// Previous job information.
|
||||
func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization,
|
||||
arg.Offline,
|
||||
pq.Array(arg.Statuses),
|
||||
arg.StaleIntervalMS,
|
||||
arg.OrganizationID,
|
||||
pq.Array(arg.IDs),
|
||||
arg.Tags,
|
||||
arg.MaxAgeMs,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,13 +32,13 @@ WHERE
|
||||
SELECT
|
||||
sqlc.embed(pd),
|
||||
CASE
|
||||
WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval)
|
||||
THEN 'offline'
|
||||
ELSE CASE
|
||||
WHEN current_job.id IS NOT NULL THEN 'busy'
|
||||
ELSE 'idle'
|
||||
END
|
||||
END::provisioner_daemon_status AS status,
|
||||
WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
|
||||
WHEN (COALESCE(sqlc.narg('offline')::bool, false) = true
|
||||
OR 'offline'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
|
||||
THEN 'offline'::provisioner_daemon_status
|
||||
ELSE 'idle'::provisioner_daemon_status
|
||||
END AS status,
|
||||
pk.name AS key_name,
|
||||
-- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them.
|
||||
current_job.id AS current_job_id,
|
||||
@@ -110,6 +110,38 @@ WHERE
|
||||
pd.organization_id = @organization_id::uuid
|
||||
AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pd.id = ANY(@ids::uuid[]))
|
||||
AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, @tags::tagset))
|
||||
-- Filter by max age if provided
|
||||
AND (
|
||||
sqlc.narg('max_age_ms')::bigint IS NULL
|
||||
OR pd.last_seen_at IS NULL
|
||||
OR pd.last_seen_at >= (NOW() - (sqlc.narg('max_age_ms')::bigint || ' ms')::interval)
|
||||
)
|
||||
AND (
|
||||
-- Always include online daemons
|
||||
(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
|
||||
-- Include offline daemons if offline param is true or 'offline' status is requested
|
||||
OR (
|
||||
(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
|
||||
AND (
|
||||
COALESCE(sqlc.narg('offline')::bool, false) = true
|
||||
OR 'offline'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[])
|
||||
)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- Filter daemons by any statuses if provided
|
||||
COALESCE(array_length(@statuses::provisioner_daemon_status[], 1), 0) = 0
|
||||
OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
|
||||
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
|
||||
OR (
|
||||
'offline'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[])
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
|
||||
)
|
||||
OR (
|
||||
COALESCE(sqlc.narg('offline')::bool, false) = true
|
||||
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
pd.created_at DESC
|
||||
LIMIT
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Package sdk2db provides common conversion routines from codersdk types to database types
|
||||
package sdk2db
|
||||
|
||||
import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func ProvisionerDaemonStatus(status codersdk.ProvisionerDaemonStatus) database.ProvisionerDaemonStatus {
|
||||
return database.ProvisionerDaemonStatus(status)
|
||||
}
|
||||
|
||||
func ProvisionerDaemonStatuses(params []codersdk.ProvisionerDaemonStatus) []database.ProvisionerDaemonStatus {
|
||||
return db2sdk.List(params, ProvisionerDaemonStatus)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package sdk2db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/sdk2db"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestProvisionerDaemonStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input codersdk.ProvisionerDaemonStatus
|
||||
expect database.ProvisionerDaemonStatus
|
||||
}{
|
||||
{"busy", codersdk.ProvisionerDaemonBusy, database.ProvisionerDaemonStatusBusy},
|
||||
{"offline", codersdk.ProvisionerDaemonOffline, database.ProvisionerDaemonStatusOffline},
|
||||
{"idle", codersdk.ProvisionerDaemonIdle, database.ProvisionerDaemonStatusIdle},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := sdk2db.ProvisionerDaemonStatus(tc.input)
|
||||
if !got.Valid() {
|
||||
t.Errorf("ProvisionerDaemonStatus(%v) returned invalid status", tc.input)
|
||||
}
|
||||
if got != tc.expect {
|
||||
t.Errorf("ProvisionerDaemonStatus(%v) = %v; want %v", tc.input, got, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -287,6 +287,29 @@ func (p *QueryParamParser) JSONStringMap(vals url.Values, def map[string]string,
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *QueryParamParser) ProvisionerDaemonStatuses(vals url.Values, def []codersdk.ProvisionerDaemonStatus, queryParam string) []codersdk.ProvisionerDaemonStatus {
|
||||
return ParseCustomList(p, vals, def, queryParam, func(v string) (codersdk.ProvisionerDaemonStatus, error) {
|
||||
return codersdk.ProvisionerDaemonStatus(v), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *QueryParamParser) Duration(vals url.Values, def time.Duration, queryParam string) time.Duration {
|
||||
v, err := parseQueryParam(p, vals, func(v string) (time.Duration, error) {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d, nil
|
||||
}, def, queryParam)
|
||||
if err != nil {
|
||||
p.Errors = append(p.Errors, codersdk.ValidationError{
|
||||
Field: queryParam,
|
||||
Detail: fmt.Sprintf("Query param %q must be a valid duration (e.g., '24h', '30m', '1h30m'): %s", queryParam, err.Error()),
|
||||
})
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ValidEnum represents an enum that can be parsed and validated.
|
||||
type ValidEnum interface {
|
||||
// Add more types as needed (avoid importing large dependency trees).
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/sdk2db"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
@@ -45,6 +46,9 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
limit := p.PositiveInt32(qp, 50, "limit")
|
||||
ids := p.UUIDs(qp, nil, "ids")
|
||||
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
|
||||
includeOffline := p.NullableBoolean(qp, sql.NullBool{}, "offline")
|
||||
statuses := p.ProvisionerDaemonStatuses(qp, []codersdk.ProvisionerDaemonStatus{}, "status")
|
||||
maxAge := p.Duration(qp, 0, "max_age")
|
||||
p.ErrorExcessParams(qp)
|
||||
if len(p.Errors) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -54,12 +58,17 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
dbStatuses := sdk2db.ProvisionerDaemonStatuses(statuses)
|
||||
|
||||
daemons, err := api.Database.GetProvisionerDaemonsWithStatusByOrganization(
|
||||
ctx,
|
||||
database.GetProvisionerDaemonsWithStatusByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
StaleIntervalMS: provisionerdserver.StaleInterval.Milliseconds(),
|
||||
Limit: sql.NullInt32{Int32: limit, Valid: limit > 0},
|
||||
Offline: includeOffline,
|
||||
Statuses: dbStatuses,
|
||||
MaxAgeMs: sql.NullInt64{Int64: maxAge.Milliseconds(), Valid: maxAge > 0},
|
||||
IDs: ids,
|
||||
Tags: tags,
|
||||
},
|
||||
|
||||
@@ -146,7 +146,9 @@ func TestProvisionerDaemons(t *testing.T) {
|
||||
t.Run("Default limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Offline: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 50)
|
||||
})
|
||||
@@ -155,7 +157,8 @@ func TestProvisionerDaemons(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd1.ID, pd2.ID},
|
||||
IDs: []uuid.UUID{pd1.ID, pd2.ID},
|
||||
Offline: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 2)
|
||||
@@ -167,7 +170,8 @@ func TestProvisionerDaemons(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
Tags: map[string]string{"count": "1"},
|
||||
Tags: map[string]string{"count": "1"},
|
||||
Offline: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
@@ -209,7 +213,8 @@ func TestProvisionerDaemons(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
||||
IDs: []uuid.UUID{pd2.ID},
|
||||
IDs: []uuid.UUID{pd2.ID},
|
||||
Offline: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
|
||||
@@ -344,9 +344,12 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e
|
||||
}
|
||||
|
||||
type OrganizationProvisionerDaemonsOptions struct {
|
||||
Limit int
|
||||
IDs []uuid.UUID
|
||||
Tags map[string]string
|
||||
Limit int
|
||||
Offline bool
|
||||
Status []ProvisionerDaemonStatus
|
||||
MaxAge time.Duration
|
||||
IDs []uuid.UUID
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerDaemonsOptions) ([]ProvisionerDaemon, error) {
|
||||
@@ -355,6 +358,15 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio
|
||||
if opts.Limit > 0 {
|
||||
qp.Add("limit", strconv.Itoa(opts.Limit))
|
||||
}
|
||||
if opts.Offline {
|
||||
qp.Add("offline", "true")
|
||||
}
|
||||
if len(opts.Status) > 0 {
|
||||
qp.Add("status", joinSlice(opts.Status))
|
||||
}
|
||||
if opts.MaxAge > 0 {
|
||||
qp.Add("max_age", opts.MaxAge.String())
|
||||
}
|
||||
if len(opts.IDs) > 0 {
|
||||
qp.Add("ids", joinSliceStringer(opts.IDs))
|
||||
}
|
||||
|
||||
@@ -49,6 +49,14 @@ const (
|
||||
ProvisionerDaemonBusy ProvisionerDaemonStatus = "busy"
|
||||
)
|
||||
|
||||
func ProvisionerDaemonStatusEnums() []ProvisionerDaemonStatus {
|
||||
return []ProvisionerDaemonStatus{
|
||||
ProvisionerDaemonOffline,
|
||||
ProvisionerDaemonIdle,
|
||||
ProvisionerDaemonBusy,
|
||||
}
|
||||
}
|
||||
|
||||
type ProvisionerDaemon struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"`
|
||||
|
||||
Generated
+27
@@ -25,6 +25,33 @@ coder provisioner list [flags]
|
||||
|
||||
Limit the number of provisioners returned.
|
||||
|
||||
### -f, --show-offline
|
||||
|
||||
| | |
|
||||
|-------------|----------------------------------------------|
|
||||
| Type | <code>bool</code> |
|
||||
| Environment | <code>$CODER_PROVISIONER_SHOW_OFFLINE</code> |
|
||||
|
||||
Show offline provisioners.
|
||||
|
||||
### -s, --status
|
||||
|
||||
| | |
|
||||
|-------------|---------------------------------------------|
|
||||
| Type | <code>[offline\|idle\|busy]</code> |
|
||||
| Environment | <code>$CODER_PROVISIONER_LIST_STATUS</code> |
|
||||
|
||||
Filter by provisioner status.
|
||||
|
||||
### -m, --max-age
|
||||
|
||||
| | |
|
||||
|-------------|----------------------------------------------|
|
||||
| Type | <code>duration</code> |
|
||||
| Environment | <code>$CODER_PROVISIONER_LIST_MAX_AGE</code> |
|
||||
|
||||
Filter provisioners by maximum age.
|
||||
|
||||
### -O, --org
|
||||
|
||||
| | |
|
||||
|
||||
@@ -17,8 +17,17 @@ OPTIONS:
|
||||
-l, --limit int, $CODER_PROVISIONER_LIST_LIMIT (default: 50)
|
||||
Limit the number of provisioners returned.
|
||||
|
||||
-m, --max-age duration, $CODER_PROVISIONER_LIST_MAX_AGE
|
||||
Filter provisioners by maximum age.
|
||||
|
||||
-o, --output table|json (default: table)
|
||||
Output format.
|
||||
|
||||
-f, --show-offline bool, $CODER_PROVISIONER_SHOW_OFFLINE
|
||||
Show offline provisioners.
|
||||
|
||||
-s, --status [offline|idle|busy], $CODER_PROVISIONER_LIST_STATUS
|
||||
Filter by provisioner status.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
|
||||
@@ -421,6 +421,8 @@ export type GetProvisionerDaemonsParams = {
|
||||
// Stringified JSON Object
|
||||
tags?: string;
|
||||
limit?: number;
|
||||
// Include offline provisioner daemons?
|
||||
offline?: boolean;
|
||||
};
|
||||
|
||||
export type TasksFilter = {
|
||||
|
||||
Generated
+3
@@ -1840,6 +1840,9 @@ export interface OrganizationMemberWithUserData extends OrganizationMember {
|
||||
// From codersdk/organizations.go
|
||||
export interface OrganizationProvisionerDaemonsOptions {
|
||||
readonly Limit: number;
|
||||
readonly Offline: boolean;
|
||||
readonly Status: readonly ProvisionerDaemonStatus[];
|
||||
readonly MaxAge: number;
|
||||
readonly IDs: readonly string[];
|
||||
readonly Tags: Record<string, string>;
|
||||
}
|
||||
|
||||
+7
-1
@@ -20,6 +20,7 @@ const OrganizationProvisionersPage: FC = () => {
|
||||
const queryParams = {
|
||||
ids: searchParams.get("ids") ?? "",
|
||||
tags: searchParams.get("tags") ?? "",
|
||||
offline: searchParams.get("offline") === "true",
|
||||
};
|
||||
const { organization, organizationPermissions } = useOrganizationSettings();
|
||||
const { entitlements } = useDashboard();
|
||||
@@ -66,7 +67,12 @@ const OrganizationProvisionersPage: FC = () => {
|
||||
buildVersion={buildInfoQuery.data?.version}
|
||||
onRetry={provisionersQuery.refetch}
|
||||
filter={queryParams}
|
||||
onFilterChange={setSearchParams}
|
||||
onFilterChange={({ ids, offline }) => {
|
||||
setSearchParams({
|
||||
ids,
|
||||
offline: offline.toString(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+16
@@ -23,9 +23,14 @@ const meta: Meta<typeof OrganizationProvisionersPageView> = {
|
||||
...MockProvisionerWithTags,
|
||||
version: "0.0.0",
|
||||
},
|
||||
{
|
||||
...MockUserProvisioner,
|
||||
status: "offline",
|
||||
},
|
||||
],
|
||||
filter: {
|
||||
ids: "",
|
||||
offline: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -69,6 +74,17 @@ export const FilterByID: Story = {
|
||||
provisioners: [MockProvisioner],
|
||||
filter: {
|
||||
ids: MockProvisioner.id,
|
||||
offline: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FilterByOffline: Story = {
|
||||
args: {
|
||||
provisioners: [MockProvisioner],
|
||||
filter: {
|
||||
ids: "",
|
||||
offline: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+77
-56
@@ -1,6 +1,7 @@
|
||||
import type { ProvisionerDaemon } from "api/typesGenerated";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Checkbox } from "components/Checkbox/Checkbox";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { SquareArrowOutUpRightIcon, XIcon } from "lucide-react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
import { LastConnectionHead } from "./LastConnectionHead";
|
||||
@@ -32,6 +33,7 @@ import { ProvisionerRow } from "./ProvisionerRow";
|
||||
|
||||
type ProvisionersFilter = {
|
||||
ids: string;
|
||||
offline: boolean;
|
||||
};
|
||||
|
||||
interface OrganizationProvisionersPageViewProps {
|
||||
@@ -102,70 +104,89 @@ export const OrganizationProvisionersPageView: FC<
|
||||
documentationLink={docs("/")}
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Key</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Tags</TableHead>
|
||||
<TableHead>
|
||||
<LastConnectionHead />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{provisioners ? (
|
||||
provisioners.length > 0 ? (
|
||||
provisioners.map((provisioner) => (
|
||||
<ProvisionerRow
|
||||
provisioner={provisioner}
|
||||
key={provisioner.id}
|
||||
buildVersion={buildVersion}
|
||||
defaultIsOpen={filter.ids.includes(provisioner.id)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Checkbox
|
||||
id="offline-filter"
|
||||
checked={filter.offline}
|
||||
onCheckedChange={(checked) => {
|
||||
onFilterChange({
|
||||
...filter,
|
||||
offline: checked === true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="offline-filter"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
Include offline provisioners
|
||||
</label>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Key</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Tags</TableHead>
|
||||
<TableHead>
|
||||
<LastConnectionHead />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{provisioners ? (
|
||||
provisioners.length > 0 ? (
|
||||
provisioners.map((provisioner) => (
|
||||
<ProvisionerRow
|
||||
provisioner={provisioner}
|
||||
key={provisioner.id}
|
||||
buildVersion={buildVersion}
|
||||
defaultIsOpen={filter.ids.includes(provisioner.id)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message="No provisioners found"
|
||||
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
|
||||
cta={
|
||||
<Button size="sm" asChild>
|
||||
<Link href={docs("/admin/provisioners")}>
|
||||
Create a provisioner
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
) : error ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message="No provisioners found"
|
||||
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
|
||||
message="Error loading the provisioner jobs"
|
||||
cta={
|
||||
<Button size="sm" asChild>
|
||||
<a href={docs("/admin/provisioners")}>
|
||||
Create a provisioner
|
||||
<SquareArrowOutUpRightIcon />
|
||||
</a>
|
||||
<Button onClick={onRetry} size="sm">
|
||||
Retry
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
) : error ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message="Error loading the provisioner jobs"
|
||||
cta={
|
||||
<Button onClick={onRetry} size="sm">
|
||||
Retry
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<Loader />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<Loader />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user