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)
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