Skip to content

Commit ac75ebc

Browse files
committed
feat(site): add a provisioner warning to workspace builds (#15686)
This PR adds warnings about provisioner health to workspace build pages. It closes #15048 ![image](https://github.com/user-attachments/assets/fa54d0e8-c51f-427a-8f66-7e5dbbc9baca) ![image](https://github.com/user-attachments/assets/b5169669-ab05-43d5-8553-315a3099b4fd) (cherry picked from commit b39becb)
1 parent 4578e6b commit ac75ebc

27 files changed

+827
-99
lines changed

cli/testdata/coder_list_--output_json.golden

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@
5050
"deadline": "[timestamp]",
5151
"max_deadline": null,
5252
"status": "running",
53-
"daily_cost": 0
53+
"daily_cost": 0,
54+
"matched_provisioners": {
55+
"count": 0,
56+
"available": 0,
57+
"most_recently_seen": null
58+
}
5459
},
5560
"outdated": false,
5661
"name": "test-workspace",

coderd/database/dbauthz/dbauthz.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,10 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get
15611561
return q.db.GetDeploymentWorkspaceStats(ctx)
15621562
}
15631563

1564+
func (q *querier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
1565+
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetEligibleProvisionerDaemonsByProvisionerJobIDs)(ctx, provisionerJobIds)
1566+
}
1567+
15641568
func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
15651569
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg)
15661570
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,29 @@ func (s *MethodTestSuite) TestExtraMethods() {
21002100
s.NoError(err, "get provisioner daemon by org")
21012101
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
21022102
}))
2103+
s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) {
2104+
org := dbgen.Organization(s.T(), db, database.Organization{})
2105+
tags := database.StringMap(map[string]string{
2106+
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
2107+
})
2108+
j, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
2109+
OrganizationID: org.ID,
2110+
Type: database.ProvisionerJobTypeWorkspaceBuild,
2111+
Tags: tags,
2112+
Provisioner: database.ProvisionerTypeEcho,
2113+
StorageMethod: database.ProvisionerStorageMethodFile,
2114+
})
2115+
s.NoError(err, "insert provisioner job")
2116+
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
2117+
OrganizationID: org.ID,
2118+
Tags: tags,
2119+
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
2120+
})
2121+
s.NoError(err, "insert provisioner daemon")
2122+
ds, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{j.ID})
2123+
s.NoError(err, "get provisioner daemon by org")
2124+
check.Args(uuid.UUIDs{j.ID}).Asserts(d, policy.ActionRead).Returns(ds)
2125+
}))
21032126
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
21042127
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
21052128
Tags: database.StringMap(map[string]string{

coderd/database/dbgen/dbgen.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,46 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
502502
return groupMember
503503
}
504504

505+
// ProvisionerDaemon creates a provisioner daemon as far as the database is concerned. It does not run a provisioner daemon.
506+
// If no key is provided, it will create one.
507+
func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.ProvisionerDaemon) database.ProvisionerDaemon {
508+
t.Helper()
509+
510+
if daemon.KeyID == uuid.Nil {
511+
key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{
512+
ID: uuid.New(),
513+
Name: daemon.Name + "-key",
514+
OrganizationID: daemon.OrganizationID,
515+
HashedSecret: []byte("secret"),
516+
CreatedAt: dbtime.Now(),
517+
Tags: daemon.Tags,
518+
})
519+
require.NoError(t, err)
520+
daemon.KeyID = key.ID
521+
}
522+
523+
if daemon.CreatedAt.IsZero() {
524+
daemon.CreatedAt = dbtime.Now()
525+
}
526+
if daemon.Name == "" {
527+
daemon.Name = "test-daemon"
528+
}
529+
530+
d, err := db.UpsertProvisionerDaemon(genCtx, database.UpsertProvisionerDaemonParams{
531+
Name: daemon.Name,
532+
OrganizationID: daemon.OrganizationID,
533+
CreatedAt: daemon.CreatedAt,
534+
Provisioners: daemon.Provisioners,
535+
Tags: daemon.Tags,
536+
KeyID: daemon.KeyID,
537+
LastSeenAt: daemon.LastSeenAt,
538+
Version: daemon.Version,
539+
APIVersion: daemon.APIVersion,
540+
})
541+
require.NoError(t, err)
542+
return d
543+
}
544+
505545
// ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps
506546
// can be set to nil if you are SURE that you don't require a provisionerdaemon to acquire the job in your test.
507547
func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig database.ProvisionerJob) database.ProvisionerJob {

coderd/database/dbmem/dbmem.go

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,14 @@ func (q *FakeQuerier) getWorkspaceAgentScriptsByAgentIDsNoLock(ids []uuid.UUID)
11191119
return scripts, nil
11201120
}
11211121

