diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e523745f5e..759671714f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9124,7 +9124,8 @@ const docTemplate = `{ "succeeded", "canceling", "canceled", - "failed" + "failed", + "unknown" ], "x-enum-varnames": [ "ProvisionerJobPending", @@ -9132,7 +9133,8 @@ const docTemplate = `{ "ProvisionerJobSucceeded", "ProvisionerJobCanceling", "ProvisionerJobCanceled", - "ProvisionerJobFailed" + "ProvisionerJobFailed", + "ProvisionerJobUnknown" ] }, "codersdk.ProvisionerLogLevel": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a97a865aae..90272bcbf8 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8211,7 +8211,8 @@ "succeeded", "canceling", "canceled", - "failed" + "failed", + "unknown" ], "x-enum-varnames": [ "ProvisionerJobPending", @@ -8219,7 +8220,8 @@ "ProvisionerJobSucceeded", "ProvisionerJobCanceling", "ProvisionerJobCanceled", - "ProvisionerJobFailed" + "ProvisionerJobFailed", + "ProvisionerJobUnknown" ] }, "codersdk.ProvisionerLogLevel": { diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 294db00344..65e3e90afe 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -13,7 +13,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/provisionerjobs" @@ -303,7 +302,7 @@ func getNextTransition( // isEligibleForAutostart returns true if the workspace should be autostarted. func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool { // Don't attempt to autostart failed workspaces. - if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed { + if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed { return false } @@ -337,7 +336,7 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild // isEligibleForAutostart returns true if the workspace should be autostopped. func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool { - if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed { + if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed { return false } @@ -379,7 +378,7 @@ func isEligibleForFailedStop(build database.WorkspaceBuild, job database.Provisi // If the template has specified a failure TLL. return templateSchedule.FailureTTL > 0 && // And the job resulted in failure. - db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed && + codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed && build.Transition == database.WorkspaceTransitionStart && // And sufficient time has elapsed since the job has completed. job.CompletedAt.Valid && diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index d6a5bf4b69..a2b1d85ac8 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -71,31 +71,6 @@ func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk }, nil } -func ProvisionerJobStatus(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJobStatus { - // The case where jobs are hung is handled by the unhang package. We can't - // just return Failed here when it's hung because that doesn't reflect in - // the database. - switch { - case provisionerJob.CanceledAt.Valid: - if !provisionerJob.CompletedAt.Valid { - return codersdk.ProvisionerJobCanceling - } - if provisionerJob.Error.String == "" { - return codersdk.ProvisionerJobCanceled - } - return codersdk.ProvisionerJobFailed - case !provisionerJob.StartedAt.Valid: - return codersdk.ProvisionerJobPending - case provisionerJob.CompletedAt.Valid: - if provisionerJob.Error.String == "" { - return codersdk.ProvisionerJobSucceeded - } - return codersdk.ProvisionerJobFailed - default: - return codersdk.ProvisionerJobRunning - } -} - func User(user database.User, organizationIDs []uuid.UUID) codersdk.User { convertedUser := codersdk.User{ ID: user.ID, diff --git a/coderd/database/db2sdk/db2sdk_test.go b/coderd/database/db2sdk/db2sdk_test.go index d1d360ad61..a25b44ad92 100644 --- a/coderd/database/db2sdk/db2sdk_test.go +++ b/coderd/database/db2sdk/db2sdk_test.go @@ -4,13 +4,17 @@ import ( "crypto/rand" "database/sql" "encoding/json" + "fmt" "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk/proto" @@ -110,11 +114,36 @@ func TestProvisionerJobStatus(t *testing.T) { }, } - for _, tc := range cases { + // Share db for all job inserts. + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + + for i, tc := range cases { tc := tc + i := i t.Run(tc.name, func(t *testing.T) { t.Parallel() - actual := db2sdk.ProvisionerJobStatus(tc.job) + // Populate standard fields + now := dbtime.Now().Round(time.Minute) + tc.job.ID = uuid.New() + tc.job.CreatedAt = now + tc.job.UpdatedAt = now + tc.job.InitiatorID = org.ID + tc.job.OrganizationID = org.ID + tc.job.Input = []byte("{}") + tc.job.Provisioner = database.ProvisionerTypeEcho + // Unique tags for each job. + tc.job.Tags = map[string]string{fmt.Sprintf("%d", i): "true"} + + inserted := dbgen.ProvisionerJob(t, db, nil, tc.job) + // Make sure the inserted job has the right values. + require.Equal(t, tc.job.StartedAt.Time.UTC(), inserted.StartedAt.Time.UTC(), "started at") + require.Equal(t, tc.job.CompletedAt.Time.UTC(), inserted.CompletedAt.Time.UTC(), "completed at") + require.Equal(t, tc.job.CanceledAt.Time.UTC(), inserted.CanceledAt.Time.UTC(), "cancelled at") + require.Equal(t, tc.job.Error, inserted.Error, "error") + require.Equal(t, tc.job.ErrorCode, inserted.ErrorCode, "error code") + + actual := codersdk.ProvisionerJobStatus(inserted.JobStatus) require.Equal(t, tc.status, actual) }) } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 1e6e4d9d41..1edfffb11c 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -20,7 +20,6 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/rbac" @@ -604,16 +603,6 @@ func (q *FakeQuerier) getGroupByIDNoLock(_ context.Context, id uuid.UUID) (datab return database.Group{}, sql.ErrNoRows } -// isNull is only used in dbfake, so reflect is ok. Use this to make the logic -// look more similar to the postgres. -func isNull(v interface{}) bool { - return !isNotNull(v) -} - -func isNotNull(v interface{}) bool { - return reflect.ValueOf(v).FieldByName("Valid").Bool() -} - // ErrUnimplemented is returned by methods only used by the enterprise/tailnet.pgCoord. This coordinator explicitly // depends on postgres triggers that announce changes on the pubsub. Implementing support for this in the fake // database would strongly couple the FakeQuerier to the pubsub, which is undesirable. Furthermore, it makes little @@ -695,6 +684,36 @@ func minTime(t, u time.Time) time.Time { return u } +func provisonerJobStatus(j database.ProvisionerJob) database.ProvisionerJobStatus { + if isNotNull(j.CompletedAt) { + if j.Error.String != "" { + return database.ProvisionerJobStatusFailed + } + if isNotNull(j.CanceledAt) { + return database.ProvisionerJobStatusCanceled + } + return database.ProvisionerJobStatusSucceeded + } + + if isNotNull(j.CanceledAt) { + return database.ProvisionerJobStatusCanceling + } + if isNull(j.StartedAt) { + return database.ProvisionerJobStatusPending + } + return database.ProvisionerJobStatusRunning +} + +// isNull is only used in dbfake, so reflect is ok. Use this to make the logic +// look more similar to the postgres. +func isNull(v interface{}) bool { + return !isNotNull(v) +} + +func isNotNull(v interface{}) bool { + return reflect.ValueOf(v).FieldByName("Valid").Bool() +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -748,6 +767,7 @@ func (q *FakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu provisionerJob.StartedAt = arg.StartedAt provisionerJob.UpdatedAt = arg.StartedAt.Time provisionerJob.WorkerID = arg.WorkerID + provisionerJob.JobStatus = provisonerJobStatus(provisionerJob) q.provisionerJobs[index] = provisionerJob return provisionerJob, nil } @@ -4077,7 +4097,7 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no if err != nil { return nil, xerrors.Errorf("get provisioner job by ID: %w", err) } - if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed { + if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed { workspaces = append(workspaces, workspace) continue } @@ -4464,6 +4484,7 @@ func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser Input: arg.Input, Tags: arg.Tags, } + job.JobStatus = provisonerJobStatus(job) q.provisionerJobs = append(q.provisionerJobs, job) return job, nil } @@ -5393,6 +5414,7 @@ func (q *FakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.U continue } job.UpdatedAt = arg.UpdatedAt + job.JobStatus = provisonerJobStatus(job) q.provisionerJobs[index] = job return nil } @@ -5413,6 +5435,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg } job.CanceledAt = arg.CanceledAt job.CompletedAt = arg.CompletedAt + job.JobStatus = provisonerJobStatus(job) q.provisionerJobs[index] = job return nil } @@ -5435,6 +5458,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar job.CompletedAt = arg.CompletedAt job.Error = arg.Error job.ErrorCode = arg.ErrorCode + job.JobStatus = provisonerJobStatus(job) q.provisionerJobs[index] = job return nil } @@ -6604,61 +6628,30 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. // This logic should match the logic in the workspace.sql file. var statusMatch bool switch database.WorkspaceStatus(arg.Status) { - case database.WorkspaceStatusPending: - statusMatch = isNull(job.StartedAt) case database.WorkspaceStatusStarting: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && build.Transition == database.WorkspaceTransitionStart - - case database.WorkspaceStatusRunning: - statusMatch = isNotNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && - build.Transition == database.WorkspaceTransitionStart - case database.WorkspaceStatusStopping: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && build.Transition == database.WorkspaceTransitionStop - - case database.WorkspaceStatusStopped: - statusMatch = isNotNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusFailed: - statusMatch = (isNotNull(job.CanceledAt) && isNotNull(job.Error)) || - (isNotNull(job.CompletedAt) && isNotNull(job.Error)) - - case database.WorkspaceStatusCanceling: - statusMatch = isNotNull(job.CanceledAt) && - isNull(job.CompletedAt) - - case database.WorkspaceStatusCanceled: - statusMatch = isNotNull(job.CanceledAt) && - isNotNull(job.CompletedAt) - - case database.WorkspaceStatusDeleted: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNotNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && - build.Transition == database.WorkspaceTransitionDelete && - isNull(job.Error) - case database.WorkspaceStatusDeleting: - statusMatch = isNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && build.Transition == database.WorkspaceTransitionDelete + case "started": + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart + case database.WorkspaceStatusDeleted: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionDelete + case database.WorkspaceStatusStopped: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusRunning: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart default: - return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) + statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status) } if !statusMatch { continue diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index d460c3fbaf..5397ca1e3d 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -328,16 +328,16 @@ func GroupMember(t testing.TB, db database.Store, orig database.GroupMember) dat // ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps // can be set to nil if you are SURE that you don't require a provisionerdaemon to acquire the job in your test. func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig database.ProvisionerJob) database.ProvisionerJob { - id := takeFirst(orig.ID, uuid.New()) + jobID := takeFirst(orig.ID, uuid.New()) // Always set some tags to prevent Acquire from grabbing jobs it should not. if !orig.StartedAt.Time.IsZero() { if orig.Tags == nil { orig.Tags = make(database.StringMap) } // Make sure when we acquire the job, we only get this one. - orig.Tags[id.String()] = "true" + orig.Tags[jobID.String()] = "true" } - jobID := takeFirst(orig.ID, uuid.New()) + job, err := db.InsertProvisionerJob(genCtx, database.InsertProvisionerJobParams{ ID: jobID, CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), @@ -365,6 +365,8 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data WorkerID: uuid.NullUUID{}, }) require.NoError(t, err) + // There is no easy way to make sure we acquire the correct job. + require.Equal(t, jobID, job.ID, "acquired incorrect job") } if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 17bb9c539e..73b9cf66ad 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -89,6 +89,18 @@ CREATE TYPE parameter_type_system AS ENUM ( 'hcl' ); +CREATE TYPE provisioner_job_status AS ENUM ( + 'pending', + 'running', + 'succeeded', + 'canceling', + 'canceled', + 'failed', + 'unknown' +); + +COMMENT ON TYPE provisioner_job_status IS 'Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.'; + CREATE TYPE provisioner_job_type AS ENUM ( 'template_version_import', 'workspace_build', @@ -500,9 +512,27 @@ CREATE TABLE provisioner_jobs ( file_id uuid NOT NULL, tags jsonb DEFAULT '{"scope": "organization"}'::jsonb NOT NULL, error_code text, - trace_metadata jsonb + trace_metadata jsonb, + job_status provisioner_job_status GENERATED ALWAYS AS ( +CASE + WHEN (completed_at IS NOT NULL) THEN + CASE + WHEN (error <> ''::text) THEN 'failed'::provisioner_job_status + WHEN (canceled_at IS NOT NULL) THEN 'canceled'::provisioner_job_status + ELSE 'succeeded'::provisioner_job_status + END + ELSE + CASE + WHEN (error <> ''::text) THEN 'failed'::provisioner_job_status + WHEN (canceled_at IS NOT NULL) THEN 'canceling'::provisioner_job_status + WHEN (started_at IS NULL) THEN 'pending'::provisioner_job_status + ELSE 'running'::provisioner_job_status + END +END) STORED NOT NULL ); +COMMENT ON COLUMN provisioner_jobs.job_status IS 'Computed column to track the status of the job.'; + CREATE TABLE replicas ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, diff --git a/coderd/database/migrations/000160_provisioner_job_status.down.sql b/coderd/database/migrations/000160_provisioner_job_status.down.sql new file mode 100644 index 0000000000..3f04c8dd11 --- /dev/null +++ b/coderd/database/migrations/000160_provisioner_job_status.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE provisioner_jobs DROP COLUMN job_status; +DROP TYPE provisioner_job_status; + +COMMIT; diff --git a/coderd/database/migrations/000160_provisioner_job_status.up.sql b/coderd/database/migrations/000160_provisioner_job_status.up.sql new file mode 100644 index 0000000000..9cfea7fbfb --- /dev/null +++ b/coderd/database/migrations/000160_provisioner_job_status.up.sql @@ -0,0 +1,38 @@ +BEGIN; + +CREATE TYPE provisioner_job_status AS ENUM ('pending', 'running', 'succeeded', 'canceling', 'canceled', 'failed', 'unknown'); +COMMENT ON TYPE provisioner_job_status IS 'Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.'; + +ALTER TABLE provisioner_jobs ADD COLUMN + job_status provisioner_job_status NOT NULL GENERATED ALWAYS AS ( + CASE + -- Completed means it is not in an "-ing" state + WHEN completed_at IS NOT NULL THEN + CASE + -- The order of these checks are important. + -- Check the error first, then cancelled, then completed. + WHEN error != '' THEN 'failed'::provisioner_job_status + WHEN canceled_at IS NOT NULL THEN 'canceled'::provisioner_job_status + ELSE 'succeeded'::provisioner_job_status + END + -- Not completed means it is in some "-ing" state + ELSE + CASE + -- This should never happen because all errors set + -- should also set a completed_at timestamp. + -- But if there is an error, we should always return + -- a failed state. + WHEN error != '' THEN 'failed'::provisioner_job_status + WHEN canceled_at IS NOT NULL THEN 'canceling'::provisioner_job_status + -- Not done and not started means it is pending + WHEN started_at IS NULL THEN 'pending'::provisioner_job_status + ELSE 'running'::provisioner_job_status + END + END + -- Stored so we do not have to recompute it every time. + ) STORED; + + +COMMENT ON COLUMN provisioner_jobs.job_status IS 'Computed column to track the status of the job.'; + +COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index ab6d8861ee..bc9ba550ef 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -837,6 +837,80 @@ func AllParameterTypeSystemValues() []ParameterTypeSystem { } } +// Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state. +type ProvisionerJobStatus string + +const ( + ProvisionerJobStatusPending ProvisionerJobStatus = "pending" + ProvisionerJobStatusRunning ProvisionerJobStatus = "running" + ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded" + ProvisionerJobStatusCanceling ProvisionerJobStatus = "canceling" + ProvisionerJobStatusCanceled ProvisionerJobStatus = "canceled" + ProvisionerJobStatusFailed ProvisionerJobStatus = "failed" + ProvisionerJobStatusUnknown ProvisionerJobStatus = "unknown" +) + +func (e *ProvisionerJobStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ProvisionerJobStatus(s) + case string: + *e = ProvisionerJobStatus(s) + default: + return fmt.Errorf("unsupported scan type for ProvisionerJobStatus: %T", src) + } + return nil +} + +type NullProvisionerJobStatus struct { + ProvisionerJobStatus ProvisionerJobStatus `json:"provisioner_job_status"` + Valid bool `json:"valid"` // Valid is true if ProvisionerJobStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullProvisionerJobStatus) Scan(value interface{}) error { + if value == nil { + ns.ProvisionerJobStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ProvisionerJobStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullProvisionerJobStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ProvisionerJobStatus), nil +} + +func (e ProvisionerJobStatus) Valid() bool { + switch e { + case ProvisionerJobStatusPending, + ProvisionerJobStatusRunning, + ProvisionerJobStatusSucceeded, + ProvisionerJobStatusCanceling, + ProvisionerJobStatusCanceled, + ProvisionerJobStatusFailed, + ProvisionerJobStatusUnknown: + return true + } + return false +} + +func AllProvisionerJobStatusValues() []ProvisionerJobStatus { + return []ProvisionerJobStatus{ + ProvisionerJobStatusPending, + ProvisionerJobStatusRunning, + ProvisionerJobStatusSucceeded, + ProvisionerJobStatusCanceling, + ProvisionerJobStatusCanceled, + ProvisionerJobStatusFailed, + ProvisionerJobStatusUnknown, + } +} + type ProvisionerJobType string const ( @@ -1671,6 +1745,8 @@ type ProvisionerJob struct { Tags StringMap `db:"tags" json:"tags"` ErrorCode sql.NullString `db:"error_code" json:"error_code"` TraceMetadata pqtype.NullRawMessage `db:"trace_metadata" json:"trace_metadata"` + // Computed column to track the status of the job. + JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"` } type ProvisionerJobLog struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 40b7ee7e72..a2949bb542 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2988,7 +2988,7 @@ WHERE SKIP LOCKED LIMIT 1 - ) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata + ) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status ` type AcquireProvisionerJobParams struct { @@ -3031,13 +3031,14 @@ func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvi &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ) return i, err } const getHungProvisionerJobs = `-- name: GetHungProvisionerJobs :many SELECT - id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata + id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status FROM provisioner_jobs WHERE @@ -3074,6 +3075,7 @@ func (q *sqlQuerier) GetHungProvisionerJobs(ctx context.Context, updatedAt time. &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ); err != nil { return nil, err } @@ -3090,7 +3092,7 @@ func (q *sqlQuerier) GetHungProvisionerJobs(ctx context.Context, updatedAt time. const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one SELECT - id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata + id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status FROM provisioner_jobs WHERE @@ -3119,13 +3121,14 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ) return i, err } const getProvisionerJobsByIDs = `-- name: GetProvisionerJobsByIDs :many SELECT - id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata + id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status FROM provisioner_jobs WHERE @@ -3160,6 +3163,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUI &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ); err != nil { return nil, err } @@ -3194,7 +3198,7 @@ queue_size AS ( SELECT COUNT(*) as count FROM unstarted_jobs ) SELECT - pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, + pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, COALESCE(qp.queue_position, 0) AS queue_position, COALESCE(qs.count, 0) AS queue_size FROM @@ -3241,6 +3245,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex &i.ProvisionerJob.Tags, &i.ProvisionerJob.ErrorCode, &i.ProvisionerJob.TraceMetadata, + &i.ProvisionerJob.JobStatus, &i.QueuePosition, &i.QueueSize, ); err != nil { @@ -3258,7 +3263,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex } const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many -SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata FROM provisioner_jobs WHERE created_at > $1 +SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status FROM provisioner_jobs WHERE created_at > $1 ` func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) { @@ -3289,6 +3294,7 @@ func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, created &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ); err != nil { return nil, err } @@ -3320,7 +3326,7 @@ INSERT INTO trace_metadata ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status ` type InsertProvisionerJobParams struct { @@ -3373,6 +3379,7 @@ func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisi &i.Tags, &i.ErrorCode, &i.TraceMetadata, + &i.JobStatus, ) return i, err } @@ -9841,7 +9848,8 @@ LEFT JOIN LATERAL ( provisioner_jobs.updated_at, provisioner_jobs.canceled_at, provisioner_jobs.completed_at, - provisioner_jobs.error + provisioner_jobs.error, + provisioner_jobs.job_status FROM workspace_builds LEFT JOIN @@ -9873,63 +9881,42 @@ WHERE AND CASE WHEN $2 :: text != '' THEN CASE - WHEN $2 = 'pending' THEN - latest_build.started_at IS NULL + -- Some workspace specific status refer to the transition + -- type. By default, the standard provisioner job status + -- search strings are supported. + -- 'running' states WHEN $2 = 'starting' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition - - WHEN $2 = 'running' THEN - latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'start'::workspace_transition - WHEN $2 = 'stopping' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition - - WHEN $2 = 'stopped' THEN - latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'stop'::workspace_transition - - WHEN $2 = 'failed' THEN - (latest_build.canceled_at IS NOT NULL AND - latest_build.error IS NOT NULL) OR - (latest_build.completed_at IS NOT NULL AND - latest_build.error IS NOT NULL) - - WHEN $2 = 'canceling' THEN - latest_build.canceled_at IS NOT NULL AND - latest_build.completed_at IS NULL - - WHEN $2 = 'canceled' THEN - latest_build.canceled_at IS NOT NULL AND - latest_build.completed_at IS NOT NULL - - WHEN $2 = 'deleted' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND - latest_build.transition = 'delete'::workspace_transition AND - -- If the error field is not null, the status is 'failed' - latest_build.error IS NULL - WHEN $2 = 'deleting' THEN - latest_build.completed_at IS NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND + latest_build.job_status = 'running' AND latest_build.transition = 'delete'::workspace_transition + -- 'succeeded' states + WHEN $2 = 'deleted' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'delete'::workspace_transition + WHEN $2 = 'stopped' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'stop'::workspace_transition + WHEN $2 = 'started' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'start'::workspace_transition + + -- Special case where the provisioner status and workspace status + -- differ. A workspace is "running" if the job is "succeeded" and + -- the transition is "start". This is because a workspace starts + -- running when a job is complete. + WHEN $2 = 'running' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'start'::workspace_transition + + WHEN $2 != '' THEN + -- By default just match the job status exactly + latest_build.job_status = $2::provisioner_job_status ELSE true END diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 0aa073301e..df5c46ff2a 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -96,7 +96,8 @@ LEFT JOIN LATERAL ( provisioner_jobs.updated_at, provisioner_jobs.canceled_at, provisioner_jobs.completed_at, - provisioner_jobs.error + provisioner_jobs.error, + provisioner_jobs.job_status FROM workspace_builds LEFT JOIN @@ -128,63 +129,42 @@ WHERE AND CASE WHEN @status :: text != '' THEN CASE - WHEN @status = 'pending' THEN - latest_build.started_at IS NULL + -- Some workspace specific status refer to the transition + -- type. By default, the standard provisioner job status + -- search strings are supported. + -- 'running' states WHEN @status = 'starting' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition - - WHEN @status = 'running' THEN - latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'start'::workspace_transition - WHEN @status = 'stopping' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition - - WHEN @status = 'stopped' THEN - latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'stop'::workspace_transition - - WHEN @status = 'failed' THEN - (latest_build.canceled_at IS NOT NULL AND - latest_build.error IS NOT NULL) OR - (latest_build.completed_at IS NOT NULL AND - latest_build.error IS NOT NULL) - - WHEN @status = 'canceling' THEN - latest_build.canceled_at IS NOT NULL AND - latest_build.completed_at IS NULL - - WHEN @status = 'canceled' THEN - latest_build.canceled_at IS NOT NULL AND - latest_build.completed_at IS NOT NULL - - WHEN @status = 'deleted' THEN - latest_build.started_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND - latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND - latest_build.transition = 'delete'::workspace_transition AND - -- If the error field is not null, the status is 'failed' - latest_build.error IS NULL - WHEN @status = 'deleting' THEN - latest_build.completed_at IS NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND + latest_build.job_status = 'running' AND latest_build.transition = 'delete'::workspace_transition + -- 'succeeded' states + WHEN @status = 'deleted' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'delete'::workspace_transition + WHEN @status = 'stopped' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'stop'::workspace_transition + WHEN @status = 'started' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'start'::workspace_transition + + -- Special case where the provisioner status and workspace status + -- differ. A workspace is "running" if the job is "succeeded" and + -- the transition is "start". This is because a workspace starts + -- running when a job is complete. + WHEN @status = 'running' THEN + latest_build.job_status = 'succeeded'::provisioner_job_status AND + latest_build.transition = 'start'::workspace_transition + + WHEN @status != '' THEN + -- By default just match the job status exactly + latest_build.job_status = @status::provisioner_job_status ELSE true END diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index b020912f99..7145c2afa3 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -10,13 +10,14 @@ import ( "sync/atomic" "time" + "github.com/coder/coder/v2/codersdk" + "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "tailscale.com/tailcfg" "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/tailnet" @@ -119,7 +120,7 @@ func Workspaces(ctx context.Context, registerer prometheus.Registerer, db databa gauge.Reset() for _, job := range jobs { - status := db2sdk.ProvisionerJobStatus(job) + status := codersdk.ProvisionerJobStatus(job.JobStatus) gauge.WithLabelValues(string(status)).Add(1) } } diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 5e3d27ee32..315fe38593 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -17,7 +17,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/httpapi" @@ -258,7 +257,7 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR if provisionerJob.WorkerID.Valid { job.WorkerID = &provisionerJob.WorkerID.UUID } - job.Status = db2sdk.ProvisionerJobStatus(provisionerJob) + job.Status = codersdk.ProvisionerJobStatus(pj.ProvisionerJob.JobStatus) return job } @@ -282,7 +281,7 @@ func fetchAndWriteLogs(ctx context.Context, db database.Store, jobID uuid.UUID, } func jobIsComplete(logger slog.Logger, job database.ProvisionerJob) bool { - status := db2sdk.ProvisionerJobStatus(job) + status := codersdk.ProvisionerJobStatus(job.JobStatus) switch status { case codersdk.ProvisionerJobCanceled: return true diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index 6168ade5ff..05fddb722b 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -44,8 +44,10 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { expected codersdk.ProvisionerJob }{ { - name: "empty", - input: database.ProvisionerJob{}, + name: "empty", + input: database.ProvisionerJob{ + JobStatus: database.ProvisionerJobStatusPending, + }, expected: codersdk.ProvisionerJob{ Status: codersdk.ProvisionerJobPending, }, @@ -55,6 +57,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { input: database.ProvisionerJob{ CanceledAt: validNullTimeMock, CompletedAt: invalidNullTimeMock, + JobStatus: database.ProvisionerJobStatusCanceling, }, expected: codersdk.ProvisionerJob{ CanceledAt: &validNullTimeMock.Time, @@ -67,6 +70,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { CanceledAt: validNullTimeMock, CompletedAt: validNullTimeMock, Error: errorMock, + JobStatus: database.ProvisionerJobStatusFailed, }, expected: codersdk.ProvisionerJob{ CanceledAt: &validNullTimeMock.Time, @@ -80,6 +84,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { input: database.ProvisionerJob{ CanceledAt: validNullTimeMock, CompletedAt: validNullTimeMock, + JobStatus: database.ProvisionerJobStatusCanceled, }, expected: codersdk.ProvisionerJob{ CanceledAt: &validNullTimeMock.Time, @@ -91,6 +96,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { name: "job pending", input: database.ProvisionerJob{ StartedAt: invalidNullTimeMock, + JobStatus: database.ProvisionerJobStatusPending, }, expected: codersdk.ProvisionerJob{ Status: codersdk.ProvisionerJobPending, @@ -102,6 +108,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { CompletedAt: validNullTimeMock, StartedAt: validNullTimeMock, Error: errorMock, + JobStatus: database.ProvisionerJobStatusFailed, }, expected: codersdk.ProvisionerJob{ CompletedAt: &validNullTimeMock.Time, @@ -115,6 +122,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { input: database.ProvisionerJob{ CompletedAt: validNullTimeMock, StartedAt: validNullTimeMock, + JobStatus: database.ProvisionerJobStatusSucceeded, }, expected: codersdk.ProvisionerJob{ CompletedAt: &validNullTimeMock.Time, @@ -156,7 +164,8 @@ func Test_logFollower_completeBeforeFollow(t *testing.T) { Time: now.Add(-time.Second), Valid: true, }, - Error: sql.NullString{}, + Error: sql.NullString{}, + JobStatus: database.ProvisionerJobStatusSucceeded, } // we need an HTTP server to get a websocket @@ -217,6 +226,7 @@ func Test_logFollower_completeBeforeSubscribe(t *testing.T) { CanceledAt: sql.NullTime{}, CompletedAt: sql.NullTime{}, Error: sql.NullString{}, + JobStatus: database.ProvisionerJobStatusRunning, } // we need an HTTP server to get a websocket @@ -238,6 +248,7 @@ func Test_logFollower_completeBeforeSubscribe(t *testing.T) { Time: now.Add(-time.Millisecond), Valid: true, }, + JobStatus: database.ProvisionerJobStatusSucceeded, }, nil, ) @@ -293,6 +304,7 @@ func Test_logFollower_EndOfLogs(t *testing.T) { CanceledAt: sql.NullTime{}, CompletedAt: sql.NullTime{}, Error: sql.NullString{}, + JobStatus: database.ProvisionerJobStatusRunning, } // we need an HTTP server to get a websocket diff --git a/coderd/unhanger/detector.go b/coderd/unhanger/detector.go index c250711b52..9a3440f705 100644 --- a/coderd/unhanger/detector.go +++ b/coderd/unhanger/detector.go @@ -14,7 +14,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" @@ -240,7 +239,7 @@ func unhangJob(ctx context.Context, log slog.Logger, db database.Store, pub pubs } if job.CompletedAt.Valid { return jobInelligibleError{ - Err: xerrors.Errorf("job is completed (status %s)", db2sdk.ProvisionerJobStatus(job)), + Err: xerrors.Errorf("job is completed (status %s)", job.JobStatus), } } if job.UpdatedAt.After(time.Now().Add(-HungJobDuration)) { diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index f7c1699d29..008bc88ab7 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -718,7 +718,7 @@ func (b *Builder) checkTemplateJobStatus() error { } } - templateVersionJobStatus := db2sdk.ProvisionerJobStatus(*templateVersionJob) + templateVersionJobStatus := codersdk.ProvisionerJobStatus(templateVersionJob.JobStatus) switch templateVersionJobStatus { case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning: msg := fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus) @@ -755,7 +755,7 @@ func (b *Builder) checkRunningBuild() error { if err != nil { return BuildError{http.StatusInternalServerError, "failed to fetch prior build", err} } - if db2sdk.ProvisionerJobStatus(*job).Active() { + if codersdk.ProvisionerJobStatus(job.JobStatus).Active() { msg := "A workspace build is already active." return BuildError{ http.StatusConflict, diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 4e787c6fe1..ce2dd08758 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -64,6 +64,7 @@ const ( ProvisionerJobCanceling ProvisionerJobStatus = "canceling" ProvisionerJobCanceled ProvisionerJobStatus = "canceled" ProvisionerJobFailed ProvisionerJobStatus = "failed" + ProvisionerJobUnknown ProvisionerJobStatus = "unknown" ) // JobErrorCode defines the error code returned by job runner. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 7413bf4b48..e02b0f818d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3758,6 +3758,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `canceling` | | `canceled` | | `failed` | +| `unknown` | ## codersdk.ProvisionerLogLevel diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index b686bd7f9f..c78d971876 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -11,7 +11,6 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" agpl "github.com/coder/coder/v2/coderd/schedule" @@ -242,7 +241,7 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte if err != nil { return xerrors.Errorf("get provisioner job %q: %w", build.JobID, err) } - if db2sdk.ProvisionerJobStatus(job) != codersdk.ProvisionerJobSucceeded { + if codersdk.ProvisionerJobStatus(job.JobStatus) != codersdk.ProvisionerJobSucceeded { // Only touch builds that are completed. return nil } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3ce72f96db..6f9dc91527 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1755,7 +1755,8 @@ export type ProvisionerJobStatus = | "failed" | "pending" | "running" - | "succeeded"; + | "succeeded" + | "unknown"; export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ "canceled", "canceling", @@ -1763,6 +1764,7 @@ export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ "pending", "running", "succeeded", + "unknown", ]; // From codersdk/workspaces.go diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx index 1580cad6a4..efe63454c2 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx @@ -55,6 +55,7 @@ export const getStatus = ( text: "Canceled", icon: , }; + case "unknown": case "failed": return { type: "error", diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 1cc36f4568..8e4c6596e4 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -51,6 +51,8 @@ export const getDisplayWorkspaceBuildStatus = ( color: theme.palette.primary.main, status: DisplayWorkspaceBuildStatusLanguage.running, } as const; + // Just handle unknown as failed + case "unknown": case "failed": return { type: "error",