Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b39becb

Browse files
authored
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)
1 parent 104898a commit b39becb

27 files changed

+825
-99
lines changed

cli/testdata/coder_list_--output_json.golden

+6-1
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

+4
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

+23
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

+40
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

+77-12
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

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/modelmethods.go

+4
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

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)