1122+
// getOwnerFromTags returns the lowercase owner from tags, matching SQL's COALESCE(tags ->> 'owner', ”)
1123+
func getOwnerFromTags(tags map[string]string) string {
1124+
if owner, ok := tags["owner"]; ok {
1125+
return strings.ToLower(owner)
1126+
}
1127+
return ""
1128+
}
1129+
11221130
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
11231131
return xerrors.New("AcquireLock must only be called within a transaction")
11241132
}
@@ -2743,6 +2751,63 @@ func (q *FakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database
27432751
return stat, nil
27442752
}
27452753

2754+
func (q *FakeQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(_ context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
2755+
q.mutex.RLock()
2756+
defer q.mutex.RUnlock()
2757+
2758+
results := make([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, 0)
2759+
seen := make(map[string]struct{}) // Track unique combinations
2760+
2761+
for _, jobID := range provisionerJobIds {
2762+
var job database.ProvisionerJob
2763+
found := false
2764+
for _, j := range q.provisionerJobs {
2765+
if j.ID == jobID {
2766+
job = j
2767+
found = true
2768+
break
2769+
}
2770+
}
2771+
if !found {
2772+
continue
2773+
}
2774+
2775+
for _, daemon := range q.provisionerDaemons {
2776+
if daemon.OrganizationID != job.OrganizationID {
2777+
continue
2778+
}
2779+
2780+
if !tagsSubset(job.Tags, daemon.Tags) {
2781+
continue
2782+
}
2783+
2784+
provisionerMatches := false
2785+
for _, p := range daemon.Provisioners {
2786+
if p == job.Provisioner {
2787+
provisionerMatches = true
2788+
break
2789+
}
2790+
}
2791+
if !provisionerMatches {
2792+
continue
2793+
}
2794+
2795+
key := jobID.String() + "-" + daemon.ID.String()
2796+
if _, exists := seen[key]; exists {
2797+
continue
2798+
}
2799+
seen[key] = struct{}{}
2800+
2801+
results = append(results, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
2802+
JobID: jobID,
2803+
ProvisionerDaemon: daemon,
2804+
})
2805+
}
2806+
}
2807+
2808+
return results, nil
2809+
}
2810+
27462811
func (q *FakeQuerier) GetExternalAuthLink(_ context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
27472812
if err := validateDatabaseType(arg); err != nil {
27482813
return database.ExternalAuthLink{}, err
@@ -10249,25 +10314,26 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err
1024910314
}
1025010315

1025110316
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
10252-
err := validateDatabaseType(arg)
10253-
if err != nil {
10317+
if err := validateDatabaseType(arg); err != nil {
1025410318
return database.ProvisionerDaemon{}, err
1025510319
}
1025610320

1025710321
q.mutex.Lock()
1025810322
defer q.mutex.Unlock()
10259-
for _, d := range q.provisionerDaemons {
10260-
if d.Name == arg.Name {
10261-
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" {
10262-
continue
10263-
}
10264-
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] {
10265-
continue
10266-
}
10323+
10324+
// Look for existing daemon using the same composite key as SQL
10325+
for i, d := range q.provisionerDaemons {
10326+
if d.OrganizationID == arg.OrganizationID &&
10327+
d.Name == arg.Name &&
10328+
getOwnerFromTags(d.Tags) == getOwnerFromTags(arg.Tags) {
1026710329
d.Provisioners = arg.Provisioners
1026810330
d.Tags = maps.Clone(arg.Tags)
10269-
d.Version = arg.Version
1027010331
d.LastSeenAt = arg.LastSeenAt
10332+
d.Version = arg.Version
10333+
d.APIVersion = arg.APIVersion
10334+
d.OrganizationID = arg.OrganizationID
10335+
d.KeyID = arg.KeyID
10336+
q.provisionerDaemons[i] = d
1027110337
return d, nil
1027210338
}
1027310339
}
@@ -10277,7 +10343,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
1027710343
Name: arg.Name,
1027810344
Provisioners: arg.Provisioners,
1027910345
Tags: maps.Clone(arg.Tags),
10280-
ReplicaID: uuid.NullUUID{},
1028110346
LastSeenAt: arg.LastSeenAt,
1028210347
Version: arg.Version,
1028310348
APIVersion: arg.APIVersion,

coderd/database/dbmetrics/querymetrics.go

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

coderd/database/dbmock/dbmock.go

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

coderd/database/modelmethods.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object {
268268
InOrg(p.OrganizationID)
269269
}
270270

271+
func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object {
272+
return p.ProvisionerDaemon.RBACObject()
273+
}
274+
271275
func (p ProvisionerKey) RBACObject() rbac.Object {
272276
return rbac.ResourceProvisionerKeys.
273277
WithID(p.ID).

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)