mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: don't create autostart workspace builds with no available provisioners (#19067)
This should fix https://github.com/coder/coder/issues/17941 by introducing a check for whether there are any valid (non-stale provisioners for a job in the autobuild executor code path. --------- Signed-off-by: Callum Styan <callumstyan@gmail.com>
This commit is contained in:
@@ -55,6 +55,7 @@ import (
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/archive"
|
||||
"github.com/coder/coder/v2/coderd/files"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/wsbuilder"
|
||||
"github.com/coder/quartz"
|
||||
|
||||
@@ -386,6 +387,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
options.NotificationsEnqueuer,
|
||||
experiments,
|
||||
).WithStatsChannel(options.AutobuildStats)
|
||||
|
||||
lifecycleExecutor.Run()
|
||||
|
||||
jobReaperTicker := time.NewTicker(options.DeploymentValues.JobReaperDetectorInterval.Value())
|
||||
@@ -1590,3 +1592,112 @@ func DeploymentValues(t testing.TB, mut ...func(*codersdk.DeploymentValues)) *co
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// GetProvisionerForTags returns the first valid provisioner for a workspace + template tags.
|
||||
func GetProvisionerForTags(tx database.Store, curTime time.Time, orgID uuid.UUID, tags map[string]string) (database.ProvisionerDaemon, error) {
|
||||
if tags == nil {
|
||||
tags = map[string]string{}
|
||||
}
|
||||
queryParams := database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: orgID,
|
||||
WantTags: tags,
|
||||
}
|
||||
|
||||
// nolint: gocritic // The user (in this case, the user/context for autostart builds) may not have the full
|
||||
// permissions to read provisioner daemons, but we need to check if there's any for the job prior to the
|
||||
// execution of the job via autostart to fix: https://github.com/coder/coder/issues/17941
|
||||
provisionerDaemons, err := tx.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(context.Background()), queryParams)
|
||||
if err != nil {
|
||||
return database.ProvisionerDaemon{}, xerrors.Errorf("get provisioner daemons: %w", err)
|
||||
}
|
||||
|
||||
// Check if any provisioners are active (not stale)
|
||||
for _, pd := range provisionerDaemons {
|
||||
if pd.LastSeenAt.Valid {
|
||||
age := curTime.Sub(pd.LastSeenAt.Time)
|
||||
if age <= provisionerdserver.StaleInterval {
|
||||
return pd, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return database.ProvisionerDaemon{}, xerrors.New("no available provisioners found")
|
||||
}
|
||||
|
||||
func ctxWithProvisionerPermissions(ctx context.Context) context.Context {
|
||||
// Use system restricted context which has permissions to update provisioner daemons
|
||||
//nolint: gocritic // We need system context to modify this.
|
||||
return dbauthz.AsSystemRestricted(ctx)
|
||||
}
|
||||
|
||||
// UpdateProvisionerLastSeenAt updates the provisioner daemon's LastSeenAt timestamp
|
||||
// to the specified time to prevent it from appearing stale during autobuild operations
|
||||
func UpdateProvisionerLastSeenAt(t *testing.T, db database.Store, id uuid.UUID, tickTime time.Time) {
|
||||
t.Helper()
|
||||
ctx := ctxWithProvisionerPermissions(context.Background())
|
||||
t.Logf("Updating provisioner %s LastSeenAt to %v", id, tickTime)
|
||||
err := db.UpdateProvisionerDaemonLastSeenAt(ctx, database.UpdateProvisionerDaemonLastSeenAtParams{
|
||||
ID: id,
|
||||
LastSeenAt: sql.NullTime{Time: tickTime, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("Successfully updated provisioner LastSeenAt")
|
||||
}
|
||||
|
||||
func MustWaitForAnyProvisioner(t *testing.T, db database.Store) {
|
||||
t.Helper()
|
||||
ctx := ctxWithProvisionerPermissions(testutil.Context(t, testutil.WaitShort))
|
||||
require.Eventually(t, func() bool {
|
||||
daemons, err := db.GetProvisionerDaemons(ctx)
|
||||
return err == nil && len(daemons) > 0
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
}
|
||||
|
||||
// MustWaitForProvisionersAvailable waits for provisioners to be available for a specific workspace.
|
||||
func MustWaitForProvisionersAvailable(t *testing.T, db database.Store, workspace codersdk.Workspace) uuid.UUID {
|
||||
t.Helper()
|
||||
ctx := ctxWithProvisionerPermissions(testutil.Context(t, testutil.WaitShort))
|
||||
id := uuid.UUID{}
|
||||
// Get the workspace from the database
|
||||
require.Eventually(t, func() bool {
|
||||
ws, err := db.GetWorkspaceByID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the latest build
|
||||
latestBuild, err := db.GetWorkspaceBuildByID(ctx, workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the template version job
|
||||
templateVersionJob, err := db.GetProvisionerJobByID(ctx, latestBuild.JobID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if provisioners are available using the same logic as hasAvailableProvisioners
|
||||
provisionerDaemons, err := db.GetProvisionerDaemonsByOrganization(ctx, database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: ws.OrganizationID,
|
||||
WantTags: templateVersionJob.Tags,
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if any provisioners are active (not stale)
|
||||
now := time.Now()
|
||||
for _, pd := range provisionerDaemons {
|
||||
if pd.LastSeenAt.Valid {
|
||||
age := now.Sub(pd.LastSeenAt.Time)
|
||||
if age <= provisionerdserver.StaleInterval {
|
||||
id = pd.ID
|
||||
return true // Found an active provisioner
|
||||
}
|
||||
}
|
||||
}
|
||||
return false // No active provisioners found
|
||||
}, testutil.WaitLong, testutil.IntervalFast)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user