mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(site): add a provisioner warning to workspace builds (#15686)
This PR adds warnings about provisioner health to workspace build pages. It closes https://github.com/coder/coder/issues/15048  
This commit is contained in:
+6
-1
@@ -50,7 +50,12 @@
|
||||
"deadline": "[timestamp]",
|
||||
"max_deadline": null,
|
||||
"status": "running",
|
||||
"daily_cost": 0
|
||||
"daily_cost": 0,
|
||||
"matched_provisioners": {
|
||||
"count": 0,
|
||||
"available": 0,
|
||||
"most_recently_seen": null
|
||||
}
|
||||
},
|
||||
"outdated": false,
|
||||
"name": "test-workspace",
|
||||
|
||||
@@ -1568,6 +1568,10 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get
|
||||
return q.db.GetDeploymentWorkspaceStats(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetEligibleProvisionerDaemonsByProvisionerJobIDs)(ctx, provisionerJobIds)
|
||||
}
|
||||
|
||||
func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg)
|
||||
}
|
||||
|
||||
@@ -2119,6 +2119,29 @@ func (s *MethodTestSuite) TestExtraMethods() {
|
||||
s.NoError(err, "get provisioner daemon by org")
|
||||
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
|
||||
}))
|
||||
s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) {
|
||||
org := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
tags := database.StringMap(map[string]string{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
})
|
||||
j, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Tags: tags,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
})
|
||||
s.NoError(err, "insert provisioner job")
|
||||
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
||||
OrganizationID: org.ID,
|
||||
Tags: tags,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
})
|
||||
s.NoError(err, "insert provisioner daemon")
|
||||
ds, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{j.ID})
|
||||
s.NoError(err, "get provisioner daemon by org")
|
||||
check.Args(uuid.UUIDs{j.ID}).Asserts(d, policy.ActionRead).Returns(ds)
|
||||
}))
|
||||
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
|
||||
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
||||
Tags: database.StringMap(map[string]string{
|
||||
|
||||
@@ -503,6 +503,46 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
|
||||
return groupMember
|
||||
}
|
||||
|
||||
// ProvisionerDaemon creates a provisioner daemon as far as the database is concerned. It does not run a provisioner daemon.
|
||||
// If no key is provided, it will create one.
|
||||
func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.ProvisionerDaemon) database.ProvisionerDaemon {
|
||||
t.Helper()
|
||||
|
||||
if daemon.KeyID == uuid.Nil {
|
||||
key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{
|
||||
ID: uuid.New(),
|
||||
Name: daemon.Name + "-key",
|
||||
OrganizationID: daemon.OrganizationID,
|
||||
HashedSecret: []byte("secret"),
|
||||
CreatedAt: dbtime.Now(),
|
||||
Tags: daemon.Tags,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
daemon.KeyID = key.ID
|
||||
}
|
||||
|
||||
if daemon.CreatedAt.IsZero() {
|
||||
daemon.CreatedAt = dbtime.Now()
|
||||
}
|
||||
if daemon.Name == "" {
|
||||
daemon.Name = "test-daemon"
|
||||
}
|
||||
|
||||
d, err := db.UpsertProvisionerDaemon(genCtx, database.UpsertProvisionerDaemonParams{
|
||||
Name: daemon.Name,
|
||||
OrganizationID: daemon.OrganizationID,
|
||||
CreatedAt: daemon.CreatedAt,
|
||||
Provisioners: daemon.Provisioners,
|
||||
Tags: daemon.Tags,
|
||||
KeyID: daemon.KeyID,
|
||||
LastSeenAt: daemon.LastSeenAt,
|
||||
Version: daemon.Version,
|
||||
APIVersion: daemon.APIVersion,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return d
|
||||
}
|
||||
|
||||
// ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps
|
||||
// can be set to nil if you are SURE that you don't require a provisionerdaemon to acquire the job in your test.
|
||||
func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig database.ProvisionerJob) database.ProvisionerJob {
|
||||
|
||||
@@ -1120,6 +1120,14 @@ func (q *FakeQuerier) getWorkspaceAgentScriptsByAgentIDsNoLock(ids []uuid.UUID)
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// getOwnerFromTags returns the lowercase owner from tags, matching SQL's COALESCE(tags ->> 'owner', ”)
|
||||
func getOwnerFromTags(tags map[string]string) string {
|
||||
if owner, ok := tags["owner"]; ok {
|
||||
return strings.ToLower(owner)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
|
||||
return xerrors.New("AcquireLock must only be called within a transaction")
|
||||
}
|
||||
@@ -2773,6 +2781,63 @@ func (q *FakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(_ context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
results := make([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, 0)
|
||||
seen := make(map[string]struct{}) // Track unique combinations
|
||||
|
||||
for _, jobID := range provisionerJobIds {
|
||||
var job database.ProvisionerJob
|
||||
found := false
|
||||
for _, j := range q.provisionerJobs {
|
||||
if j.ID == jobID {
|
||||
job = j
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, daemon := range q.provisionerDaemons {
|
||||
if daemon.OrganizationID != job.OrganizationID {
|
||||
continue
|
||||
}
|
||||
|
||||
if !tagsSubset(job.Tags, daemon.Tags) {
|
||||
continue
|
||||
}
|
||||
|
||||
provisionerMatches := false
|
||||
for _, p := range daemon.Provisioners {
|
||||
if p == job.Provisioner {
|
||||
provisionerMatches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !provisionerMatches {
|
||||
continue
|
||||
}
|
||||
|
||||
key := jobID.String() + "-" + daemon.ID.String()
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
results = append(results, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
|
||||
JobID: jobID,
|
||||
ProvisionerDaemon: daemon,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetExternalAuthLink(_ context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.ExternalAuthLink{}, err
|
||||
@@ -10344,25 +10409,26 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.ProvisionerDaemon{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
for _, d := range q.provisionerDaemons {
|
||||
if d.Name == arg.Name {
|
||||
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" {
|
||||
continue
|
||||
}
|
||||
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for existing daemon using the same composite key as SQL
|
||||
for i, d := range q.provisionerDaemons {
|
||||
if d.OrganizationID == arg.OrganizationID &&
|
||||
d.Name == arg.Name &&
|
||||
getOwnerFromTags(d.Tags) == getOwnerFromTags(arg.Tags) {
|
||||
d.Provisioners = arg.Provisioners
|
||||
d.Tags = maps.Clone(arg.Tags)
|
||||
d.Version = arg.Version
|
||||
d.LastSeenAt = arg.LastSeenAt
|
||||
d.Version = arg.Version
|
||||
d.APIVersion = arg.APIVersion
|
||||
d.OrganizationID = arg.OrganizationID
|
||||
d.KeyID = arg.KeyID
|
||||
q.provisionerDaemons[i] = d
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
@@ -10372,7 +10438,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
|
||||
Name: arg.Name,
|
||||
Provisioners: arg.Provisioners,
|
||||
Tags: maps.Clone(arg.Tags),
|
||||
ReplicaID: uuid.NullUUID{},
|
||||
LastSeenAt: arg.LastSeenAt,
|
||||
Version: arg.Version,
|
||||
APIVersion: arg.APIVersion,
|
||||
|
||||
@@ -637,6 +637,13 @@ func (m queryMetricsStore) GetDeploymentWorkspaceStats(ctx context.Context) (dat
|
||||
return row, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx, provisionerJobIds)
|
||||
m.queryLatencies.WithLabelValues("GetEligibleProvisionerDaemonsByProvisionerJobIDs").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
start := time.Now()
|
||||
link, err := m.s.GetExternalAuthLink(ctx, arg)
|
||||
|
||||
@@ -1281,6 +1281,21 @@ func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceStats(arg0 any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentWorkspaceStats", reflect.TypeOf((*MockStore)(nil).GetDeploymentWorkspaceStats), arg0)
|
||||
}
|
||||
|
||||
// GetEligibleProvisionerDaemonsByProvisionerJobIDs mocks base method.
|
||||
func (m *MockStore) GetEligibleProvisionerDaemonsByProvisionerJobIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetEligibleProvisionerDaemonsByProvisionerJobIDs", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetEligibleProvisionerDaemonsByProvisionerJobIDs indicates an expected call of GetEligibleProvisionerDaemonsByProvisionerJobIDs.
|
||||
func (mr *MockStoreMockRecorder) GetEligibleProvisionerDaemonsByProvisionerJobIDs(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEligibleProvisionerDaemonsByProvisionerJobIDs", reflect.TypeOf((*MockStore)(nil).GetEligibleProvisionerDaemonsByProvisionerJobIDs), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetExternalAuthLink mocks base method.
|
||||
func (m *MockStore) GetExternalAuthLink(arg0 context.Context, arg1 database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -269,6 +269,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object {
|
||||
InOrg(p.OrganizationID)
|
||||
}
|
||||
|
||||
func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object {
|
||||
return p.ProvisionerDaemon.RBACObject()
|
||||
}
|
||||
|
||||
func (p ProvisionerKey) RBACObject() rbac.Object {
|
||||
return rbac.ResourceProvisionerKeys.
|
||||
WithID(p.ID).
|
||||
|
||||
@@ -145,6 +145,7 @@ type sqlcQuerier interface {
|
||||
GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error)
|
||||
GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error)
|
||||
GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error)
|
||||
GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error)
|
||||
GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error)
|
||||
GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error)
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -211,6 +212,145 @@ func TestGetDeploymentWorkspaceAgentUsageStats(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEligibleProvisionerDaemonsByProvisionerJobIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NoJobsReturnsEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, daemons)
|
||||
})
|
||||
|
||||
t.Run("MatchesProvisionerType", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
matchingDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "matching-daemon",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "non-matching-daemon",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
require.Equal(t, matchingDaemon.ID, daemons[0].ProvisionerDaemon.ID)
|
||||
})
|
||||
|
||||
t.Run("MatchesOrganizationScope", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
provisionersdk.TagOwner: "",
|
||||
},
|
||||
})
|
||||
|
||||
orgDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "org-daemon",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
provisionersdk.TagOwner: "",
|
||||
},
|
||||
})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "user-daemon",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeUser,
|
||||
},
|
||||
})
|
||||
|
||||
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
require.Equal(t, orgDaemon.ID, daemons[0].ProvisionerDaemon.ID)
|
||||
})
|
||||
|
||||
t.Run("MatchesMultipleProvisioners", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
daemon1 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "daemon-1",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
daemon2 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "daemon-2",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
|
||||
Name: "daemon-3",
|
||||
OrganizationID: org.ID,
|
||||
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
|
||||
Tags: database.StringMap{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 2)
|
||||
|
||||
daemonIDs := []uuid.UUID{daemons[0].ProvisionerDaemon.ID, daemons[1].ProvisionerDaemon.ID}
|
||||
require.ElementsMatch(t, []uuid.UUID{daemon1.ID, daemon2.ID}, daemonIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWorkspaceAgentUsageStats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -5255,6 +5255,60 @@ func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const getEligibleProvisionerDaemonsByProvisionerJobIDs = `-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
|
||||
SELECT DISTINCT
|
||||
provisioner_jobs.id as job_id, provisioner_daemons.id, provisioner_daemons.created_at, provisioner_daemons.name, provisioner_daemons.provisioners, provisioner_daemons.replica_id, provisioner_daemons.tags, provisioner_daemons.last_seen_at, provisioner_daemons.version, provisioner_daemons.api_version, provisioner_daemons.organization_id, provisioner_daemons.key_id
|
||||
FROM
|
||||
provisioner_jobs
|
||||
JOIN
|
||||
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
|
||||
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
|
||||
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
|
||||
WHERE
|
||||
provisioner_jobs.id = ANY($1 :: uuid[])
|
||||
`
|
||||
|
||||
type GetEligibleProvisionerDaemonsByProvisionerJobIDsRow struct {
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getEligibleProvisionerDaemonsByProvisionerJobIDs, pq.Array(provisionerJobIds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
for rows.Next() {
|
||||
var i GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
if err := rows.Scan(
|
||||
&i.JobID,
|
||||
&i.ProvisionerDaemon.ID,
|
||||
&i.ProvisionerDaemon.CreatedAt,
|
||||
&i.ProvisionerDaemon.Name,
|
||||
pq.Array(&i.ProvisionerDaemon.Provisioners),
|
||||
&i.ProvisionerDaemon.ReplicaID,
|
||||
&i.ProvisionerDaemon.Tags,
|
||||
&i.ProvisionerDaemon.LastSeenAt,
|
||||
&i.ProvisionerDaemon.Version,
|
||||
&i.ProvisionerDaemon.APIVersion,
|
||||
&i.ProvisionerDaemon.OrganizationID,
|
||||
&i.ProvisionerDaemon.KeyID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
|
||||
SELECT
|
||||
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
|
||||
|
||||
@@ -16,6 +16,18 @@ WHERE
|
||||
-- adding support for searching by tags:
|
||||
(@want_tags :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, @want_tags::tagset));
|
||||
|
||||
-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
|
||||
SELECT DISTINCT
|
||||
provisioner_jobs.id as job_id, sqlc.embed(provisioner_daemons)
|
||||
FROM
|
||||
provisioner_jobs
|
||||
JOIN
|
||||
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
|
||||
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
|
||||
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
|
||||
WHERE
|
||||
provisioner_jobs.id = ANY(@provisioner_job_ids :: uuid[]);
|
||||
|
||||
-- name: DeleteOldProvisionerDaemons :exec
|
||||
-- Delete provisioner daemons that have been created at least a week ago
|
||||
-- and have not connected to coderd since a week.
|
||||
|
||||
+52
-31
@@ -202,6 +202,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.templateVersions,
|
||||
data.provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -291,7 +292,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.templateVersions[0],
|
||||
nil,
|
||||
data.provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -395,10 +396,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var matchedProvisioners codersdk.MatchedProvisioners
|
||||
if provisionerJob != nil {
|
||||
matchedProvisioners = db2sdk.MatchedProvisioners(provisionerDaemons, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
}
|
||||
apiBuild, err := api.convertWorkspaceBuild(
|
||||
*workspaceBuild,
|
||||
workspace,
|
||||
@@ -413,7 +410,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
[]database.WorkspaceAgentScript{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
database.TemplateVersion{},
|
||||
&matchedProvisioners,
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -648,14 +645,15 @@ func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type workspaceBuildsData struct {
|
||||
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
||||
templateVersions []database.TemplateVersion
|
||||
resources []database.WorkspaceResource
|
||||
metadata []database.WorkspaceResourceMetadatum
|
||||
agents []database.WorkspaceAgent
|
||||
apps []database.WorkspaceApp
|
||||
scripts []database.WorkspaceAgentScript
|
||||
logSources []database.WorkspaceAgentLogSource
|
||||
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
||||
templateVersions []database.TemplateVersion
|
||||
resources []database.WorkspaceResource
|
||||
metadata []database.WorkspaceResourceMetadatum
|
||||
agents []database.WorkspaceAgent
|
||||
apps []database.WorkspaceApp
|
||||
scripts []database.WorkspaceAgentScript
|
||||
logSources []database.WorkspaceAgentLogSource
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
}
|
||||
|
||||
func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) {
|
||||
@@ -667,6 +665,17 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return workspaceBuildsData{}, xerrors.Errorf("get provisioner jobs: %w", err)
|
||||
}
|
||||
pendingJobIDs := []uuid.UUID{}
|
||||
for _, job := range jobs {
|
||||
if job.ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
|
||||
pendingJobIDs = append(pendingJobIDs, job.ProvisionerJob.ID)
|
||||
}
|
||||
}
|
||||
|
||||
pendingJobProvisioners, err := api.Database.GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx, pendingJobIDs)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return workspaceBuildsData{}, xerrors.Errorf("get provisioner daemons: %w", err)
|
||||
}
|
||||
|
||||
templateVersionIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
|
||||
for _, build := range workspaceBuilds {
|
||||
@@ -687,8 +696,9 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
||||
|
||||
if len(resources) == 0 {
|
||||
return workspaceBuildsData{
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
provisionerDaemons: pendingJobProvisioners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -711,10 +721,11 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
||||
|
||||
if len(resources) == 0 {
|
||||
return workspaceBuildsData{
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
resources: resources,
|
||||
metadata: metadata,
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
resources: resources,
|
||||
metadata: metadata,
|
||||
provisionerDaemons: pendingJobProvisioners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -751,14 +762,15 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
||||
}
|
||||
|
||||
return workspaceBuildsData{
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
resources: resources,
|
||||
metadata: metadata,
|
||||
agents: agents,
|
||||
apps: apps,
|
||||
scripts: scripts,
|
||||
logSources: logSources,
|
||||
jobs: jobs,
|
||||
templateVersions: templateVersions,
|
||||
resources: resources,
|
||||
metadata: metadata,
|
||||
agents: agents,
|
||||
apps: apps,
|
||||
scripts: scripts,
|
||||
logSources: logSources,
|
||||
provisionerDaemons: pendingJobProvisioners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -773,6 +785,7 @@ func (api *API) convertWorkspaceBuilds(
|
||||
agentScripts []database.WorkspaceAgentScript,
|
||||
agentLogSources []database.WorkspaceAgentLogSource,
|
||||
templateVersions []database.TemplateVersion,
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||
) ([]codersdk.WorkspaceBuild, error) {
|
||||
workspaceByID := map[uuid.UUID]database.Workspace{}
|
||||
for _, workspace := range workspaces {
|
||||
@@ -814,7 +827,7 @@ func (api *API) convertWorkspaceBuilds(
|
||||
agentScripts,
|
||||
agentLogSources,
|
||||
templateVersion,
|
||||
nil,
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("converting workspace build: %w", err)
|
||||
@@ -837,7 +850,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
agentScripts []database.WorkspaceAgentScript,
|
||||
agentLogSources []database.WorkspaceAgentLogSource,
|
||||
templateVersion database.TemplateVersion,
|
||||
matchedProvisioners *codersdk.MatchedProvisioners,
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||
) (codersdk.WorkspaceBuild, error) {
|
||||
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
|
||||
for _, resource := range workspaceResources {
|
||||
@@ -863,6 +876,14 @@ func (api *API) convertWorkspaceBuild(
|
||||
for _, logSource := range agentLogSources {
|
||||
logSourcesByAgentID[logSource.WorkspaceAgentID] = append(logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
|
||||
}
|
||||
provisionerDaemonsForThisWorkspaceBuild := []database.ProvisionerDaemon{}
|
||||
for _, provisionerDaemon := range provisionerDaemons {
|
||||
if provisionerDaemon.JobID != job.ProvisionerJob.ID {
|
||||
continue
|
||||
}
|
||||
provisionerDaemonsForThisWorkspaceBuild = append(provisionerDaemonsForThisWorkspaceBuild, provisionerDaemon.ProvisionerDaemon)
|
||||
}
|
||||
matchedProvisioners := db2sdk.MatchedProvisioners(provisionerDaemonsForThisWorkspaceBuild, job.ProvisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
|
||||
resources := resourcesByJobID[job.ProvisionerJob.ID]
|
||||
apiResources := make([]codersdk.WorkspaceResource, 0)
|
||||
@@ -930,7 +951,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
Resources: apiResources,
|
||||
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
|
||||
DailyCost: build.DailyCost,
|
||||
MatchedProvisioners: matchedProvisioners,
|
||||
MatchedProvisioners: &matchedProvisioners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
@@ -612,10 +611,9 @@ func createWorkspace(
|
||||
}
|
||||
|
||||
var (
|
||||
provisionerJob *database.ProvisionerJob
|
||||
workspaceBuild *database.WorkspaceBuild
|
||||
provisionerDaemons []database.ProvisionerDaemon
|
||||
matchedProvisioners codersdk.MatchedProvisioners
|
||||
provisionerJob *database.ProvisionerJob
|
||||
workspaceBuild *database.WorkspaceBuild
|
||||
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
)
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := dbtime.Now()
|
||||
@@ -688,9 +686,6 @@ func createWorkspace(
|
||||
// Client probably doesn't care about this error, so just log it.
|
||||
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
||||
}
|
||||
if provisionerJob != nil {
|
||||
matchedProvisioners = db2sdk.MatchedProvisioners(provisionerDaemons, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
|
||||
}
|
||||
|
||||
auditReq.New = workspace.WorkspaceTable()
|
||||
|
||||
@@ -713,7 +708,7 @@ func createWorkspace(
|
||||
[]database.WorkspaceAgentScript{},
|
||||
[]database.WorkspaceAgentLogSource{},
|
||||
database.TemplateVersion{},
|
||||
&matchedProvisioners,
|
||||
provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -1843,6 +1838,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
|
||||
data.scripts,
|
||||
data.logSources,
|
||||
data.templateVersions,
|
||||
data.provisionerDaemons,
|
||||
)
|
||||
if err != nil {
|
||||
return workspaceData{}, xerrors.Errorf("convert workspace builds: %w", err)
|
||||
|
||||
@@ -214,7 +214,7 @@ func (b *Builder) Build(
|
||||
authFunc func(action policy.Action, object rbac.Objecter) bool,
|
||||
auditBaggage audit.WorkspaceBuildBaggage,
|
||||
) (
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.ProvisionerDaemon, error,
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error,
|
||||
) {
|
||||
var err error
|
||||
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
|
||||
@@ -228,7 +228,7 @@ func (b *Builder) Build(
|
||||
// later reads are consistent with earlier ones.
|
||||
var workspaceBuild *database.WorkspaceBuild
|
||||
var provisionerJob *database.ProvisionerJob
|
||||
var provisionerDaemons []database.ProvisionerDaemon
|
||||
var provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
||||
var err error
|
||||
b.store = tx
|
||||
@@ -248,7 +248,7 @@ func (b *Builder) Build(
|
||||
//
|
||||
// In order to utilize this cache, the functions that compute build attributes use a pointer receiver type.
|
||||
func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Objecter) bool) (
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.ProvisionerDaemon, error,
|
||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error,
|
||||
) {
|
||||
if authFunc != nil {
|
||||
err := b.authorize(authFunc)
|
||||
@@ -338,15 +338,12 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
|
||||
// to read all provisioner daemons. We need to retrieve the eligible
|
||||
// provisioner daemons for this job to show in the UI if there is no
|
||||
// matching provisioner daemon.
|
||||
provisionerDaemons, err := b.store.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: template.OrganizationID,
|
||||
WantTags: provisionerJob.Tags,
|
||||
})
|
||||
provisionerDaemons, err := b.store.GetEligibleProvisionerDaemonsByProvisionerJobIDs(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), []uuid.UUID{provisionerJob.ID})
|
||||
if err != nil {
|
||||
// NOTE: we do **not** want to fail a workspace build if we fail to
|
||||
// retrieve provisioner daemons. This is just to show in the UI if there
|
||||
// is no matching provisioner daemon for the job.
|
||||
provisionerDaemons = []database.ProvisionerDaemon{}
|
||||
provisionerDaemons = []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}
|
||||
}
|
||||
|
||||
templateVersionID, err := b.getTemplateVersionID()
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestBuilder_NoOptions(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -116,7 +116,7 @@ func TestBuilder_Initiator(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -161,7 +161,7 @@ func TestBuilder_Baggage(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -198,7 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(_ database.InsertProvisionerJobParams) {
|
||||
@@ -234,7 +234,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
||||
withLastBuildNotFound,
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
// previous rich parameters are not queried because there is no previous build.
|
||||
|
||||
// Outputs
|
||||
@@ -324,7 +324,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
@@ -416,7 +416,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -462,7 +462,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -596,7 +596,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -658,7 +658,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -718,7 +718,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||
@@ -1009,8 +1009,8 @@ func expectBuildParameters(
|
||||
}
|
||||
}
|
||||
|
||||
func withProvisionerDaemons(provisionerDaemons []database.ProvisionerDaemon) func(mTx *dbmock.MockStore) {
|
||||
func withProvisionerDaemons(provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) func(mTx *dbmock.MockStore) {
|
||||
return func(mTx *dbmock.MockStore) {
|
||||
mTx.EXPECT().GetProvisionerDaemonsByOrganization(gomock.Any(), gomock.Any()).Return(provisionerDaemons, nil)
|
||||
mTx.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).Return(provisionerDaemons, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
||||
@@ -35,6 +36,7 @@ import (
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
@@ -1764,6 +1766,214 @@ func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||
require.Equal(t, 0, len(memberViewWorkspaces.Workspaces), "member in other org should see 0 workspaces")
|
||||
}
|
||||
|
||||
func TestWorkspaceByOwnerAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Matching Provisioner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
user, err := client.User(ctx, userSubject.ID)
|
||||
require.NoError(t, err)
|
||||
username := user.Username
|
||||
|
||||
_ = coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
|
||||
// Pending builds should show matching provisioners
|
||||
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 1)
|
||||
|
||||
// Completed builds should not show matching provisioners, because no provisioner daemon can
|
||||
// be eligible to process a job that is already completed.
|
||||
completedBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
require.Equal(t, completedBuild.Status, codersdk.WorkspaceStatusRunning)
|
||||
require.Equal(t, completedBuild.MatchedProvisioners.Count, 0)
|
||||
require.Equal(t, completedBuild.MatchedProvisioners.Available, 0)
|
||||
|
||||
ws, err := client.WorkspaceByOwnerAndName(ctx, username, workspace.Name, codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the workspace details
|
||||
require.Equal(t, workspace.ID, ws.ID)
|
||||
require.Equal(t, workspace.Name, ws.Name)
|
||||
require.Equal(t, workspace.TemplateID, ws.TemplateID)
|
||||
require.Equal(t, completedBuild.Status, ws.LatestBuild.Status)
|
||||
require.Equal(t, ws.LatestBuild.MatchedProvisioners.Count, 0)
|
||||
require.Equal(t, ws.LatestBuild.MatchedProvisioners.Available, 0)
|
||||
|
||||
// Verify that the provisioner daemon is registered in the database
|
||||
//nolint:gocritic // unit testing
|
||||
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(daemons))
|
||||
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
|
||||
})
|
||||
|
||||
t.Run("No Matching Provisioner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
user, err := client.User(ctx, userSubject.ID)
|
||||
require.NoError(t, err)
|
||||
username := user.Username
|
||||
|
||||
closer := coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(daemons), 1)
|
||||
|
||||
// Simulate a provisioner daemon failure:
|
||||
err = closer.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate it's subsequent deletion from the database:
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
_, err = db.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
|
||||
Name: daemons[0].Name,
|
||||
OrganizationID: daemons[0].OrganizationID,
|
||||
Tags: daemons[0].Tags,
|
||||
Provisioners: daemons[0].Provisioners,
|
||||
Version: daemons[0].Version,
|
||||
APIVersion: daemons[0].APIVersion,
|
||||
KeyID: daemons[0].KeyID,
|
||||
// Simulate the passing of time such that the provisioner daemon is considered stale
|
||||
// and will be deleted:
|
||||
CreatedAt: time.Now().Add(-time.Hour * 24 * 8),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Time: time.Now().Add(-time.Hour * 24 * 8),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// nolint:gocritic // unit testing
|
||||
err = db.DeleteOldProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a workspace that will not be able to provision due to a lack of provisioner daemons:
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
|
||||
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 0)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
_, err = client.WorkspaceByOwnerAndName(dbauthz.As(ctx, userSubject), username, workspace.Name, codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 0)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
|
||||
})
|
||||
|
||||
t.Run("Unavailable Provisioner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
user, err := client.User(ctx, userSubject.ID)
|
||||
require.NoError(t, err)
|
||||
username := user.Username
|
||||
|
||||
closer := coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(daemons), 1)
|
||||
|
||||
// Simulate a provisioner daemon failure:
|
||||
err = closer.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
_, err = db.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
|
||||
Name: daemons[0].Name,
|
||||
OrganizationID: daemons[0].OrganizationID,
|
||||
Tags: daemons[0].Tags,
|
||||
Provisioners: daemons[0].Provisioners,
|
||||
Version: daemons[0].Version,
|
||||
APIVersion: daemons[0].APIVersion,
|
||||
KeyID: daemons[0].KeyID,
|
||||
// Simulate the passing of time such that the provisioner daemon, though not stale, has been
|
||||
// has been inactive for a while:
|
||||
CreatedAt: time.Now().Add(-time.Hour * 24 * 2),
|
||||
LastSeenAt: sql.NullTime{
|
||||
Time: time.Now().Add(-time.Hour * 24 * 2),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a workspace that will not be able to provision due to a lack of provisioner daemons:
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
|
||||
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
|
||||
|
||||
// nolint:gocritic // unit testing
|
||||
_, err = client.WorkspaceByOwnerAndName(dbauthz.As(ctx, userSubject), username, workspace.Name, codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
|
||||
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { chromatic } from "testHelpers/chromatic";
|
||||
import { ProvisionerAlert } from "./ProvisionerAlert";
|
||||
import { AlertVariant, ProvisionerAlert } from "./ProvisionerAlert";
|
||||
|
||||
const meta: Meta<typeof ProvisionerAlert> = {
|
||||
title: "modules/provisioners/ProvisionerAlert",
|
||||
@@ -21,6 +21,26 @@ export default meta;
|
||||
type Story = StoryObj<typeof ProvisionerAlert>;
|
||||
|
||||
export const Info: Story = {};
|
||||
|
||||
export const InfoInline: Story = {
|
||||
args: {
|
||||
variant: AlertVariant.Inline,
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
severity: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const WarningInline: Story = {
|
||||
args: {
|
||||
severity: "warning",
|
||||
variant: AlertVariant.Inline,
|
||||
},
|
||||
};
|
||||
|
||||
export const NullTags: Story = {
|
||||
args: {
|
||||
tags: undefined,
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
import type { Theme } from "@emotion/react";
|
||||
import AlertTitle from "@mui/material/AlertTitle";
|
||||
import { Alert, type AlertColor } from "components/Alert/Alert";
|
||||
import { AlertDetail } from "components/Alert/Alert";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { ProvisionerTag } from "modules/provisioners/ProvisionerTag";
|
||||
import type { FC } from "react";
|
||||
|
||||
export enum AlertVariant {
|
||||
// Alerts are usually styled with a full rounded border and meant to use as a visually distinct element of the page.
|
||||
// The Standalone variant conforms to this styling.
|
||||
Standalone = "Standalone",
|
||||
// We show these same alerts in environments such as log drawers where we stream the logs from builds.
|
||||
// In this case the full border is incongruent with the surroundings of the component.
|
||||
// The Inline variant replaces the full rounded border with a left border and a divider so that it complements the surroundings.
|
||||
Inline = "Inline",
|
||||
}
|
||||
|
||||
interface ProvisionerAlertProps {
|
||||
title: string;
|
||||
detail: string;
|
||||
severity: AlertColor;
|
||||
tags: Record<string, string>;
|
||||
variant?: AlertVariant;
|
||||
}
|
||||
|
||||
const getAlertStyles = (variant: AlertVariant, severity: AlertColor) => {
|
||||
switch (variant) {
|
||||
case AlertVariant.Inline:
|
||||
return {
|
||||
css: (theme: Theme) => ({
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
borderLeft: `2px solid ${theme.palette[severity].main}`,
|
||||
}),
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const ProvisionerAlert: FC<ProvisionerAlertProps> = ({
|
||||
title,
|
||||
detail,
|
||||
severity,
|
||||
tags,
|
||||
variant = AlertVariant.Standalone,
|
||||
}) => {
|
||||
return (
|
||||
<Alert
|
||||
severity={severity}
|
||||
css={(theme) => {
|
||||
return {
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
borderLeft: `2px solid ${theme.palette[severity].main}`,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Alert severity={severity} {...getAlertStyles(variant, severity)}>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDetail>
|
||||
<div>{detail}</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { chromatic } from "testHelpers/chromatic";
|
||||
import { MockTemplateVersion } from "testHelpers/entities";
|
||||
import { AlertVariant } from "./ProvisionerAlert";
|
||||
import { ProvisionerStatusAlert } from "./ProvisionerStatusAlert";
|
||||
|
||||
const meta: Meta<typeof ProvisionerStatusAlert> = {
|
||||
@@ -47,9 +48,24 @@ export const NoMatchingProvisioners: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const NoMatchingProvisionersInLogs: Story = {
|
||||
args: {
|
||||
matchingProvisioners: 0,
|
||||
variant: AlertVariant.Inline,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAvailableProvisioners: Story = {
|
||||
args: {
|
||||
matchingProvisioners: 1,
|
||||
availableProvisioners: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAvailableProvisionersInLogs: Story = {
|
||||
args: {
|
||||
matchingProvisioners: 1,
|
||||
availableProvisioners: 0,
|
||||
variant: AlertVariant.Inline,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { AlertColor } from "components/Alert/Alert";
|
||||
import type { FC } from "react";
|
||||
import { ProvisionerAlert } from "./ProvisionerAlert";
|
||||
import { AlertVariant, ProvisionerAlert } from "./ProvisionerAlert";
|
||||
|
||||
interface ProvisionerStatusAlertProps {
|
||||
matchingProvisioners: number | undefined;
|
||||
availableProvisioners: number | undefined;
|
||||
tags: Record<string, string>;
|
||||
variant?: AlertVariant;
|
||||
}
|
||||
|
||||
export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
||||
matchingProvisioners,
|
||||
availableProvisioners,
|
||||
tags,
|
||||
variant = AlertVariant.Standalone,
|
||||
}) => {
|
||||
let title: string;
|
||||
let detail: string;
|
||||
@@ -42,6 +44,7 @@ export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
||||
detail={detail}
|
||||
severity={severity}
|
||||
tags={tags}
|
||||
variant={variant}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { visuallyHidden } from "@mui/utils";
|
||||
import { JobError } from "api/queries/templates";
|
||||
import type { TemplateVersion } from "api/typesGenerated";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { AlertVariant } from "modules/provisioners/ProvisionerAlert";
|
||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
||||
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||
@@ -94,6 +95,7 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
||||
matchingProvisioners={matchingProvisioners}
|
||||
availableProvisioners={availableProvisioners}
|
||||
tags={templateVersion?.job.tags ?? {}}
|
||||
variant={AlertVariant.Inline}
|
||||
/>
|
||||
<Loader />
|
||||
</>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||
import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert";
|
||||
import { AlertVariant } from "modules/provisioners/ProvisionerAlert";
|
||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||
import { TemplateFileTree } from "modules/templates/TemplateFiles/TemplateFileTree";
|
||||
import { isBinaryData } from "modules/templates/TemplateFiles/isBinaryData";
|
||||
@@ -593,6 +594,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
detail={templateVersion.job.error}
|
||||
severity="error"
|
||||
tags={templateVersion.job.tags}
|
||||
variant={AlertVariant.Inline}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -602,6 +604,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
matchingProvisioners={matchingProvisioners}
|
||||
availableProvisioners={availableProvisioners}
|
||||
tags={templateVersion.job.tags}
|
||||
variant={AlertVariant.Inline}
|
||||
/>
|
||||
<Loader css={{ height: "100%" }} />
|
||||
</>
|
||||
|
||||
@@ -95,6 +95,51 @@ export const PendingInQueue: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingWithNoProvisioners: Story = {
|
||||
args: {
|
||||
...Running.args,
|
||||
workspace: {
|
||||
...Mocks.MockPendingWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockPendingWorkspace.latest_build,
|
||||
matched_provisioners: {
|
||||
count: 0,
|
||||
available: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingWithNoAvailableProvisioners: Story = {
|
||||
args: {
|
||||
...Running.args,
|
||||
workspace: {
|
||||
...Mocks.MockPendingWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockPendingWorkspace.latest_build,
|
||||
matched_provisioners: {
|
||||
count: 1,
|
||||
available: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingWithUndefinedProvisioners: Story = {
|
||||
args: {
|
||||
...Running.args,
|
||||
workspace: {
|
||||
...Mocks.MockPendingWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockPendingWorkspace.latest_build,
|
||||
matched_provisioners: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Starting: Story = {
|
||||
args: {
|
||||
...Running.args,
|
||||
@@ -130,7 +175,7 @@ export const FailedWithLogs: Story = {
|
||||
},
|
||||
},
|
||||
},
|
||||
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
|
||||
buildLogs: makeFailedBuildLogs(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -148,7 +193,7 @@ export const FailedWithRetry: Story = {
|
||||
},
|
||||
},
|
||||
},
|
||||
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
|
||||
buildLogs: makeFailedBuildLogs(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type * as TypesGen from "api/typesGenerated";
|
||||
import { Alert, AlertDetail } from "components/Alert/Alert";
|
||||
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
|
||||
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
|
||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||
import { AgentRow } from "modules/resources/AgentRow";
|
||||
import { WorkspaceTimings } from "modules/workspaces/WorkspaceTiming/WorkspaceTimings";
|
||||
import type { FC } from "react";
|
||||
@@ -14,6 +15,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { HistorySidebar } from "./HistorySidebar";
|
||||
import { ResourceMetadata } from "./ResourceMetadata";
|
||||
import { ResourcesSidebar } from "./ResourcesSidebar";
|
||||
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
||||
import {
|
||||
ActiveTransition,
|
||||
WorkspaceBuildProgress,
|
||||
@@ -46,7 +48,7 @@ export interface WorkspaceProps {
|
||||
canDebugMode: boolean;
|
||||
handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
buildLogs?: React.ReactNode;
|
||||
buildLogs?: TypesGen.ProvisionerJobLog[];
|
||||
latestVersion?: TypesGen.TemplateVersion;
|
||||
permissions: WorkspacePermissions;
|
||||
isOwner: boolean;
|
||||
@@ -108,6 +110,14 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
(r) => resourceOptionValue(r) === resourcesNav.value,
|
||||
);
|
||||
|
||||
const shouldDisplayBuildLogs =
|
||||
(buildLogs ?? []).length > 0 && workspace.latest_build.status !== "running";
|
||||
|
||||
const provisionersHealthy =
|
||||
(workspace.latest_build.matched_provisioners?.available ?? 0) > 0;
|
||||
const shouldShowProvisionerAlert =
|
||||
!provisionersHealthy && (!buildLogs || buildLogs.length === 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
@@ -208,6 +218,18 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowProvisionerAlert && (
|
||||
<ProvisionerStatusAlert
|
||||
matchingProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.count
|
||||
}
|
||||
availableProvisioners={
|
||||
workspace.latest_build.matched_provisioners?.available ?? 0
|
||||
}
|
||||
tags={workspace.latest_build.job.tags}
|
||||
/>
|
||||
)}
|
||||
|
||||
{workspace.latest_build.job.error && (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>Workspace build failed</AlertTitle>
|
||||
@@ -222,7 +244,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{buildLogs}
|
||||
{shouldDisplayBuildLogs && (
|
||||
<WorkspaceBuildLogsSection logs={buildLogs} />
|
||||
)}
|
||||
|
||||
{selectedResource && (
|
||||
<section
|
||||
|
||||
@@ -35,7 +35,6 @@ import { pageTitle } from "utils/page";
|
||||
import { ChangeVersionDialog } from "./ChangeVersionDialog";
|
||||
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
||||
import { Workspace } from "./Workspace";
|
||||
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
||||
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
||||
import type { WorkspacePermissions } from "./permissions";
|
||||
|
||||
@@ -71,10 +70,10 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
});
|
||||
|
||||
// Build logs
|
||||
const shouldDisplayBuildLogs = workspace.latest_build.status !== "running";
|
||||
const shouldStreamBuildLogs = workspace.latest_build.status !== "running";
|
||||
const buildLogs = useWorkspaceBuildLogs(
|
||||
workspace.latest_build.id,
|
||||
shouldDisplayBuildLogs,
|
||||
shouldStreamBuildLogs,
|
||||
);
|
||||
|
||||
// Restart
|
||||
@@ -278,11 +277,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
buildInfo={buildInfoQuery.data}
|
||||
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
||||
template={template}
|
||||
buildLogs={
|
||||
shouldDisplayBuildLogs && (
|
||||
<WorkspaceBuildLogsSection logs={buildLogs} />
|
||||
)
|
||||
}
|
||||
buildLogs={buildLogs}
|
||||
isOwner={isOwner}
|
||||
timings={timingsQuery.data}
|
||||
/>
|
||||
|
||||
@@ -1199,6 +1199,10 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
|
||||
resources: [MockWorkspaceResource],
|
||||
status: "running",
|
||||
daily_cost: 20,
|
||||
matched_provisioners: {
|
||||
count: 1,
|
||||
available: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = {
|
||||
|
||||
Reference in New Issue
Block a user