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]",
|
"deadline": "[timestamp]",
|
||||||
"max_deadline": null,
|
"max_deadline": null,
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"daily_cost": 0
|
"daily_cost": 0,
|
||||||
|
"matched_provisioners": {
|
||||||
|
"count": 0,
|
||||||
|
"available": 0,
|
||||||
|
"most_recently_seen": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"outdated": false,
|
"outdated": false,
|
||||||
"name": "test-workspace",
|
"name": "test-workspace",
|
||||||
|
|||||||
@@ -1568,6 +1568,10 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get
|
|||||||
return q.db.GetDeploymentWorkspaceStats(ctx)
|
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) {
|
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)
|
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")
|
s.NoError(err, "get provisioner daemon by org")
|
||||||
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
|
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) {
|
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
|
||||||
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
||||||
Tags: database.StringMap(map[string]string{
|
Tags: database.StringMap(map[string]string{
|
||||||
|
|||||||
@@ -503,6 +503,46 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
|
|||||||
return groupMember
|
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
|
// 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.
|
// 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 {
|
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
|
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 {
|
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
|
||||||
return xerrors.New("AcquireLock must only be called within a transaction")
|
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
|
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) {
|
func (q *FakeQuerier) GetExternalAuthLink(_ context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||||
if err := validateDatabaseType(arg); err != nil {
|
if err := validateDatabaseType(arg); err != nil {
|
||||||
return database.ExternalAuthLink{}, err
|
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) {
|
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
|
||||||
err := validateDatabaseType(arg)
|
if err := validateDatabaseType(arg); err != nil {
|
||||||
if err != nil {
|
|
||||||
return database.ProvisionerDaemon{}, err
|
return database.ProvisionerDaemon{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
for _, d := range q.provisionerDaemons {
|
|
||||||
if d.Name == arg.Name {
|
// Look for existing daemon using the same composite key as SQL
|
||||||
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" {
|
for i, d := range q.provisionerDaemons {
|
||||||
continue
|
if d.OrganizationID == arg.OrganizationID &&
|
||||||
}
|
d.Name == arg.Name &&
|
||||||
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] {
|
getOwnerFromTags(d.Tags) == getOwnerFromTags(arg.Tags) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
d.Provisioners = arg.Provisioners
|
d.Provisioners = arg.Provisioners
|
||||||
d.Tags = maps.Clone(arg.Tags)
|
d.Tags = maps.Clone(arg.Tags)
|
||||||
d.Version = arg.Version
|
|
||||||
d.LastSeenAt = arg.LastSeenAt
|
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
|
return d, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10372,7 +10438,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
|
|||||||
Name: arg.Name,
|
Name: arg.Name,
|
||||||
Provisioners: arg.Provisioners,
|
Provisioners: arg.Provisioners,
|
||||||
Tags: maps.Clone(arg.Tags),
|
Tags: maps.Clone(arg.Tags),
|
||||||
ReplicaID: uuid.NullUUID{},
|
|
||||||
LastSeenAt: arg.LastSeenAt,
|
LastSeenAt: arg.LastSeenAt,
|
||||||
Version: arg.Version,
|
Version: arg.Version,
|
||||||
APIVersion: arg.APIVersion,
|
APIVersion: arg.APIVersion,
|
||||||
|
|||||||
@@ -637,6 +637,13 @@ func (m queryMetricsStore) GetDeploymentWorkspaceStats(ctx context.Context) (dat
|
|||||||
return row, err
|
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) {
|
func (m queryMetricsStore) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
link, err := m.s.GetExternalAuthLink(ctx, arg)
|
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)
|
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.
|
// GetExternalAuthLink mocks base method.
|
||||||
func (m *MockStore) GetExternalAuthLink(arg0 context.Context, arg1 database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
func (m *MockStore) GetExternalAuthLink(arg0 context.Context, arg1 database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object {
|
|||||||
InOrg(p.OrganizationID)
|
InOrg(p.OrganizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object {
|
||||||
|
return p.ProvisionerDaemon.RBACObject()
|
||||||
|
}
|
||||||
|
|
||||||
func (p ProvisionerKey) RBACObject() rbac.Object {
|
func (p ProvisionerKey) RBACObject() rbac.Object {
|
||||||
return rbac.ResourceProvisionerKeys.
|
return rbac.ResourceProvisionerKeys.
|
||||||
WithID(p.ID).
|
WithID(p.ID).
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ type sqlcQuerier interface {
|
|||||||
GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error)
|
GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error)
|
||||||
GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error)
|
GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error)
|
||||||
GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error)
|
GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error)
|
||||||
|
GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error)
|
||||||
GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error)
|
GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||||
GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error)
|
GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error)
|
||||||
GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, 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/httpmw"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
|
"github.com/coder/coder/v2/provisionersdk"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"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) {
|
func TestGetWorkspaceAgentUsageStats(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -5255,6 +5255,60 @@ func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
|||||||
return err
|
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
|
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
|
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:
|
-- adding support for searching by tags:
|
||||||
(@want_tags :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, @want_tags::tagset));
|
(@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
|
-- name: DeleteOldProvisionerDaemons :exec
|
||||||
-- Delete provisioner daemons that have been created at least a week ago
|
-- Delete provisioner daemons that have been created at least a week ago
|
||||||
-- and have not connected to coderd since a week.
|
-- 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.scripts,
|
||||||
data.logSources,
|
data.logSources,
|
||||||
data.templateVersions,
|
data.templateVersions,
|
||||||
|
data.provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
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.scripts,
|
||||||
data.logSources,
|
data.logSources,
|
||||||
data.templateVersions[0],
|
data.templateVersions[0],
|
||||||
nil,
|
data.provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
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(
|
apiBuild, err := api.convertWorkspaceBuild(
|
||||||
*workspaceBuild,
|
*workspaceBuild,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -413,7 +410,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
[]database.WorkspaceAgentScript{},
|
[]database.WorkspaceAgentScript{},
|
||||||
[]database.WorkspaceAgentLogSource{},
|
[]database.WorkspaceAgentLogSource{},
|
||||||
database.TemplateVersion{},
|
database.TemplateVersion{},
|
||||||
&matchedProvisioners,
|
provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
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 {
|
type workspaceBuildsData struct {
|
||||||
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
||||||
templateVersions []database.TemplateVersion
|
templateVersions []database.TemplateVersion
|
||||||
resources []database.WorkspaceResource
|
resources []database.WorkspaceResource
|
||||||
metadata []database.WorkspaceResourceMetadatum
|
metadata []database.WorkspaceResourceMetadatum
|
||||||
agents []database.WorkspaceAgent
|
agents []database.WorkspaceAgent
|
||||||
apps []database.WorkspaceApp
|
apps []database.WorkspaceApp
|
||||||
scripts []database.WorkspaceAgentScript
|
scripts []database.WorkspaceAgentScript
|
||||||
logSources []database.WorkspaceAgentLogSource
|
logSources []database.WorkspaceAgentLogSource
|
||||||
|
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) {
|
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) {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return workspaceBuildsData{}, xerrors.Errorf("get provisioner jobs: %w", err)
|
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))
|
templateVersionIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
|
||||||
for _, build := range workspaceBuilds {
|
for _, build := range workspaceBuilds {
|
||||||
@@ -687,8 +696,9 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
|||||||
|
|
||||||
if len(resources) == 0 {
|
if len(resources) == 0 {
|
||||||
return workspaceBuildsData{
|
return workspaceBuildsData{
|
||||||
jobs: jobs,
|
jobs: jobs,
|
||||||
templateVersions: templateVersions,
|
templateVersions: templateVersions,
|
||||||
|
provisionerDaemons: pendingJobProvisioners,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,10 +721,11 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
|||||||
|
|
||||||
if len(resources) == 0 {
|
if len(resources) == 0 {
|
||||||
return workspaceBuildsData{
|
return workspaceBuildsData{
|
||||||
jobs: jobs,
|
jobs: jobs,
|
||||||
templateVersions: templateVersions,
|
templateVersions: templateVersions,
|
||||||
resources: resources,
|
resources: resources,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
provisionerDaemons: pendingJobProvisioners,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,14 +762,15 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
|
|||||||
}
|
}
|
||||||
|
|
||||||
return workspaceBuildsData{
|
return workspaceBuildsData{
|
||||||
jobs: jobs,
|
jobs: jobs,
|
||||||
templateVersions: templateVersions,
|
templateVersions: templateVersions,
|
||||||
resources: resources,
|
resources: resources,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
agents: agents,
|
agents: agents,
|
||||||
apps: apps,
|
apps: apps,
|
||||||
scripts: scripts,
|
scripts: scripts,
|
||||||
logSources: logSources,
|
logSources: logSources,
|
||||||
|
provisionerDaemons: pendingJobProvisioners,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +785,7 @@ func (api *API) convertWorkspaceBuilds(
|
|||||||
agentScripts []database.WorkspaceAgentScript,
|
agentScripts []database.WorkspaceAgentScript,
|
||||||
agentLogSources []database.WorkspaceAgentLogSource,
|
agentLogSources []database.WorkspaceAgentLogSource,
|
||||||
templateVersions []database.TemplateVersion,
|
templateVersions []database.TemplateVersion,
|
||||||
|
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||||
) ([]codersdk.WorkspaceBuild, error) {
|
) ([]codersdk.WorkspaceBuild, error) {
|
||||||
workspaceByID := map[uuid.UUID]database.Workspace{}
|
workspaceByID := map[uuid.UUID]database.Workspace{}
|
||||||
for _, workspace := range workspaces {
|
for _, workspace := range workspaces {
|
||||||
@@ -814,7 +827,7 @@ func (api *API) convertWorkspaceBuilds(
|
|||||||
agentScripts,
|
agentScripts,
|
||||||
agentLogSources,
|
agentLogSources,
|
||||||
templateVersion,
|
templateVersion,
|
||||||
nil,
|
provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("converting workspace build: %w", err)
|
return nil, xerrors.Errorf("converting workspace build: %w", err)
|
||||||
@@ -837,7 +850,7 @@ func (api *API) convertWorkspaceBuild(
|
|||||||
agentScripts []database.WorkspaceAgentScript,
|
agentScripts []database.WorkspaceAgentScript,
|
||||||
agentLogSources []database.WorkspaceAgentLogSource,
|
agentLogSources []database.WorkspaceAgentLogSource,
|
||||||
templateVersion database.TemplateVersion,
|
templateVersion database.TemplateVersion,
|
||||||
matchedProvisioners *codersdk.MatchedProvisioners,
|
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
|
||||||
) (codersdk.WorkspaceBuild, error) {
|
) (codersdk.WorkspaceBuild, error) {
|
||||||
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
|
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
|
||||||
for _, resource := range workspaceResources {
|
for _, resource := range workspaceResources {
|
||||||
@@ -863,6 +876,14 @@ func (api *API) convertWorkspaceBuild(
|
|||||||
for _, logSource := range agentLogSources {
|
for _, logSource := range agentLogSources {
|
||||||
logSourcesByAgentID[logSource.WorkspaceAgentID] = append(logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
|
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]
|
resources := resourcesByJobID[job.ProvisionerJob.ID]
|
||||||
apiResources := make([]codersdk.WorkspaceResource, 0)
|
apiResources := make([]codersdk.WorkspaceResource, 0)
|
||||||
@@ -930,7 +951,7 @@ func (api *API) convertWorkspaceBuild(
|
|||||||
Resources: apiResources,
|
Resources: apiResources,
|
||||||
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
|
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
|
||||||
DailyCost: build.DailyCost,
|
DailyCost: build.DailyCost,
|
||||||
MatchedProvisioners: matchedProvisioners,
|
MatchedProvisioners: &matchedProvisioners,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
"github.com/coder/coder/v2/coderd/httpmw"
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
"github.com/coder/coder/v2/coderd/notifications"
|
"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"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
"github.com/coder/coder/v2/coderd/schedule"
|
"github.com/coder/coder/v2/coderd/schedule"
|
||||||
@@ -612,10 +611,9 @@ func createWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
provisionerJob *database.ProvisionerJob
|
provisionerJob *database.ProvisionerJob
|
||||||
workspaceBuild *database.WorkspaceBuild
|
workspaceBuild *database.WorkspaceBuild
|
||||||
provisionerDaemons []database.ProvisionerDaemon
|
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||||
matchedProvisioners codersdk.MatchedProvisioners
|
|
||||||
)
|
)
|
||||||
err = api.Database.InTx(func(db database.Store) error {
|
err = api.Database.InTx(func(db database.Store) error {
|
||||||
now := dbtime.Now()
|
now := dbtime.Now()
|
||||||
@@ -688,9 +686,6 @@ func createWorkspace(
|
|||||||
// Client probably doesn't care about this error, so just log it.
|
// 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))
|
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()
|
auditReq.New = workspace.WorkspaceTable()
|
||||||
|
|
||||||
@@ -713,7 +708,7 @@ func createWorkspace(
|
|||||||
[]database.WorkspaceAgentScript{},
|
[]database.WorkspaceAgentScript{},
|
||||||
[]database.WorkspaceAgentLogSource{},
|
[]database.WorkspaceAgentLogSource{},
|
||||||
database.TemplateVersion{},
|
database.TemplateVersion{},
|
||||||
&matchedProvisioners,
|
provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
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.scripts,
|
||||||
data.logSources,
|
data.logSources,
|
||||||
data.templateVersions,
|
data.templateVersions,
|
||||||
|
data.provisionerDaemons,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return workspaceData{}, xerrors.Errorf("convert workspace builds: %w", err)
|
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,
|
authFunc func(action policy.Action, object rbac.Objecter) bool,
|
||||||
auditBaggage audit.WorkspaceBuildBaggage,
|
auditBaggage audit.WorkspaceBuildBaggage,
|
||||||
) (
|
) (
|
||||||
*database.WorkspaceBuild, *database.ProvisionerJob, []database.ProvisionerDaemon, error,
|
*database.WorkspaceBuild, *database.ProvisionerJob, []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error,
|
||||||
) {
|
) {
|
||||||
var err error
|
var err error
|
||||||
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
|
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
|
||||||
@@ -228,7 +228,7 @@ func (b *Builder) Build(
|
|||||||
// later reads are consistent with earlier ones.
|
// later reads are consistent with earlier ones.
|
||||||
var workspaceBuild *database.WorkspaceBuild
|
var workspaceBuild *database.WorkspaceBuild
|
||||||
var provisionerJob *database.ProvisionerJob
|
var provisionerJob *database.ProvisionerJob
|
||||||
var provisionerDaemons []database.ProvisionerDaemon
|
var provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||||
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
||||||
var err error
|
var err error
|
||||||
b.store = tx
|
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.
|
// 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) (
|
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 {
|
if authFunc != nil {
|
||||||
err := b.authorize(authFunc)
|
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
|
// 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
|
// provisioner daemons for this job to show in the UI if there is no
|
||||||
// matching provisioner daemon.
|
// matching provisioner daemon.
|
||||||
provisionerDaemons, err := b.store.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), database.GetProvisionerDaemonsByOrganizationParams{
|
provisionerDaemons, err := b.store.GetEligibleProvisionerDaemonsByProvisionerJobIDs(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), []uuid.UUID{provisionerJob.ID})
|
||||||
OrganizationID: template.OrganizationID,
|
|
||||||
WantTags: provisionerJob.Tags,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// NOTE: we do **not** want to fail a workspace build if we fail to
|
// 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
|
// retrieve provisioner daemons. This is just to show in the UI if there
|
||||||
// is no matching provisioner daemon for the job.
|
// is no matching provisioner daemon for the job.
|
||||||
provisionerDaemons = []database.ProvisionerDaemon{}
|
provisionerDaemons = []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}
|
||||||
}
|
}
|
||||||
|
|
||||||
templateVersionID, err := b.getTemplateVersionID()
|
templateVersionID, err := b.getTemplateVersionID()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func TestBuilder_NoOptions(t *testing.T) {
|
|||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||||
@@ -116,7 +116,7 @@ func TestBuilder_Initiator(t *testing.T) {
|
|||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||||
@@ -161,7 +161,7 @@ func TestBuilder_Baggage(t *testing.T) {
|
|||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||||
@@ -198,7 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
|
|||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(_ database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(_ database.InsertProvisionerJobParams) {
|
||||||
@@ -234,7 +234,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
|||||||
withLastBuildNotFound,
|
withLastBuildNotFound,
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
// previous rich parameters are not queried because there is no previous build.
|
// previous rich parameters are not queried because there is no previous build.
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
@@ -324,7 +324,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||||
@@ -416,7 +416,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||||
@@ -462,7 +462,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||||
@@ -596,7 +596,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||||
@@ -658,7 +658,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
||||||
@@ -718,7 +718,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
withProvisionerDaemons([]database.ProvisionerDaemon{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
|
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) {
|
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/dbfake"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"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/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
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/license"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||||
"github.com/coder/coder/v2/provisioner/echo"
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
|
"github.com/coder/coder/v2/provisionersdk"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
"github.com/coder/quartz"
|
"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")
|
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 {
|
func must[T any](value T, err error) T {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { chromatic } from "testHelpers/chromatic";
|
import { chromatic } from "testHelpers/chromatic";
|
||||||
import { ProvisionerAlert } from "./ProvisionerAlert";
|
import { AlertVariant, ProvisionerAlert } from "./ProvisionerAlert";
|
||||||
|
|
||||||
const meta: Meta<typeof ProvisionerAlert> = {
|
const meta: Meta<typeof ProvisionerAlert> = {
|
||||||
title: "modules/provisioners/ProvisionerAlert",
|
title: "modules/provisioners/ProvisionerAlert",
|
||||||
@@ -21,6 +21,26 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof ProvisionerAlert>;
|
type Story = StoryObj<typeof ProvisionerAlert>;
|
||||||
|
|
||||||
export const Info: Story = {};
|
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 = {
|
export const NullTags: Story = {
|
||||||
args: {
|
args: {
|
||||||
tags: undefined,
|
tags: undefined,
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
|
import type { Theme } from "@emotion/react";
|
||||||
import AlertTitle from "@mui/material/AlertTitle";
|
import AlertTitle from "@mui/material/AlertTitle";
|
||||||
import { Alert, type AlertColor } from "components/Alert/Alert";
|
import { Alert, type AlertColor } from "components/Alert/Alert";
|
||||||
import { AlertDetail } from "components/Alert/Alert";
|
import { AlertDetail } from "components/Alert/Alert";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { ProvisionerTag } from "modules/provisioners/ProvisionerTag";
|
import { ProvisionerTag } from "modules/provisioners/ProvisionerTag";
|
||||||
import type { FC } from "react";
|
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 {
|
interface ProvisionerAlertProps {
|
||||||
title: string;
|
title: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
severity: AlertColor;
|
severity: AlertColor;
|
||||||
tags: Record<string, string>;
|
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> = ({
|
export const ProvisionerAlert: FC<ProvisionerAlertProps> = ({
|
||||||
title,
|
title,
|
||||||
detail,
|
detail,
|
||||||
severity,
|
severity,
|
||||||
tags,
|
tags,
|
||||||
|
variant = AlertVariant.Standalone,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert severity={severity} {...getAlertStyles(variant, severity)}>
|
||||||
severity={severity}
|
|
||||||
css={(theme) => {
|
|
||||||
return {
|
|
||||||
borderRadius: 0,
|
|
||||||
border: 0,
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderLeft: `2px solid ${theme.palette[severity].main}`,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AlertTitle>{title}</AlertTitle>
|
<AlertTitle>{title}</AlertTitle>
|
||||||
<AlertDetail>
|
<AlertDetail>
|
||||||
<div>{detail}</div>
|
<div>{detail}</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { chromatic } from "testHelpers/chromatic";
|
import { chromatic } from "testHelpers/chromatic";
|
||||||
import { MockTemplateVersion } from "testHelpers/entities";
|
import { MockTemplateVersion } from "testHelpers/entities";
|
||||||
|
import { AlertVariant } from "./ProvisionerAlert";
|
||||||
import { ProvisionerStatusAlert } from "./ProvisionerStatusAlert";
|
import { ProvisionerStatusAlert } from "./ProvisionerStatusAlert";
|
||||||
|
|
||||||
const meta: Meta<typeof 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 = {
|
export const NoAvailableProvisioners: Story = {
|
||||||
args: {
|
args: {
|
||||||
matchingProvisioners: 1,
|
matchingProvisioners: 1,
|
||||||
availableProvisioners: 0,
|
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 { AlertColor } from "components/Alert/Alert";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { ProvisionerAlert } from "./ProvisionerAlert";
|
import { AlertVariant, ProvisionerAlert } from "./ProvisionerAlert";
|
||||||
|
|
||||||
interface ProvisionerStatusAlertProps {
|
interface ProvisionerStatusAlertProps {
|
||||||
matchingProvisioners: number | undefined;
|
matchingProvisioners: number | undefined;
|
||||||
availableProvisioners: number | undefined;
|
availableProvisioners: number | undefined;
|
||||||
tags: Record<string, string>;
|
tags: Record<string, string>;
|
||||||
|
variant?: AlertVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
||||||
matchingProvisioners,
|
matchingProvisioners,
|
||||||
availableProvisioners,
|
availableProvisioners,
|
||||||
tags,
|
tags,
|
||||||
|
variant = AlertVariant.Standalone,
|
||||||
}) => {
|
}) => {
|
||||||
let title: string;
|
let title: string;
|
||||||
let detail: string;
|
let detail: string;
|
||||||
@@ -42,6 +44,7 @@ export const ProvisionerStatusAlert: FC<ProvisionerStatusAlertProps> = ({
|
|||||||
detail={detail}
|
detail={detail}
|
||||||
severity={severity}
|
severity={severity}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { visuallyHidden } from "@mui/utils";
|
|||||||
import { JobError } from "api/queries/templates";
|
import { JobError } from "api/queries/templates";
|
||||||
import type { TemplateVersion } from "api/typesGenerated";
|
import type { TemplateVersion } from "api/typesGenerated";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
|
import { AlertVariant } from "modules/provisioners/ProvisionerAlert";
|
||||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||||
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
||||||
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||||
@@ -94,6 +95,7 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
|||||||
matchingProvisioners={matchingProvisioners}
|
matchingProvisioners={matchingProvisioners}
|
||||||
availableProvisioners={availableProvisioners}
|
availableProvisioners={availableProvisioners}
|
||||||
tags={templateVersion?.job.tags ?? {}}
|
tags={templateVersion?.job.tags ?? {}}
|
||||||
|
variant={AlertVariant.Inline}
|
||||||
/>
|
/>
|
||||||
<Loader />
|
<Loader />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||||
import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert";
|
import { ProvisionerAlert } from "modules/provisioners/ProvisionerAlert";
|
||||||
|
import { AlertVariant } from "modules/provisioners/ProvisionerAlert";
|
||||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||||
import { TemplateFileTree } from "modules/templates/TemplateFiles/TemplateFileTree";
|
import { TemplateFileTree } from "modules/templates/TemplateFiles/TemplateFileTree";
|
||||||
import { isBinaryData } from "modules/templates/TemplateFiles/isBinaryData";
|
import { isBinaryData } from "modules/templates/TemplateFiles/isBinaryData";
|
||||||
@@ -593,6 +594,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
detail={templateVersion.job.error}
|
detail={templateVersion.job.error}
|
||||||
severity="error"
|
severity="error"
|
||||||
tags={templateVersion.job.tags}
|
tags={templateVersion.job.tags}
|
||||||
|
variant={AlertVariant.Inline}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -602,6 +604,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
matchingProvisioners={matchingProvisioners}
|
matchingProvisioners={matchingProvisioners}
|
||||||
availableProvisioners={availableProvisioners}
|
availableProvisioners={availableProvisioners}
|
||||||
tags={templateVersion.job.tags}
|
tags={templateVersion.job.tags}
|
||||||
|
variant={AlertVariant.Inline}
|
||||||
/>
|
/>
|
||||||
<Loader css={{ height: "100%" }} />
|
<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 = {
|
export const Starting: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Running.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 { Alert, AlertDetail } from "components/Alert/Alert";
|
||||||
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
|
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
|
||||||
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
|
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
|
||||||
|
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||||
import { AgentRow } from "modules/resources/AgentRow";
|
import { AgentRow } from "modules/resources/AgentRow";
|
||||||
import { WorkspaceTimings } from "modules/workspaces/WorkspaceTiming/WorkspaceTimings";
|
import { WorkspaceTimings } from "modules/workspaces/WorkspaceTiming/WorkspaceTimings";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
@@ -14,6 +15,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { HistorySidebar } from "./HistorySidebar";
|
import { HistorySidebar } from "./HistorySidebar";
|
||||||
import { ResourceMetadata } from "./ResourceMetadata";
|
import { ResourceMetadata } from "./ResourceMetadata";
|
||||||
import { ResourcesSidebar } from "./ResourcesSidebar";
|
import { ResourcesSidebar } from "./ResourcesSidebar";
|
||||||
|
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
||||||
import {
|
import {
|
||||||
ActiveTransition,
|
ActiveTransition,
|
||||||
WorkspaceBuildProgress,
|
WorkspaceBuildProgress,
|
||||||
@@ -46,7 +48,7 @@ export interface WorkspaceProps {
|
|||||||
canDebugMode: boolean;
|
canDebugMode: boolean;
|
||||||
handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||||
handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||||
buildLogs?: React.ReactNode;
|
buildLogs?: TypesGen.ProvisionerJobLog[];
|
||||||
latestVersion?: TypesGen.TemplateVersion;
|
latestVersion?: TypesGen.TemplateVersion;
|
||||||
permissions: WorkspacePermissions;
|
permissions: WorkspacePermissions;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
@@ -108,6 +110,14 @@ export const Workspace: FC<WorkspaceProps> = ({
|
|||||||
(r) => resourceOptionValue(r) === resourcesNav.value,
|
(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 (
|
return (
|
||||||
<div
|
<div
|
||||||
css={{
|
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 && (
|
{workspace.latest_build.job.error && (
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
<AlertTitle>Workspace build failed</AlertTitle>
|
<AlertTitle>Workspace build failed</AlertTitle>
|
||||||
@@ -222,7 +244,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{buildLogs}
|
{shouldDisplayBuildLogs && (
|
||||||
|
<WorkspaceBuildLogsSection logs={buildLogs} />
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedResource && (
|
{selectedResource && (
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { pageTitle } from "utils/page";
|
|||||||
import { ChangeVersionDialog } from "./ChangeVersionDialog";
|
import { ChangeVersionDialog } from "./ChangeVersionDialog";
|
||||||
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
||||||
import { Workspace } from "./Workspace";
|
import { Workspace } from "./Workspace";
|
||||||
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
|
||||||
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
||||||
import type { WorkspacePermissions } from "./permissions";
|
import type { WorkspacePermissions } from "./permissions";
|
||||||
|
|
||||||
@@ -71,10 +70,10 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build logs
|
// Build logs
|
||||||
const shouldDisplayBuildLogs = workspace.latest_build.status !== "running";
|
const shouldStreamBuildLogs = workspace.latest_build.status !== "running";
|
||||||
const buildLogs = useWorkspaceBuildLogs(
|
const buildLogs = useWorkspaceBuildLogs(
|
||||||
workspace.latest_build.id,
|
workspace.latest_build.id,
|
||||||
shouldDisplayBuildLogs,
|
shouldStreamBuildLogs,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
@@ -278,11 +277,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
|||||||
buildInfo={buildInfoQuery.data}
|
buildInfo={buildInfoQuery.data}
|
||||||
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
||||||
template={template}
|
template={template}
|
||||||
buildLogs={
|
buildLogs={buildLogs}
|
||||||
shouldDisplayBuildLogs && (
|
|
||||||
<WorkspaceBuildLogsSection logs={buildLogs} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
timings={timingsQuery.data}
|
timings={timingsQuery.data}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1199,6 +1199,10 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
|
|||||||
resources: [MockWorkspaceResource],
|
resources: [MockWorkspaceResource],
|
||||||
status: "running",
|
status: "running",
|
||||||
daily_cost: 20,
|
daily_cost: 20,
|
||||||
|
matched_provisioners: {
|
||||||
|
count: 1,
|
||||||
|
available: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = {
|
export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = {
|
||||||
|
|||||||
Reference in New Issue
Block a user