Skip to content

Commit 95713c5

Browse files
committed
feat: add max-age option to provisioners list command
1 parent d55026e commit 95713c5

File tree

13 files changed

+176
-20
lines changed

13 files changed

+176
-20
lines changed

cli/provisioners.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"time"
56

67
"golang.org/x/xerrors"
78

@@ -43,6 +44,7 @@ func (r *RootCmd) provisionerList() *serpent.Command {
4344
limit int64
4445
offline bool
4546
status []string
47+
maxAge time.Duration
4648
)
4749

4850
cmd := &serpent.Command{
@@ -65,6 +67,7 @@ func (r *RootCmd) provisionerList() *serpent.Command {
6567
Limit: int(limit),
6668
Offline: offline,
6769
Status: slice.StringEnums[codersdk.ProvisionerDaemonStatus](status),
70+
MaxAge: maxAge,
6871
})
6972
if err != nil {
7073
return xerrors.Errorf("list provisioner daemons: %w", err)
@@ -104,10 +107,11 @@ func (r *RootCmd) provisionerList() *serpent.Command {
104107
Value: serpent.Int64Of(&limit),
105108
},
106109
{
107-
Flag: "show-offline",
108-
Env: "CODER_PROVISIONER_SHOW_OFFLINE",
109-
Description: "Show offline provisioners.",
110-
Value: serpent.BoolOf(&offline),
110+
Flag: "show-offline",
111+
FlagShorthand: "f",
112+
Env: "CODER_PROVISIONER_SHOW_OFFLINE",
113+
Description: "Show offline provisioners.",
114+
Value: serpent.BoolOf(&offline),
111115
},
112116
{
113117
Flag: "status",
@@ -116,6 +120,13 @@ func (r *RootCmd) provisionerList() *serpent.Command {
116120
Description: "Filter by provisioner status.",
117121
Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerDaemonStatusEnums())...),
118122
},
123+
{
124+
Flag: "max-age",
125+
FlagShorthand: "m",
126+
Env: "CODER_PROVISIONER_LIST_MAX_AGE",
127+
Description: "Filter provisioners by maximum age.",
128+
Value: serpent.DurationOf(&maxAge),
129+
},
119130
}...)
120131

121132
orgContext.AttachOptions(cmd)

cli/provisioners_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ func TestProvisioners_Golden(t *testing.T) {
249249
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
250250
})
251251

252+
t.Run("list provisioner daemons by max age", func(t *testing.T) {
253+
t.Parallel()
254+
255+
var got bytes.Buffer
256+
inv, root := clitest.New(t,
257+
"provisioners",
258+
"list",
259+
"--max-age=1h",
260+
)
261+
inv.Stdout = &got
262+
clitest.SetupConfig(t, templateAdminClient, root)
263+
err := inv.Run()
264+
require.NoError(t, err)
265+
266+
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
267+
})
268+
252269
// Test jobs list with template admin as members are currently
253270
// unable to access provisioner jobs. In the future (with RBAC
254271
// 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]

cli/testdata/coder_provisioner_list_--help.golden

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ OPTIONS:
1717
-l, --limit int, $CODER_PROVISIONER_LIST_LIMIT (default: 50)
1818
Limit the number of provisioners returned.
1919

20+
-m, --max-age duration, $CODER_PROVISIONER_LIST_MAX_AGE
21+
Filter provisioners by maximum age.
22+
2023
-o, --output table|json (default: table)
2124
Output format.
2225

23-
--show-offline bool, $CODER_PROVISIONER_SHOW_OFFLINE
26+
-f, --show-offline bool, $CODER_PROVISIONER_SHOW_OFFLINE
2427
Show offline provisioners.
2528

2629
-s, --status [offline|idle|busy], $CODER_PROVISIONER_LIST_STATUS

coderd/database/querier_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,73 @@ func TestGetProvisionerDaemonsWithStatusByOrganization(t *testing.T) {
635635
})
636636
}
637637
})
638+
639+
t.Run("FilterByMaxAge", func(t *testing.T) {
640+
t.Parallel()
641+
db, _ := dbtestutil.NewDB(t)
642+
org := dbgen.Organization(t, db, database.Organization{})
643+
644+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
645+
Name: "foo-daemon",
646+
OrganizationID: org.ID,
647+
CreatedAt: dbtime.Now().Add(-(45 * time.Minute)),
648+
LastSeenAt: sql.NullTime{
649+
Valid: true,
650+
Time: dbtime.Now().Add(-(45 * time.Minute)),
651+
},
652+
})
653+
654+
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
655+
Name: "bar-daemon",
656+
OrganizationID: org.ID,
657+
CreatedAt: dbtime.Now().Add(-(25 * time.Minute)),
658+
LastSeenAt: sql.NullTime{
659+
Valid: true,
660+
Time: dbtime.Now().Add(-(25 * time.Minute)),
661+
},
662+
})
663+
664+
type testCase struct {
665+
name string
666+
maxAge sql.NullInt64
667+
expectedNum int
668+
}
669+
670+
tests := []testCase{
671+
{
672+
name: "Max age 1 hour",
673+
maxAge: sql.NullInt64{Int64: time.Hour.Milliseconds(), Valid: true},
674+
expectedNum: 2,
675+
},
676+
{
677+
name: "Max age 30 minutes",
678+
maxAge: sql.NullInt64{Int64: (30 * time.Minute).Milliseconds(), Valid: true},
679+
expectedNum: 1,
680+
},
681+
{
682+
name: "Max age 15 minutes",
683+
maxAge: sql.NullInt64{Int64: (15 * time.Minute).Milliseconds(), Valid: true},
684+
expectedNum: 0,
685+
},
686+
{
687+
name: "No max age",
688+
maxAge: sql.NullInt64{Valid: false},
689+
expectedNum: 2,
690+
},
691+
}
692+
for _, tc := range tests {
693+
//nolint:tparallel,paralleltest
694+
t.Run(tc.name, func(t *testing.T) {
695+
daemons, err := db.GetProvisionerDaemonsWithStatusByOrganization(context.Background(), database.GetProvisionerDaemonsWithStatusByOrganizationParams{
696+
OrganizationID: org.ID,
697+
StaleIntervalMS: 60 * time.Minute.Milliseconds(),
698+
MaxAgeMs: tc.maxAge,
699+
})
700+
require.NoError(t, err)
701+
require.Len(t, daemons, tc.expectedNum)
702+
})
703+
}
704+
})
638705
}
639706

