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
+50
View File
@@ -29,6 +29,7 @@ import (
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/wsbuilder"
"github.com/coder/coder/v2/codersdk"
@@ -132,6 +133,39 @@ func (e *Executor) Run() {
})
}
// hasValidProvisioner checks whether there is at least one valid (non-stale, correct tags) provisioner
// based on time t and the tags maps (such as from a templateVersionJob).
func (e *Executor) hasValidProvisioner(ctx context.Context, tx database.Store, t time.Time, ws database.Workspace, tags map[string]string) (bool, error) {
queryParams := database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: ws.OrganizationID,
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(ctx), queryParams)
if err != nil {
return false, xerrors.Errorf("get provisioner daemons: %w", err)
}
logger := e.log.With(slog.F("tags", tags))
// Check if any provisioners are active (not stale)
for _, pd := range provisionerDaemons {
if pd.LastSeenAt.Valid {
age := t.Sub(pd.LastSeenAt.Time)
if age <= provisionerdserver.StaleInterval {
logger.Debug(ctx, "hasValidProvisioner: found active provisioner",
slog.F("daemon_id", pd.ID),
)
return true, nil
}
}
}
logger.Debug(ctx, "hasValidProvisioner: no active provisioners found")
return false, nil
}
func (e *Executor) runOnce(t time.Time) Stats {
stats := Stats{
Transitions: make(map[uuid.UUID]database.WorkspaceTransition),
@@ -281,6 +315,22 @@ func (e *Executor) runOnce(t time.Time) Stats {
return nil
}
// Get the template version job to access tags
templateVersionJob, err := tx.GetProvisionerJobByID(e.ctx, activeTemplateVersion.JobID)
if err != nil {
return xerrors.Errorf("get template version job: %w", err)
}
// Before creating the workspace build, check for available provisioners
hasProvisioners, err := e.hasValidProvisioner(e.ctx, tx, t, ws, templateVersionJob.Tags)
if err != nil {
return xerrors.Errorf("check provisioner availability: %w", err)
}
if !hasProvisioners {
log.Warn(e.ctx, "skipping autostart - no available provisioners")
return nil // Skip this workspace
}
if nextTransition != "" {
builder := wsbuilder.New(ws, nextTransition, *e.buildUsageChecker.Load()).
SetLastWorkspaceBuildInTx(&latestBuild).