diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index d8a07dd20680e..0ef065dd86a81 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -43,7 +43,12 @@ "scope": "organization" }, "queue_position": 0, - "queue_size": 0 + "queue_size": 0, + "organization_id": "===========[first org ID]===========", + "input": { + "workspace_build_id": "========[workspace build ID]========" + }, + "type": "workspace_build" }, "reason": "initiator", "resources": [], diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3b319226e043f..e034492d7882f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3025,6 +3025,71 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/provisionerjobs": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Organizations" + ], + "summary": "Get provisioner jobs", + "operationId": "get-provisioner-jobs", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed", + "unknown", + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "type": "string", + "description": "Filter results by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerJob" + } + } + } + } + } + }, "/organizations/{organization}/provisionerkeys": { "get": { "security": [ @@ -12561,6 +12626,13 @@ const docTemplate = `{ "codersdk.ProvisionerJob": { "type": "object", "properties": { + "available_workers": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, "canceled_at": { "type": "string", "format": "date-time" @@ -12594,6 +12666,13 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -12625,12 +12704,31 @@ const docTemplate = `{ "type": "string" } }, + "type": { + "$ref": "#/definitions/codersdk.ProvisionerJobType" + }, "worker_id": { "type": "string", "format": "uuid" } } }, + "codersdk.ProvisionerJobInput": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "template_version_id": { + "type": "string", + "format": "uuid" + }, + "workspace_build_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerJobLog": { "type": "object", "properties": { @@ -12687,6 +12785,19 @@ const docTemplate = `{ "ProvisionerJobUnknown" ] }, + "codersdk.ProvisionerJobType": { + "type": "string", + "enum": [ + "template_version_import", + "workspace_build", + "template_version_dry_run" + ], + "x-enum-varnames": [ + "ProvisionerJobTypeTemplateVersionImport", + "ProvisionerJobTypeWorkspaceBuild", + "ProvisionerJobTypeTemplateVersionDryRun" + ] + }, "codersdk.ProvisionerKey": { "type": "object", "properties": { @@ -12900,6 +13011,7 @@ const docTemplate = `{ "organization", "organization_member", "provisioner_daemon", + "provisioner_jobs", "provisioner_keys", "replicas", "system", @@ -12934,6 +13046,7 @@ const docTemplate = `{ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerJobs", "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c4611109df79b..ca71f58dbe195 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2657,6 +2657,67 @@ } } }, + "/organizations/{organization}/provisionerjobs": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Organizations"], + "summary": "Get provisioner jobs", + "operationId": "get-provisioner-jobs", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed", + "unknown", + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "type": "string", + "description": "Filter results by status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerJob" + } + } + } + } + } + }, "/organizations/{organization}/provisionerkeys": { "get": { "security": [ @@ -11334,6 +11395,13 @@ "codersdk.ProvisionerJob": { "type": "object", "properties": { + "available_workers": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, "canceled_at": { "type": "string", "format": "date-time" @@ -11365,6 +11433,13 @@ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -11396,12 +11471,31 @@ "type": "string" } }, + "type": { + "$ref": "#/definitions/codersdk.ProvisionerJobType" + }, "worker_id": { "type": "string", "format": "uuid" } } }, + "codersdk.ProvisionerJobInput": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "template_version_id": { + "type": "string", + "format": "uuid" + }, + "workspace_build_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerJobLog": { "type": "object", "properties": { @@ -11452,6 +11546,19 @@ "ProvisionerJobUnknown" ] }, + "codersdk.ProvisionerJobType": { + "type": "string", + "enum": [ + "template_version_import", + "workspace_build", + "template_version_dry_run" + ], + "x-enum-varnames": [ + "ProvisionerJobTypeTemplateVersionImport", + "ProvisionerJobTypeWorkspaceBuild", + "ProvisionerJobTypeTemplateVersionDryRun" + ] + }, "codersdk.ProvisionerKey": { "type": "object", "properties": { @@ -11647,6 +11754,7 @@ "organization", "organization_member", "provisioner_daemon", + "provisioner_jobs", "provisioner_keys", "replicas", "system", @@ -11681,6 +11789,7 @@ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerJobs", "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", diff --git a/coderd/coderd.go b/coderd/coderd.go index 4cd44f7cc64c0..9530f7cef10c9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1010,6 +1010,9 @@ func New(options *Options) *API { r.Route("/provisionerdaemons", func(r chi.Router) { r.Get("/", api.provisionerDaemons) }) + r.Route("/provisionerjobs", func(r chi.Router) { + r.Get("/", api.provisionerJobs) + }) }) }) r.Route("/templates", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b2f0491949238..b51b65e3261b8 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -5,7 +5,6 @@ import ( "database/sql" "encoding/json" "errors" - "fmt" "strings" "sync/atomic" "testing" @@ -48,7 +47,11 @@ type NotAuthorizedError struct { var _ httpapiconstraints.IsUnauthorizedError = (*NotAuthorizedError)(nil) func (e NotAuthorizedError) Error() string { - return fmt.Sprintf("unauthorized: %s", e.Err.Error()) + var detail string + if e.Err != nil { + detail = ": " + e.Err.Error() + } + return "unauthorized" + detail } // IsUnauthorized implements the IsUnauthorized interface. @@ -1975,7 +1978,7 @@ func (q *querier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uui return q.db.GetProvisionerJobTimingsByJobID(ctx, jobID) } -// TODO: we need to add a provisioner job resource +// TODO: We have a ProvisionerJobs resource, but it hasn't been checked for this use-case. func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { // return nil, err @@ -1983,12 +1986,16 @@ func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) return q.db.GetProvisionerJobsByIDs(ctx, ids) } -// TODO: we need to add a provisioner job resource +// TODO: We have a ProvisionerJobs resource, but it hasn't been checked for this use-case. func (q *querier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { return q.db.GetProvisionerJobsByIDsWithQueuePosition(ctx, ids) } -// TODO: We need to create a ProvisionerJob resource type +func (q *querier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner)(ctx, arg) +} + +// TODO: We have a ProvisionerJobs resource, but it hasn't been checked for this use-case. func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { // return nil, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 5ad8dca748c3f..f451e3a05da28 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3257,6 +3257,38 @@ func (s *MethodTestSuite) TestExtraMethods() { LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, }).Asserts(rbac.ResourceProvisionerDaemon, policy.ActionUpdate) })) + s.Run("GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + tags := database.StringMap(map[string]string{ + provisionersdk.TagScope: provisionersdk.ScopeOrganization, + }) + t := dbgen.Template(s.T(), db, database.Template{OrganizationID: org.ID, CreatedBy: user.ID}) + tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{OrganizationID: org.ID, CreatedBy: user.ID, TemplateID: uuid.NullUUID{UUID: t.ID, Valid: true}}) + j1 := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + Type: database.ProvisionerJobTypeTemplateVersionImport, + Input: []byte(`{"template_version_id":"` + tv.ID.String() + `"}`), + Tags: tags, + }) + w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OrganizationID: org.ID, OwnerID: user.ID, TemplateID: t.ID}) + wbID := uuid.New() + j2 := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: []byte(`{"workspace_build_id":"` + wbID.String() + `"}`), + Tags: tags, + }) + dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ID: wbID, WorkspaceID: w.ID, TemplateVersionID: tv.ID, JobID: j2.ID}) + + ds, err := db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(context.Background(), database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ + OrganizationID: uuid.NullUUID{Valid: true, UUID: org.ID}, + }) + s.NoError(err, "get provisioner jobs by org") + check.Args(database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ + OrganizationID: uuid.NullUUID{Valid: true, UUID: org.ID}, + }).Asserts(j1, policy.ActionRead, j2, policy.ActionRead).Returns(ds) + })) } func (s *MethodTestSuite) TestTailnetFunctions() { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 1208cf60d573b..fe2ec83831b6e 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net" "strings" "testing" @@ -248,12 +249,18 @@ func WorkspaceAgentScriptTiming(t testing.TB, db database.Store, orig database.W func Workspace(t testing.TB, db database.Store, orig database.WorkspaceTable) database.WorkspaceTable { t.Helper() + var defOrgID uuid.UUID + if orig.OrganizationID == uuid.Nil { + defOrg, _ := db.GetDefaultOrganization(genCtx) + defOrgID = defOrg.ID + } + workspace, err := db.InsertWorkspace(genCtx, database.InsertWorkspaceParams{ ID: takeFirst(orig.ID, uuid.New()), OwnerID: takeFirst(orig.OwnerID, uuid.New()), CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), - OrganizationID: takeFirst(orig.OrganizationID, uuid.New()), + OrganizationID: takeFirst(orig.OrganizationID, defOrgID, uuid.New()), TemplateID: takeFirst(orig.TemplateID, uuid.New()), LastUsedAt: takeFirst(orig.LastUsedAt, dbtime.Now()), Name: takeFirst(orig.Name, testutil.GetRandomName(t)), @@ -556,13 +563,15 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data } jobID := takeFirst(orig.ID, uuid.New()) + // Always set some tags to prevent Acquire from grabbing jobs it should not. + tags := maps.Clone(orig.Tags) if !orig.StartedAt.Time.IsZero() { - if orig.Tags == nil { - orig.Tags = make(database.StringMap) + if tags == nil { + tags = make(database.StringMap) } // Make sure when we acquire the job, we only get this one. - orig.Tags[jobID.String()] = "true" + tags[jobID.String()] = "true" } job, err := db.InsertProvisionerJob(genCtx, database.InsertProvisionerJobParams{ @@ -576,7 +585,7 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data FileID: takeFirst(orig.FileID, uuid.New()), Type: takeFirst(orig.Type, database.ProvisionerJobTypeWorkspaceBuild), Input: takeFirstSlice(orig.Input, []byte("{}")), - Tags: orig.Tags, + Tags: tags, TraceMetadata: pqtype.NullRawMessage{}, }) require.NoError(t, err, "insert job") @@ -588,9 +597,9 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data job, err = db.AcquireProvisionerJob(genCtx, database.AcquireProvisionerJobParams{ StartedAt: orig.StartedAt, OrganizationID: job.OrganizationID, - Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, - ProvisionerTags: must(json.Marshal(orig.Tags)), - WorkerID: uuid.NullUUID{}, + Types: []database.ProvisionerType{job.Provisioner}, + ProvisionerTags: must(json.Marshal(tags)), + WorkerID: takeFirst(orig.WorkerID, uuid.NullUUID{}), }) require.NoError(t, err) // There is no easy way to make sure we acquire the correct job. @@ -598,7 +607,7 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data } if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" { - err := db.UpdateProvisionerJobWithCompleteByID(genCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ + err = db.UpdateProvisionerJobWithCompleteByID(genCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ ID: jobID, UpdatedAt: job.UpdatedAt, CompletedAt: orig.CompletedAt, @@ -608,7 +617,7 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data require.NoError(t, err) } if !orig.CanceledAt.Time.IsZero() { - err := db.UpdateProvisionerJobWithCancelByID(genCtx, database.UpdateProvisionerJobWithCancelByIDParams{ + err = db.UpdateProvisionerJobWithCancelByID(genCtx, database.UpdateProvisionerJobWithCancelByIDParams{ ID: jobID, CanceledAt: orig.CanceledAt, CompletedAt: orig.CompletedAt, @@ -617,7 +626,7 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data } job, err = db.GetProvisionerJobByID(genCtx, jobID) - require.NoError(t, err) + require.NoError(t, err, "get job: %s", jobID.String()) return job } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fe15f8e505d3e..25997cafd736f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1130,6 +1130,96 @@ func getOwnerFromTags(tags map[string]string) string { return "" } +func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLocked(_ context.Context, ids []uuid.UUID) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { + // WITH pending_jobs AS ( + // SELECT + // id, created_at + // FROM + // provisioner_jobs + // WHERE + // started_at IS NULL + // AND + // canceled_at IS NULL + // AND + // completed_at IS NULL + // AND + // error IS NULL + // ), + type pendingJobRow struct { + ID uuid.UUID + CreatedAt time.Time + } + pendingJobs := make([]pendingJobRow, 0) + for _, job := range q.provisionerJobs { + if job.StartedAt.Valid || + job.CanceledAt.Valid || + job.CompletedAt.Valid || + job.Error.Valid { + continue + } + pendingJobs = append(pendingJobs, pendingJobRow{ + ID: job.ID, + CreatedAt: job.CreatedAt, + }) + } + + // queue_position AS ( + // SELECT + // id, + // ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position + // FROM + // pending_jobs + // ), + slices.SortFunc(pendingJobs, func(a, b pendingJobRow) int { + c := a.CreatedAt.Compare(b.CreatedAt) + return c + }) + + queuePosition := make(map[uuid.UUID]int64) + for idx, pj := range pendingJobs { + queuePosition[pj.ID] = int64(idx + 1) + } + + // queue_size AS ( + // SELECT COUNT(*) AS count FROM pending_jobs + // ), + queueSize := len(pendingJobs) + + // SELECT + // sqlc.embed(pj), + // COALESCE(qp.queue_position, 0) AS queue_position, + // COALESCE(qs.count, 0) AS queue_size + // FROM + // provisioner_jobs pj + // LEFT JOIN + // queue_position qp ON pj.id = qp.id + // LEFT JOIN + // queue_size qs ON TRUE + // WHERE + // pj.id IN (...) + jobs := make([]database.GetProvisionerJobsByIDsWithQueuePositionRow, 0) + for _, job := range q.provisionerJobs { + if ids != nil && !slices.Contains(ids, job.ID) { + continue + } + // clone the Tags before appending, since maps are reference types and + // we don't want the caller to be able to mutate the map we have inside + // dbmem! + job.Tags = maps.Clone(job.Tags) + job := database.GetProvisionerJobsByIDsWithQueuePositionRow{ + // sqlc.embed(pj), + ProvisionerJob: job, + // COALESCE(qp.queue_position, 0) AS queue_position, + QueuePosition: queuePosition[job.ID], + // COALESCE(qs.count, 0) AS queue_size + QueueSize: int64(queueSize), + } + jobs = append(jobs, job) + } + + return jobs, nil +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -3901,97 +3991,129 @@ func (q *FakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID return jobs, nil } -func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(_ context.Context, ids []uuid.UUID) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { +func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - // WITH pending_jobs AS ( - // SELECT - // id, created_at - // FROM - // provisioner_jobs - // WHERE - // started_at IS NULL - // AND - // canceled_at IS NULL - // AND - // completed_at IS NULL - // AND - // error IS NULL - // ), - type pendingJobRow struct { - ID uuid.UUID - CreatedAt time.Time + if ids == nil { + ids = []uuid.UUID{} } - pendingJobs := make([]pendingJobRow, 0) - for _, job := range q.provisionerJobs { - if job.StartedAt.Valid || - job.CanceledAt.Valid || - job.CompletedAt.Valid || - job.Error.Valid { - continue - } - pendingJobs = append(pendingJobs, pendingJobRow{ - ID: job.ID, - CreatedAt: job.CreatedAt, - }) + return q.getProvisionerJobsByIDsWithQueuePositionLocked(ctx, ids) +} + +func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err } - // queue_position AS ( - // SELECT - // id, - // ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position - // FROM - // pending_jobs - // ), - slices.SortFunc(pendingJobs, func(a, b pendingJobRow) int { - c := a.CreatedAt.Compare(b.CreatedAt) - return c - }) + q.mutex.RLock() + defer q.mutex.RUnlock() - queuePosition := make(map[uuid.UUID]int64) - for idx, pj := range pendingJobs { - queuePosition[pj.ID] = int64(idx + 1) + /* + -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many + WITH pending_jobs AS ( + SELECT + id, created_at + FROM + provisioner_jobs + WHERE + started_at IS NULL + AND + canceled_at IS NULL + AND + completed_at IS NULL + AND + error IS NULL + ), + queue_position AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position + FROM + pending_jobs + ), + queue_size AS ( + SELECT COUNT(*) AS count FROM pending_jobs + ) + SELECT + sqlc.embed(pj), + COALESCE(qp.queue_position, 0) AS queue_position, + COALESCE(qs.count, 0) AS queue_size, + array_agg(DISTINCT pd.id) FILTER (WHERE pd.id IS NOT NULL)::uuid[] AS available_workers + FROM + provisioner_jobs pj + LEFT JOIN + queue_position qp ON qp.id = pj.id + LEFT JOIN + queue_size qs ON TRUE + LEFT JOIN + provisioner_daemons pd ON ( + -- See AcquireProvisionerJob. + pj.started_at IS NULL + AND pj.organization_id = pd.organization_id + AND pj.provisioner = ANY(pd.provisioners) + AND provisioner_tagset_contains(pd.tags, pj.tags) + ) + WHERE + (sqlc.narg('organization_id')::uuid IS NULL OR pj.organization_id = @organization_id) + AND (COALESCE(array_length(@status::provisioner_job_status[], 1), 1) > 0 OR pj.job_status = ANY(@status::provisioner_job_status[])) + GROUP BY + pj.id, + qp.queue_position, + qs.count + ORDER BY + pj.created_at DESC + LIMIT + sqlc.narg('limit')::int; + */ + rowsWithQueuePosition, err := q.getProvisionerJobsByIDsWithQueuePositionLocked(ctx, nil) + if err != nil { + return nil, err } - // queue_size AS ( - // SELECT COUNT(*) AS count FROM pending_jobs - // ), - queueSize := len(pendingJobs) + var rows []database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow + for _, rowQP := range rowsWithQueuePosition { + job := rowQP.ProvisionerJob - // SELECT - // sqlc.embed(pj), - // COALESCE(qp.queue_position, 0) AS queue_position, - // COALESCE(qs.count, 0) AS queue_size - // FROM - // provisioner_jobs pj - // LEFT JOIN - // queue_position qp ON pj.id = qp.id - // LEFT JOIN - // queue_size qs ON TRUE - // WHERE - // pj.id IN (...) - jobs := make([]database.GetProvisionerJobsByIDsWithQueuePositionRow, 0) - for _, job := range q.provisionerJobs { - if !slices.Contains(ids, job.ID) { + if arg.OrganizationID.Valid && job.OrganizationID != arg.OrganizationID.UUID { continue } - // clone the Tags before appending, since maps are reference types and - // we don't want the caller to be able to mutate the map we have inside - // dbmem! - job.Tags = maps.Clone(job.Tags) - job := database.GetProvisionerJobsByIDsWithQueuePositionRow{ - // sqlc.embed(pj), - ProvisionerJob: job, - // COALESCE(qp.queue_position, 0) AS queue_position, - QueuePosition: queuePosition[job.ID], - // COALESCE(qs.count, 0) AS queue_size - QueueSize: int64(queueSize), + if len(arg.Status) > 0 && !slices.Contains(arg.Status, job.JobStatus) { + continue } - jobs = append(jobs, job) + + row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{ + ProvisionerJob: rowQP.ProvisionerJob, + QueuePosition: rowQP.QueuePosition, + QueueSize: rowQP.QueueSize, + } + if row.QueuePosition > 0 { + var availableWorkers []database.ProvisionerDaemon + for _, daemon := range q.provisionerDaemons { + if daemon.OrganizationID == job.OrganizationID && + slices.Contains(daemon.Provisioners, job.Provisioner) && + tagsSubset(job.Tags, daemon.Tags) { + availableWorkers = append(availableWorkers, daemon) + } + } + slices.SortFunc(availableWorkers, func(a, b database.ProvisionerDaemon) int { + return a.CreatedAt.Compare(b.CreatedAt) + }) + for _, worker := range availableWorkers { + row.AvailableWorkers = append(row.AvailableWorkers, worker.ID) + } + } + rows = append(rows, row) } - return jobs, nil + slices.SortFunc(rows, func(a, b database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) int { + return b.ProvisionerJob.CreatedAt.Compare(a.ProvisionerJob.CreatedAt) + }) + if arg.Limit.Valid && arg.Limit.Int32 > 0 && len(rows) > int(arg.Limit.Int32) { + rows = rows[:arg.Limit.Int32] + } + return rows, nil } func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after time.Time) ([]database.ProvisionerJob, error) { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index f486224e41bb4..ba8a1f9cdc8a6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1022,6 +1022,13 @@ func (m queryMetricsStore) GetProvisionerJobsByIDsWithQueuePosition(ctx context. return r0, r1 } +func (m queryMetricsStore) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, arg) + m.queryLatencies.WithLabelValues("GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ProvisionerJob, error) { start := time.Now() jobs, err := m.s.GetProvisionerJobsCreatedAfter(ctx, createdAt) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 39e85f489ded1..b7460f1adc69c 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2105,6 +2105,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerJobsByIDsWithQueuePosition(arg0, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsByIDsWithQueuePosition", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsByIDsWithQueuePosition), arg0, arg1) } +// GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner mocks base method. +func (m *MockStore) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(arg0 context.Context, arg1 database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner", arg0, arg1) + ret0, _ := ret[0].([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner indicates an expected call of GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner. +func (mr *MockStoreMockRecorder) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner), arg0, arg1) +} + // GetProvisionerJobsCreatedAfter mocks base method. func (m *MockStore) GetProvisionerJobsCreatedAfter(arg0 context.Context, arg1 time.Time) ([]database.ProvisionerJob, error) { m.ctrl.T.Helper() diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 0836a948d7ad0..63e03ccb27f40 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -458,6 +458,18 @@ func (g Group) IsEveryone() bool { return g.ID == g.OrganizationID } +func (p ProvisionerJob) RBACObject() rbac.Object { + switch p.Type { + // Only acceptable for known job types at this time because template + // admins may not be allowed to view new types. + case ProvisionerJobTypeTemplateVersionImport, ProvisionerJobTypeTemplateVersionDryRun, ProvisionerJobTypeWorkspaceBuild: + return rbac.ResourceProvisionerJobs.InOrg(p.OrganizationID) + + default: + panic("developer error: unknown provisioner job type " + string(p.Type)) + } +} + func (p ProvisionerJob) Finished() bool { return p.CanceledAt.Valid || p.CompletedAt.Valid } @@ -511,3 +523,7 @@ func (k CryptoKey) CanVerify(now time.Time) bool { isBeforeDeletion := !k.DeletesAt.Valid || now.Before(k.DeletesAt.Time) return hasSecret && isBeforeDeletion } + +func (r GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) RBACObject() rbac.Object { + return r.ProvisionerJob.RBACObject() +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d306082127b18..1b7d299ba7975 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -208,6 +208,7 @@ type sqlcQuerier interface { GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) + GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 42dea8928e71e..0a80c6651463c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6234,6 +6234,128 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex return items, nil } +const getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner = `-- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many +WITH pending_jobs AS ( + SELECT + id, created_at + FROM + provisioner_jobs + WHERE + started_at IS NULL + AND + canceled_at IS NULL + AND + completed_at IS NULL + AND + error IS NULL +), +queue_position AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position + FROM + pending_jobs +), +queue_size AS ( + SELECT COUNT(*) AS count FROM pending_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.job_status, + COALESCE(qp.queue_position, 0) AS queue_position, + COALESCE(qs.count, 0) AS queue_size, + -- Use subquery to utilize ORDER BY in array_agg since it cannot be + -- combined with FILTER. + ( + SELECT + -- Order for stable output. + array_agg(pd.id ORDER BY pd.created_at ASC)::uuid[] + FROM + provisioner_daemons pd + WHERE + -- See AcquireProvisionerJob. + pj.started_at IS NULL + AND pj.organization_id = pd.organization_id + AND pj.provisioner = ANY(pd.provisioners) + AND provisioner_tagset_contains(pd.tags, pj.tags) + ) AS available_workers +FROM + provisioner_jobs pj +LEFT JOIN + queue_position qp ON qp.id = pj.id +LEFT JOIN + queue_size qs ON TRUE +WHERE + ($1::uuid IS NULL OR pj.organization_id = $1) + AND (COALESCE(array_length($2::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($2::provisioner_job_status[])) +GROUP BY + pj.id, + qp.queue_position, + qs.count +ORDER BY + pj.created_at DESC +LIMIT + $3::int +` + +type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct { + OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` + Status []ProvisionerJobStatus `db:"status" json:"status"` + Limit sql.NullInt32 `db:"limit" json:"limit"` +} + +type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow struct { + ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"` + QueuePosition int64 `db:"queue_position" json:"queue_position"` + QueueSize int64 `db:"queue_size" json:"queue_size"` + AvailableWorkers []uuid.UUID `db:"available_workers" json:"available_workers"` +} + +func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + rows, err := q.db.QueryContext(ctx, getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner, arg.OrganizationID, pq.Array(arg.Status), arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow + for rows.Next() { + var i GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow + if err := rows.Scan( + &i.ProvisionerJob.ID, + &i.ProvisionerJob.CreatedAt, + &i.ProvisionerJob.UpdatedAt, + &i.ProvisionerJob.StartedAt, + &i.ProvisionerJob.CanceledAt, + &i.ProvisionerJob.CompletedAt, + &i.ProvisionerJob.Error, + &i.ProvisionerJob.OrganizationID, + &i.ProvisionerJob.InitiatorID, + &i.ProvisionerJob.Provisioner, + &i.ProvisionerJob.StorageMethod, + &i.ProvisionerJob.Type, + &i.ProvisionerJob.Input, + &i.ProvisionerJob.WorkerID, + &i.ProvisionerJob.FileID, + &i.ProvisionerJob.Tags, + &i.ProvisionerJob.ErrorCode, + &i.ProvisionerJob.TraceMetadata, + &i.ProvisionerJob.JobStatus, + &i.QueuePosition, + &i.QueueSize, + pq.Array(&i.AvailableWorkers), + ); 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 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, job_status FROM provisioner_jobs WHERE created_at > $1 ` diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index ac246d4e2ef68..371c1c4fc76e9 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -87,6 +87,68 @@ LEFT JOIN WHERE pj.id = ANY(@ids :: uuid [ ]); +-- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many +WITH pending_jobs AS ( + SELECT + id, created_at + FROM + provisioner_jobs + WHERE + started_at IS NULL + AND + canceled_at IS NULL + AND + completed_at IS NULL + AND + error IS NULL +), +queue_position AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position + FROM + pending_jobs +), +queue_size AS ( + SELECT COUNT(*) AS count FROM pending_jobs +) +SELECT + sqlc.embed(pj), + COALESCE(qp.queue_position, 0) AS queue_position, + COALESCE(qs.count, 0) AS queue_size, + -- Use subquery to utilize ORDER BY in array_agg since it cannot be + -- combined with FILTER. + ( + SELECT + -- Order for stable output. + array_agg(pd.id ORDER BY pd.created_at ASC)::uuid[] + FROM + provisioner_daemons pd + WHERE + -- See AcquireProvisionerJob. + pj.started_at IS NULL + AND pj.organization_id = pd.organization_id + AND pj.provisioner = ANY(pd.provisioners) + AND provisioner_tagset_contains(pd.tags, pj.tags) + ) AS available_workers +FROM + provisioner_jobs pj +LEFT JOIN + queue_position qp ON qp.id = pj.id +LEFT JOIN + queue_size qs ON TRUE +WHERE + (sqlc.narg('organization_id')::uuid IS NULL OR pj.organization_id = @organization_id) + AND (COALESCE(array_length(@status::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY(@status::provisioner_job_status[])) +GROUP BY + pj.id, + qp.queue_position, + qs.count +ORDER BY + pj.created_at DESC +LIMIT + sqlc.narg('limit')::int; + -- name: GetProvisionerJobsCreatedAfter :many SELECT * FROM provisioner_jobs WHERE created_at > $1; diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 4269f9a8dd57f..f61ecb1146743 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -19,12 +19,78 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/websocket" ) +// @Summary Get provisioner jobs +// @ID get-provisioner-jobs +// @Security CoderSessionToken +// @Produce json +// @Tags Organizations +// @Param organization path string true "Organization ID" format(uuid) +// @Param limit query int false "Page limit" +// @Param status query codersdk.ProvisionerJobStatus false "Filter results by status" enums(pending,running,succeeded,canceling,canceled,failed) +// @Success 200 {array} codersdk.ProvisionerJob +// @Router /organizations/{organization}/provisionerjobs [get] +func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + + // For now, only owners and template admins can access provisioner jobs. + if !api.Authorize(r, policy.ActionRead, rbac.ResourceProvisionerJobs.InOrg(org.ID)) { + httpapi.ResourceNotFound(rw) + return + } + + qp := r.URL.Query() + p := httpapi.NewQueryParamParser() + limit := p.PositiveInt32(qp, 0, "limit") + status := p.Strings(qp, nil, "status") + p.ErrorExcessParams(qp) + if len(p.Errors) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid query parameters.", + Validations: p.Errors, + }) + return + } + + jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ + OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true}, + Status: slice.StringEnums[database.ProvisionerJobStatus](status), + Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, + }) + if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching provisioner jobs.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(jobs, func(dbJob database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) codersdk.ProvisionerJob { + job := convertProvisionerJob(database.GetProvisionerJobsByIDsWithQueuePositionRow{ + ProvisionerJob: dbJob.ProvisionerJob, + QueuePosition: dbJob.QueuePosition, + QueueSize: dbJob.QueueSize, + }) + job.AvailableWorkers = dbJob.AvailableWorkers + return job + })) +} + // Returns provisioner logs based on query parameters. // The intended usage for a client to stream all logs (with JS API): // GET /logs @@ -236,14 +302,16 @@ func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) code func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionRow) codersdk.ProvisionerJob { provisionerJob := pj.ProvisionerJob job := codersdk.ProvisionerJob{ - ID: provisionerJob.ID, - CreatedAt: provisionerJob.CreatedAt, - Error: provisionerJob.Error.String, - ErrorCode: codersdk.JobErrorCode(provisionerJob.ErrorCode.String), - FileID: provisionerJob.FileID, - Tags: provisionerJob.Tags, - QueuePosition: int(pj.QueuePosition), - QueueSize: int(pj.QueueSize), + ID: provisionerJob.ID, + OrganizationID: provisionerJob.OrganizationID, + CreatedAt: provisionerJob.CreatedAt, + Type: codersdk.ProvisionerJobType(provisionerJob.Type), + Error: provisionerJob.Error.String, + ErrorCode: codersdk.JobErrorCode(provisionerJob.ErrorCode.String), + FileID: provisionerJob.FileID, + Tags: provisionerJob.Tags, + QueuePosition: int(pj.QueuePosition), + QueueSize: int(pj.QueueSize), } // Applying values optional to the struct. if provisionerJob.StartedAt.Valid { @@ -260,6 +328,13 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR } job.Status = codersdk.ProvisionerJobStatus(pj.ProvisionerJob.JobStatus) + // Only unmarshal input if it exists, this should only be zero in testing. + if len(provisionerJob.Input) > 0 { + if err := json.Unmarshal(provisionerJob.Input, &job.Input); err != nil { + job.Input.Error = xerrors.Errorf("decode input %s: %w", provisionerJob.Input, err).Error() + } + } + return job } diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index cf17d6495cfed..f7ce721f9d048 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -2,16 +2,107 @@ package coderd_test import ( "context" + "database/sql" + "encoding/json" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "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/coderd/rbac" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" ) +func TestProvisionerJobs(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Database: db, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) + memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + time.Sleep(1500 * time.Millisecond) // Ensure the workspace build job has a different timestamp for sorting. + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // Create a pending job. + w := dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: owner.OrganizationID, + OwnerID: member.ID, + TemplateID: template.ID, + }) + wbID := uuid.New() + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + OrganizationID: w.OrganizationID, + StartedAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: json.RawMessage(`{"workspace_build_id":"` + wbID.String() + `"}`), + }) + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + ID: wbID, + JobID: job.ID, + WorkspaceID: w.ID, + TemplateVersionID: version.ID, + }) + + t.Run("All", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.NoError(t, err) + require.Len(t, jobs, 3) + }) + + t.Run("Status", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Status: []codersdk.ProvisionerJobStatus{codersdk.ProvisionerJobRunning}, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) + }) + + t.Run("Limit", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Limit: 1, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) + }) + + // For now, this is not allowed even though the member has created a + // workspace. Once member-level permissions for jobs are supported + // by RBAC, this test should be updated. + t.Run("MemberDenied", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := memberClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.Error(t, err) + require.Len(t, jobs, 0) + }) +} + func TestProvisionerJobLogs(t *testing.T) { t.Parallel() t.Run("StreamAfterComplete", func(t *testing.T) { diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index d1ebd1c8f56a1..e3678c3c5454a 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -214,6 +214,13 @@ var ( Type: "provisioner_daemon", } + // ResourceProvisionerJobs + // Valid Actions + // - "ActionRead" :: read provisioner jobs + ResourceProvisionerJobs = Object{ + Type: "provisioner_jobs", + } + // ResourceProvisionerKeys // Valid Actions // - "ActionCreate" :: create a provisioner key @@ -337,6 +344,7 @@ func AllResources() []Objecter { ResourceOrganization, ResourceOrganizationMember, ResourceProvisionerDaemon, + ResourceProvisionerJobs, ResourceProvisionerKeys, ResourceReplicas, ResourceSystem, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 2691eed9fe0a9..a9c53f0c2ad80 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -169,6 +169,11 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionDelete: actDef("delete a provisioner daemon"), }, }, + "provisioner_jobs": { + Actions: map[Action]ActionDefinition{ + ActionRead: actDef("read provisioner jobs"), + }, + }, "provisioner_keys": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create a provisioner key"), diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index a57bd071a8052..f4c2d38ee93b1 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -483,6 +483,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceOrganizationMember.Type: {policy.ActionRead}, ResourceGroup.Type: {policy.ActionRead}, ResourceGroupMember.Type: {policy.ActionRead}, + ResourceProvisionerJobs.Type: {policy.ActionRead}, }), }, User: []Permission{}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 0172439829063..a213846be1fc0 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -553,6 +553,15 @@ func TestRolePermissions(t *testing.T) { false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, + { + Name: "ProvisionerJobs", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceProvisionerJobs.InOrg(orgID), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgTemplateAdmin, orgAdmin}, + false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin, orgUserAdmin, orgAuditor}, + }, + }, { Name: "System", Actions: crud, diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 7317a801a089f..2a62e23592d84 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -13,6 +13,17 @@ func ToStrings[T ~string](a []T) []string { return tmp } +func StringEnums[E ~string](a []string) []E { + if a == nil { + return nil + } + tmp := make([]E, 0, len(a)) + for _, v := range a { + tmp = append(tmp, E(v)) + } + return tmp +} + // Omit creates a new slice with the arguments omitted from the list. func Omit[T comparable](a []T, omits ...T) []T { tmp := make([]T, 0, len(a)) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 494f6c86887c4..f25135598180c 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -343,6 +344,47 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio return daemons, json.NewDecoder(res.Body).Decode(&daemons) } +type OrganizationProvisionerJobsOptions struct { + Limit int + Status []ProvisionerJobStatus +} + +func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerJobsOptions) ([]ProvisionerJob, error) { + qp := url.Values{} + if opts != nil { + if opts.Limit > 0 { + qp.Add("limit", strconv.Itoa(opts.Limit)) + } + if len(opts.Status) > 0 { + qp.Add("status", joinSlice(opts.Status)) + } + } + + res, err := c.Request(ctx, http.MethodGet, + fmt.Sprintf("/api/v2/organizations/%s/provisionerjobs?%s", organizationID.String(), qp.Encode()), + nil, + ) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var jobs []ProvisionerJob + return jobs, json.NewDecoder(res.Body).Decode(&jobs) +} + +func joinSlice[T ~string](s []T) string { + var ss []string + for _, v := range s { + ss = append(ss, string(v)) + } + return strings.Join(ss, ",") +} + // CreateTemplateVersion processes source-code and optionally associates the version with a template. // Executing without a template is useful for validating source-code. func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) { diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 675c31408243a..808cc14298cce 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -112,6 +112,34 @@ const ( ProvisionerJobUnknown ProvisionerJobStatus = "unknown" ) +func ProvisionerJobStatusEnums() []ProvisionerJobStatus { + return []ProvisionerJobStatus{ + ProvisionerJobPending, + ProvisionerJobRunning, + ProvisionerJobSucceeded, + ProvisionerJobCanceling, + ProvisionerJobCanceled, + ProvisionerJobFailed, + ProvisionerJobUnknown, + } +} + +// ProvisionerJobInput represents the input for the job. +type ProvisionerJobInput struct { + TemplateVersionID *uuid.UUID `json:"template_version_id,omitempty" format:"uuid" table:"template version id"` + WorkspaceBuildID *uuid.UUID `json:"workspace_build_id,omitempty" format:"uuid" table:"workspace build id"` + Error string `json:"error,omitempty" table:"-"` +} + +// ProvisionerJobType represents the type of job. +type ProvisionerJobType string + +const ( + ProvisionerJobTypeTemplateVersionImport ProvisionerJobType = "template_version_import" + ProvisionerJobTypeWorkspaceBuild ProvisionerJobType = "workspace_build" + ProvisionerJobTypeTemplateVersionDryRun ProvisionerJobType = "template_version_dry_run" +) + // JobErrorCode defines the error code returned by job runner. type JobErrorCode string @@ -127,19 +155,23 @@ func JobIsMissingParameterErrorCode(code JobErrorCode) bool { // ProvisionerJob describes the job executed by the provisioning daemon. type ProvisionerJob struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - StartedAt *time.Time `json:"started_at,omitempty" format:"date-time"` - CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time"` - CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time"` - Error string `json:"error,omitempty"` - ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES"` - Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed"` - WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid"` - FileID uuid.UUID `json:"file_id" format:"uuid"` - Tags map[string]string `json:"tags"` - QueuePosition int `json:"queue_position"` - QueueSize int `json:"queue_size"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at,default_sort"` + StartedAt *time.Time `json:"started_at,omitempty" format:"date-time" table:"started at"` + CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time" table:"completed at"` + CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time" table:"canceled at"` + Error string `json:"error,omitempty" table:"error"` + ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES" table:"error code"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` + WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid" table:"worker id"` + FileID uuid.UUID `json:"file_id" format:"uuid" table:"file id"` + Tags map[string]string `json:"tags" table:"tags"` + QueuePosition int `json:"queue_position" table:"queue position"` + QueueSize int `json:"queue_size" table:"queue size"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` + Input ProvisionerJobInput `json:"input" table:"input,recursive_inline"` + Type ProvisionerJobType `json:"type" table:"type"` + AvailableWorkers []uuid.UUID `json:"available_workers,omitempty" format:"uuid" table:"available workers"` } // ProvisionerJobLog represents the provisioner log entry annotated with source and level. diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index ced2568719578..d42fd1c73ab70 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -27,6 +27,7 @@ const ( ResourceOrganization RBACResource = "organization" ResourceOrganizationMember RBACResource = "organization_member" ResourceProvisionerDaemon RBACResource = "provisioner_daemon" + ResourceProvisionerJobs RBACResource = "provisioner_jobs" ResourceProvisionerKeys RBACResource = "provisioner_keys" ResourceReplicas RBACResource = "replicas" ResourceSystem RBACResource = "system" @@ -82,6 +83,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceProvisionerJobs: {ActionRead}, ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead}, ResourceReplicas: {ActionRead}, ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index d9bea14e060c1..e0cfbc372ee77 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -35,6 +35,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -42,6 +45,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -50,6 +59,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -230,6 +240,9 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -237,6 +250,12 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -245,6 +264,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -868,6 +888,9 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -875,6 +898,12 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -883,6 +912,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1136,6 +1166,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1143,6 +1176,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1151,6 +1190,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1312,6 +1352,7 @@ Status Code **200** | `» initiator_id` | string(uuid) | false | | | | `» initiator_name` | string | false | | | | `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | | `»» canceled_at` | string(date-time) | false | | | | `»» completed_at` | string(date-time) | false | | | | `»» created_at` | string(date-time) | false | | | @@ -1319,12 +1360,18 @@ Status Code **200** | `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | | `»» file_id` | string(uuid) | false | | | | `»» id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» organization_id` | string(uuid) | false | | | | `»» queue_position` | integer | false | | | | `»» queue_size` | integer | false | | | | `»» started_at` | string(date-time) | false | | | | `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | | `»» tags` | object | false | | | | `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | | `»» worker_id` | string(uuid) | false | | | | `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | | `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | @@ -1439,6 +1486,9 @@ Status Code **200** | `status` | `canceling` | | `status` | `canceled` | | `status` | `failed` | +| `type` | `template_version_import` | +| `type` | `workspace_build` | +| `type` | `template_version_dry_run` | | `reason` | `initiator` | | `reason` | `autostart` | | `reason` | `autostop` | @@ -1542,6 +1592,9 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1549,6 +1602,12 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1557,6 +1616,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 1d0c7a3c3450a..efe76a2eda58e 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -202,6 +202,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | | `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | @@ -363,6 +364,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | | `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | @@ -524,6 +526,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | | `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | @@ -654,6 +657,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | | `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | @@ -916,6 +920,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_jobs` | | `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | diff --git a/docs/reference/api/organizations.md b/docs/reference/api/organizations.md index 1c1e79dbca7af..fc58bef11342d 100644 --- a/docs/reference/api/organizations.md +++ b/docs/reference/api/organizations.md @@ -343,3 +343,131 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \ | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Organization](schemas.md#codersdkorganization) | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Get provisioner jobs + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerjobs \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/provisionerjobs` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|-------|--------------|----------|--------------------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `limit` | query | integer | false | Page limit | +| `status` | query | string | false | Filter results by status | + +#### Enumerated Values + +| Parameter | Value | +|-----------|-------------| +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `unknown` | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | + +### Example responses + +> 200 Response + +```json +[ + { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "REQUIRED_TEMPLATE_VARIABLES", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "queue_position": 0, + "queue_size": 0, + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "type": "template_version_import", + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +|--------------------------|--------------------------------------------------------------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» available_workers` | array | false | | | +| `» canceled_at` | string(date-time) | false | | | +| `» completed_at` | string(date-time) | false | | | +| `» created_at` | string(date-time) | false | | | +| `» error` | string | false | | | +| `» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `» file_id` | string(uuid) | false | | | +| `» id` | string(uuid) | false | | | +| `» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»» error` | string | false | | | +| `»» template_version_id` | string(uuid) | false | | | +| `»» workspace_build_id` | string(uuid) | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» queue_position` | integer | false | | | +| `» queue_size` | integer | false | | | +| `» started_at` | string(date-time) | false | | | +| `» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `» worker_id` | string(uuid) | false | | | + +#### Enumerated Values + +| Property | Value | +|--------------|-------------------------------| +| `error_code` | `REQUIRED_TEMPLATE_VARIABLES` | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `type` | `template_version_import` | +| `type` | `workspace_build` | +| `type` | `template_version_dry_run` | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index b20e0342de0c5..d4dd7eacbaf6c 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4446,6 +4446,9 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ```json { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -4453,6 +4456,12 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -4461,28 +4470,33 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|----------------------------------------------------------------|----------|--------------|-------------| -| `canceled_at` | string | false | | | -| `completed_at` | string | false | | | -| `created_at` | string | false | | | -| `error` | string | false | | | -| `error_code` | [codersdk.JobErrorCode](#codersdkjoberrorcode) | false | | | -| `file_id` | string | false | | | -| `id` | string | false | | | -| `queue_position` | integer | false | | | -| `queue_size` | integer | false | | | -| `started_at` | string | false | | | -| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | -| `tags` | object | false | | | -| » `[any property]` | string | false | | | -| `worker_id` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------|----------------------------------------------------------------|----------|--------------|-------------| +| `available_workers` | array of string | false | | | +| `canceled_at` | string | false | | | +| `completed_at` | string | false | | | +| `created_at` | string | false | | | +| `error` | string | false | | | +| `error_code` | [codersdk.JobErrorCode](#codersdkjoberrorcode) | false | | | +| `file_id` | string | false | | | +| `id` | string | false | | | +| `input` | [codersdk.ProvisionerJobInput](#codersdkprovisionerjobinput) | false | | | +| `organization_id` | string | false | | | +| `queue_position` | integer | false | | | +| `queue_size` | integer | false | | | +| `started_at` | string | false | | | +| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | +| `tags` | object | false | | | +| » `[any property]` | string | false | | | +| `type` | [codersdk.ProvisionerJobType](#codersdkprovisionerjobtype) | false | | | +| `worker_id` | string | false | | | #### Enumerated Values @@ -4496,6 +4510,24 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `status` | `canceled` | | `status` | `failed` | +## codersdk.ProvisionerJobInput + +```json +{ + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------------------|--------|----------|--------------|-------------| +| `error` | string | false | | | +| `template_version_id` | string | false | | | +| `workspace_build_id` | string | false | | | + ## codersdk.ProvisionerJobLog ```json @@ -4550,6 +4582,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `failed` | | `unknown` | +## codersdk.ProvisionerJobType + +```json +"template_version_import" +``` + +### Properties + +#### Enumerated Values + +| Value | +|----------------------------| +| `template_version_import` | +| `workspace_build` | +| `template_version_dry_run` | + ## codersdk.ProvisionerKey ```json @@ -4827,6 +4875,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `organization` | | `organization_member` | | `provisioner_daemon` | +| `provisioner_jobs` | | `provisioner_keys` | | `replicas` | | `system` | @@ -5895,6 +5944,9 @@ Restarts will only happen on weekdays in this list on weeks which line up with W }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -5902,6 +5954,12 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -5910,6 +5968,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -6950,6 +7009,9 @@ If the schedule is empty, the user will be updated to use the default schedule.| "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -6957,6 +7019,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -6965,6 +7033,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -7683,6 +7752,9 @@ If the schedule is empty, the user will be updated to use the default schedule.| "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -7690,6 +7762,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -7698,6 +7776,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -8337,6 +8416,9 @@ If the schedule is empty, the user will be updated to use the default schedule.| "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -8344,6 +8426,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -8352,6 +8440,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 5af9726b83b35..5e603eb5c3145 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -447,6 +447,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -454,6 +457,12 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -462,6 +471,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -525,6 +535,9 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -532,6 +545,12 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -540,6 +559,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -627,6 +647,9 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -634,6 +657,12 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -642,6 +671,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1157,6 +1187,9 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1164,6 +1197,12 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1172,6 +1211,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1202,42 +1242,49 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|--------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» archived` | boolean | false | | | -| `» created_at` | string(date-time) | false | | | -| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | -| `»» avatar_url` | string(uri) | false | | | -| `»» id` | string(uuid) | true | | | -| `»» username` | string | true | | | -| `» id` | string(uuid) | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» queue_position` | integer | false | | | -| `»» queue_size` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | -| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | -| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | -| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | -| `» message` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» readme` | string | false | | | -| `» template_id` | string(uuid) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» warnings` | array | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» archived` | boolean | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | +| `»» avatar_url` | string(uri) | false | | | +| `»» id` | string(uuid) | true | | | +| `»» username` | string | true | | | +| `» id` | string(uuid) | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» queue_position` | integer | false | | | +| `»» queue_size` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | +| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | +| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | +| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | +| `» message` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» readme` | string | false | | | +| `» template_id` | string(uuid) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» warnings` | array | false | | | #### Enumerated Values @@ -1250,6 +1297,9 @@ Status Code **200** | `status` | `canceling` | | `status` | `canceled` | | `status` | `failed` | +| `type` | `template_version_import` | +| `type` | `workspace_build` | +| `type` | `template_version_dry_run` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -1397,6 +1447,9 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1404,6 +1457,12 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1412,6 +1471,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1442,42 +1502,49 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|--------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» archived` | boolean | false | | | -| `» created_at` | string(date-time) | false | | | -| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | -| `»» avatar_url` | string(uri) | false | | | -| `»» id` | string(uuid) | true | | | -| `»» username` | string | true | | | -| `» id` | string(uuid) | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» queue_position` | integer | false | | | -| `»» queue_size` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | -| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | -| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | -| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | -| `» message` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» readme` | string | false | | | -| `» template_id` | string(uuid) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» warnings` | array | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------|--------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» archived` | boolean | false | | | +| `» created_at` | string(date-time) | false | | | +| `» created_by` | [codersdk.MinimalUser](schemas.md#codersdkminimaluser) | false | | | +| `»» avatar_url` | string(uri) | false | | | +| `»» id` | string(uuid) | true | | | +| `»» username` | string | true | | | +| `» id` | string(uuid) | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» queue_position` | integer | false | | | +| `»» queue_size` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | +| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | +| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | +| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | +| `» message` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» readme` | string | false | | | +| `» template_id` | string(uuid) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» warnings` | array | false | | | #### Enumerated Values @@ -1490,6 +1557,9 @@ Status Code **200** | `status` | `canceling` | | `status` | `canceled` | | `status` | `failed` | +| `type` | `template_version_import` | +| `type` | `workspace_build` | +| `type` | `template_version_dry_run` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -1527,6 +1597,9 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1534,6 +1607,12 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1542,6 +1621,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1614,6 +1694,9 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1621,6 +1704,12 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1629,6 +1718,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1791,6 +1881,9 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/ ```json { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1798,6 +1891,12 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1806,6 +1905,7 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" } ``` @@ -1844,6 +1944,9 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d ```json { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1851,6 +1954,12 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1859,6 +1968,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" } ``` diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 2413bb294a5f6..e39e553927bf0 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -76,6 +76,9 @@ of the template will be used. "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -83,6 +86,12 @@ of the template will be used. "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -91,6 +100,7 @@ of the template will be used. "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -307,6 +317,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -314,6 +327,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -322,6 +341,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -562,6 +582,9 @@ of the template will be used. "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -569,6 +592,12 @@ of the template will be used. "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -577,6 +606,7 @@ of the template will be used. "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -796,6 +826,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -803,6 +836,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -811,6 +850,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1024,6 +1064,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1031,6 +1074,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1039,6 +1088,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -1371,6 +1421,9 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", "initiator_name": "string", "job": { + "available_workers": [ + "497f6eca-6276-4993-bfeb-53cbbbba6f08" + ], "canceled_at": "2019-08-24T14:15:22Z", "completed_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z", @@ -1378,6 +1431,12 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "error": "string", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" + }, + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "queue_position": 0, "queue_size": 0, "started_at": "2019-08-24T14:15:22Z", @@ -1386,6 +1445,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 43f43c8ff7071..b0e5128ebb290 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -119,6 +119,9 @@ export const RBACResourceActions: Partial< read: "read provisioner daemon", update: "update a provisioner daemon", }, + provisioner_jobs: { + read: "read provisioner jobs", + }, provisioner_keys: { create: "create a provisioner key", delete: "delete a provisioner key", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9ba1e9b2a106a..84f69df437455 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1428,6 +1428,12 @@ export interface OrganizationMemberWithUserData extends OrganizationMember { readonly global_roles: readonly SlimRole[]; } +// From codersdk/organizations.go +export interface OrganizationProvisionerJobsOptions { + readonly Limit: number; + readonly Status: readonly ProvisionerJobStatus[]; +} + // From codersdk/idpsync.go export interface OrganizationSyncSettings { readonly field: string; @@ -1585,6 +1591,17 @@ export interface ProvisionerJob { readonly tags: Record; readonly queue_position: number; readonly queue_size: number; + readonly organization_id: string; + readonly input: ProvisionerJobInput; + readonly type: ProvisionerJobType; + readonly available_workers?: readonly string[]; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJobInput { + readonly template_version_id?: string; + readonly workspace_build_id?: string; + readonly error?: string; } // From codersdk/provisionerdaemons.go @@ -1617,6 +1634,18 @@ export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ "unknown", ]; +// From codersdk/provisionerdaemons.go +export type ProvisionerJobType = + | "template_version_dry_run" + | "template_version_import" + | "workspace_build"; + +export const ProvisionerJobTypes: ProvisionerJobType[] = [ + "template_version_dry_run", + "template_version_import", + "workspace_build", +]; + // From codersdk/provisionerdaemons.go export interface ProvisionerKey { readonly id: string; @@ -1767,6 +1796,7 @@ export type RBACResource = | "organization" | "organization_member" | "provisioner_daemon" + | "provisioner_jobs" | "provisioner_keys" | "replicas" | "system" @@ -1801,6 +1831,7 @@ export const RBACResources: RBACResource[] = [ "organization", "organization_member", "provisioner_daemon", + "provisioner_jobs", "provisioner_keys", "replicas", "system", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 701fc3e8e1b0c..c522457a63c1d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -655,6 +655,11 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = { }, queue_position: 0, queue_size: 0, + input: { + template_version_id: "test-template-version", // MockTemplateVersion.id + }, + organization_id: MockOrganization.id, + type: "template_version_dry_run", }; export const MockFailedProvisionerJob: TypesGen.ProvisionerJob = {