640707
func TestGetWorkspaceAgentUsageStats(t *testing.T) {

coderd/database/queries.sql.go

Lines changed: 17 additions & 7 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: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +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 max age if provided
113114
AND (
114-
-- Include daemons that have been seen recently
115+
sqlc.narg('max_age_ms')::bigint IS NULL
116+
OR pd.last_seen_at IS NULL
117+
OR pd.last_seen_at >= (NOW() - (sqlc.narg('max_age_ms')::bigint || ' ms')::interval)
118+
)
119+
AND (
120+
-- Always include online daemons
115121
(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
116-
-- Include offline daemons only when offline param is set OR 'offline' is in the list of statuses
122+
-- Include offline daemons if offline param is true or 'offline' status is requested
117123
OR (
118124
(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
119125
AND (
@@ -122,15 +128,17 @@ WHERE
122128
)
123129
)
124130
)
125-
-- Filter daemons by their current status if statuses are provided
126131
AND (
132+
-- Filter daemons by any statuses if provided
127133
COALESCE(array_length(@statuses::provisioner_daemon_status[], 1), 0) = 0
134+
OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
135+
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
128136
OR (
129-
(current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
130-
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
137+
'offline'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[])
138+
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
131139
)
132140
OR (
133-
(COALESCE(sqlc.narg('offline')::bool, false) = true OR 'offline'::provisioner_daemon_status = ANY(@statuses::provisioner_daemon_status[]))
141+
COALESCE(sqlc.narg('offline')::bool, false) = true
134142
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval))
135143
)
136144
)

coderd/httpapi/queryparams.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,23 @@ func (p *QueryParamParser) ProvisionerDaemonStatuses(vals url.Values, def []code
293293
})
294294
}
295295

296+
func (p *QueryParamParser) Duration(vals url.Values, def time.Duration, queryParam string) time.Duration {
297+
v, err := parseQueryParam(p, vals, func(v string) (time.Duration, error) {
298+
d, err := time.ParseDuration(v)
299+
if err != nil {
300+
return 0, err
301+
}
302+
return d, nil
303+
}, def, queryParam)
304+
if err != nil {
305+
p.Errors = append(p.Errors, codersdk.ValidationError{
306+
Field: queryParam,
307+
Detail: fmt.Sprintf("Query param %q must be a valid duration (e.g., '24h', '30m', '1h30m'): %s", queryParam, err.Error()),
308+
})
309+
}
310+
return v
311+
}
312+
296313
// ValidEnum represents an enum that can be parsed and validated.
297314
type ValidEnum interface {
298315
// Add more types as needed (avoid importing large dependency trees).

coderd/provisionerdaemons.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
4848
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
4949
includeOffline := p.NullableBoolean(qp, sql.NullBool{}, "offline")
5050
statuses := p.ProvisionerDaemonStatuses(qp, []codersdk.ProvisionerDaemonStatus{}, "status")
51+
maxAge := p.Duration(qp, 0, "max_age")
5152
p.ErrorExcessParams(qp)
5253
if len(p.Errors) > 0 {
5354
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -67,6 +68,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
6768
Limit: sql.NullInt32{Int32: limit, Valid: limit > 0},
6869
Offline: includeOffline,
6970
Statuses: dbStatuses,
71+
MaxAgeMs: sql.NullInt64{Int64: maxAge.Milliseconds(), Valid: maxAge > 0},
7072
IDs: ids,
7173
Tags: tags,
7274
},

codersdk/organizations.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ type OrganizationProvisionerDaemonsOptions struct {
347347
Limit int
348348
Offline bool
349349
Status []ProvisionerDaemonStatus
350+
MaxAge time.Duration
350351
IDs []uuid.UUID
351352
Tags map[string]string
352353
}
@@ -363,6 +364,9 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio
363364
if len(opts.Status) > 0 {
364365
qp.Add("status", joinSlice(opts.Status))
365366
}
367+
if opts.MaxAge > 0 {
368+
qp.Add("max_age", opts.MaxAge.String())
369+
}
366370
if len(opts.IDs) > 0 {
367371
qp.Add("ids", joinSliceStringer(opts.IDs))
368372
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy