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:
Callum Styan
2025-08-15 08:50:51 -07:00
committed by GitHub
parent a25d85631b
commit 6c902a7410
4 changed files with 476 additions and 84 deletions
+111
View File
@@ -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
}