Skip to content

Commit b39becb

Browse files
authored
feat(site): add a provisioner warning to workspace builds (coder#15686)
This PR adds warnings about provisioner health to workspace build pages. It closes coder#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)
1 parent 104898a commit b39becb

27 files changed

+825
-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
@@ -1568,6 +1568,10 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get
15681568
return q.db.GetDeploymentWorkspaceStats(ctx)
15691569
}
15701570

1571+
func (q *querier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
1572+
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetEligibleProvisionerDaemonsByProvisionerJobIDs)(ctx, provisionerJobIds)
1573+
}
1574+
15711575
func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
15721576
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg)
15731577
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,29 @@ func (s *MethodTestSuite) TestExtraMethods() {
21192119
s.NoError(err, "get provisioner daemon by org")
21202120
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
21212121
}))
2122+
s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) {
2123+
org := dbgen.Organization(s.T(), db, database.Organization{})
2124+
tags := database.StringMap(map[string]string{
2125+
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
2126+
})
2127+
j, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
2128+
OrganizationID: org.ID,
2129+
Type: database.ProvisionerJobTypeWorkspaceBuild,
2130+
Tags: tags,
2131+
Provisioner: database.ProvisionerTypeEcho,
2132+
StorageMethod: database.ProvisionerStorageMethodFile,
2133+
})
2134+
s.NoError(err, "insert provisioner job")
2135+
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
2136+
OrganizationID: org.ID,
2137+
Tags: tags,
2138+
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
2139+
})
2140+
s.NoError(err, "insert provisioner daemon")
2141+
ds, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{j.ID})
2142+
s.NoError(err, "get provisioner daemon by org")
2143+
check.Args(uuid.UUIDs{j.ID}).Asserts(d, policy.ActionRead).Returns(ds)
2144+
}))
21222145
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
21232146
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
21242147
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
@@ -503,6 +503,46 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
503503
return groupMember
504504
}
505505

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

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

2784+
func (q *FakeQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(_ context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
2785+
q.mutex.RLock()
2786+
defer q.mutex.RUnlock()
2787+
2788+
results := make([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, 0)
2789+
seen := make(map[string]struct{}) // Track unique combinations
2790+
2791+
for _, jobID := range provisionerJobIds {
2792+
var job database.ProvisionerJob
2793+
found := false
2794+
for _, j := range q.provisionerJobs {
2795+
if j.ID == jobID {
2796+
job = j
2797+
found = true
2798+
break
2799+
}
2800+
}
2801+
if !found {
2802+
continue
2803+
}
2804+
2805+
for _, daemon := range q.provisionerDaemons {
2806+
if daemon.OrganizationID != job.OrganizationID {
2807+
continue
2808+
}
2809+
2810+
if !tagsSubset(job.Tags, daemon.Tags) {
2811+
continue
2812+
}
2813+
2814+
provisionerMatches := false
2815+
for _, p := range daemon.Provisioners {
2816+
if p == job.Provisioner {
2817+
provisionerMatches = true
2818+
break
2819+
}
2820+
}
2821+
if !provisionerMatches {
2822+
continue
2823+
}
2824+
2825+
key := jobID.String() + "-" + daemon.ID.String()
2826+
if _, exists := seen[key]; exists {
2827+
continue
2828+
}
2829+
seen[key] = struct{}{}
2830+
2831+
results = append(results, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
2832+
JobID: jobID,
2833+
ProvisionerDaemon: daemon,
2834+
})
2835+
}
2836+
}
2837+
2838+
return results, nil
2839+
}
2840+
27762841
func (q *FakeQuerier) GetExternalAuthLink(_ context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
27772842
if err := validateDatabaseType(arg); err != nil {
27782843
return database.ExternalAuthLink{}, err
@@ -10344,25 +10409,26 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err
1034410409
}
1034510410

1034610411
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
10347-
err := validateDatabaseType(arg)
10348-
if err != nil {
10412+
if err := validateDatabaseType(arg); err != nil {
1034910413
return database.ProvisionerDaemon{}, err
1035010414
}
1035110415

1035210416
q.mutex.Lock()
1035310417
defer q.mutex.Unlock()
10354-
for _, d := range q.provisionerDaemons {
10355-
if d.Name == arg.Name {
10356-
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" {
10357-
continue
10358-
}
10359-
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] {
10360-
continue
10361-
}
10418+
10419+
// Look for existing daemon using the same composite key as SQL
10420+
for i, d := range q.provisionerDaemons {
10421+
if d.OrganizationID == arg.OrganizationID &&
10422+
d.Name == arg.Name &&
10423+
getOwnerFromTags(d.Tags) == getOwnerFromTags(arg.Tags) {
1036210424
d.Provisioners = arg.Provisioners
1036310425
d.Tags = maps.Clone(arg.Tags)
10364-
d.Version = arg.Version
1036510426
d.LastSeenAt = arg.LastSeenAt
10427+
d.Version = arg.Version
10428+
d.APIVersion = arg.APIVersion
10429+
d.OrganizationID = arg.OrganizationID
10430+
d.KeyID = arg.KeyID
10431+
q.provisionerDaemons[i] = d
1036610432
return d, nil
1036710433
}
1036810434
}
@@ -10372,7 +10438,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
1037210438
Name: arg.Name,
1037310439
Provisioners: arg.Provisioners,
1037410440
Tags: maps.Clone(arg.Tags),
10375-
ReplicaID: uuid.NullUUID{},
1037610441
LastSeenAt: arg.LastSeenAt,
1037710442
Version: arg.Version,
1037810443
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
@@ -269,6 +269,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object {
269269
InOrg(p.OrganizationID)
270270
}
271271

272+
func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object {
273+
return p.ProvisionerDaemon.RBACObject()
274+
}
275+
272276
func (p ProvisionerKey) RBACObject() rbac.Object {
273277
return rbac.ResourceProvisionerKeys.
274278
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