diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go new file mode 100644 index 0000000000000..4f7579fd28f83 --- /dev/null +++ b/cli/provisionerjobs.go @@ -0,0 +1,120 @@ +package cli + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +func (r *RootCmd) provisionerJobs() *serpent.Command { + cmd := &serpent.Command{ + Use: "jobs", + Short: "View and manage provisioner jobs", + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + Aliases: []string{"job"}, + Children: []*serpent.Command{ + r.provisionerJobsList(), + }, + } + return cmd +} + +func (r *RootCmd) provisionerJobsList() *serpent.Command { + type provisionerJobRow struct { + codersdk.ProvisionerJob `table:"provisioner_job,recursive_inline"` + OrganizationName string `json:"organization_name" table:"organization"` + Queue string `json:"-" table:"queue"` + } + + var ( + client = new(codersdk.Client) + orgContext = NewOrganizationContext() + formatter = cliui.NewOutputFormatter( + cliui.TableFormat([]provisionerJobRow{}, []string{"created at", "id", "organization", "status", "type", "queue", "tags"}), + cliui.JSONFormat(), + ) + status []string + limit int64 + ) + + cmd := &serpent.Command{ + Use: "list", + Short: "List provisioner jobs", + Aliases: []string{"ls"}, + Middleware: serpent.Chain( + serpent.RequireNArgs(0), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + org, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("current organization: %w", err) + } + + jobs, err := client.OrganizationProvisionerJobs(ctx, org.ID, &codersdk.OrganizationProvisionerJobsOptions{ + Status: slice.ToStringEnums[codersdk.ProvisionerJobStatus](status), + Limit: int(limit), + }) + if err != nil { + return xerrors.Errorf("list provisioner jobs: %w", err) + } + + if len(jobs) == 0 { + _, _ = fmt.Fprintln(inv.Stdout, "No provisioner jobs found") + return nil + } + + var rows []provisionerJobRow + for _, job := range jobs { + row := provisionerJobRow{ + ProvisionerJob: job, + OrganizationName: org.HumanName(), + } + if job.Status == codersdk.ProvisionerJobPending { + row.Queue = fmt.Sprintf("%d/%d", job.QueuePosition, job.QueueSize) + } + rows = append(rows, row) + } + + out, err := formatter.Format(ctx, rows) + if err != nil { + return xerrors.Errorf("display provisioner daemons: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stdout, out) + + return nil + }, + } + + cmd.Options = append(cmd.Options, []serpent.Option{ + { + Flag: "status", + FlagShorthand: "s", + Env: "CODER_PROVISIONER_JOB_LIST_STATUS", + Description: "Filter by job status.", + Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerJobStatusEnums())...), + }, + { + Flag: "limit", + FlagShorthand: "l", + Env: "CODER_PROVISIONER_JOB_LIST_LIMIT", + Description: "Limit the number of jobs returned.", + Default: "50", + Value: serpent.Int64Of(&limit), + }, + }...) + + orgContext.AttachOptions(cmd) + formatter.AttachOptions(&cmd.Options) + + return cmd +} diff --git a/cli/provisioners.go b/cli/provisioners.go new file mode 100644 index 0000000000000..af6869b89f566 --- /dev/null +++ b/cli/provisioners.go @@ -0,0 +1,93 @@ +package cli + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +func (r *RootCmd) Provisioners() *serpent.Command { + cmd := &serpent.Command{ + Use: "provisioner", + Short: "View and manage provisioner daemons and jobs", + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + Aliases: []string{"provisioners"}, + Children: []*serpent.Command{ + r.provisionerList(), + r.provisionerJobs(), + }, + } + + return cmd +} + +func (r *RootCmd) provisionerList() *serpent.Command { + type provisionerDaemonRow struct { + codersdk.ProvisionerDaemonWithStatus `table:"provisioner_daemon,recursive_inline"` + OrganizationName string `json:"organization_name" table:"organization"` + } + var ( + client = new(codersdk.Client) + orgContext = NewOrganizationContext() + formatter = cliui.NewOutputFormatter( + cliui.TableFormat([]provisionerDaemonRow{}, []string{"name", "organization", "status", "key name", "created at", "last seen at", "version", "tags"}), + cliui.JSONFormat(), + ) + ) + + cmd := &serpent.Command{ + Use: "list", + Short: "List provisioner daemons in an organization", + Aliases: []string{"ls"}, + Middleware: serpent.Chain( + serpent.RequireNArgs(0), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + + org, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("current organization: %w", err) + } + + daemons, err := client.OrganizationProvisionerDaemons(ctx, org.ID, nil) + if err != nil { + return xerrors.Errorf("list provisioner daemons: %w", err) + } + + if len(daemons) == 0 { + _, _ = fmt.Fprintln(inv.Stdout, "No provisioner daemons found") + return nil + } + + var rows []provisionerDaemonRow + for _, daemon := range daemons { + rows = append(rows, provisionerDaemonRow{ + ProvisionerDaemonWithStatus: daemon, + OrganizationName: org.HumanName(), + }) + } + + out, err := formatter.Format(ctx, rows) + if err != nil { + return xerrors.Errorf("display provisioner daemons: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stdout, out) + + return nil + }, + } + + orgContext.AttachOptions(cmd) + formatter.AttachOptions(&cmd.Options) + + return cmd +} diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go new file mode 100644 index 0000000000000..bb22d63f2e35b --- /dev/null +++ b/cli/provisioners_test.go @@ -0,0 +1,209 @@ +package cli_test + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "slices" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/codersdk" +) + +func TestProvisioners_Golden(t *testing.T) { + t.Parallel() + + // Replace UUIDs with predictable values for golden files. + replace := make(map[string]string) + updateReplaceUUIDs := func(coderdAPI *coderd.API) { + //nolint:gocritic // This is a test. + systemCtx := dbauthz.AsSystemRestricted(context.Background()) + provisioners, err := coderdAPI.Database.GetProvisionerDaemons(systemCtx) + require.NoError(t, err) + slices.SortFunc(provisioners, func(a, b database.ProvisionerDaemon) int { + return a.CreatedAt.Compare(b.CreatedAt) + }) + pIdx := 0 + for _, p := range provisioners { + if _, ok := replace[p.ID.String()]; !ok { + replace[p.ID.String()] = fmt.Sprintf("00000000-0000-0000-aaaa-%012d", pIdx) + pIdx++ + } + } + jobs, err := coderdAPI.Database.GetProvisionerJobsCreatedAfter(systemCtx, time.Time{}) + require.NoError(t, err) + slices.SortFunc(jobs, func(a, b database.ProvisionerJob) int { + return a.CreatedAt.Compare(b.CreatedAt) + }) + jIdx := 0 + for _, j := range jobs { + if _, ok := replace[j.ID.String()]; !ok { + replace[j.ID.String()] = fmt.Sprintf("00000000-0000-0000-bbbb-%012d", jIdx) + jIdx++ + } + } + } + + db, ps := dbtestutil.NewDB(t, + dbtestutil.WithDumpOnFailure(), + //nolint:gocritic // Use UTC for consistent timestamp length in golden files. + dbtestutil.WithTimezone("UTC"), + ) + client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{ + IncludeProvisionerDaemon: false, + Database: db, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + // Create initial resources with a running provisioner. + firstProvisioner := coderdtest.NewProvisionerDaemon(t, coderdAPI) + t.Cleanup(func() { _ = firstProvisioner.Close() }) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent()) + 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) + + // Stop the provisioner so it doesn't grab any more jobs. + firstProvisioner.Close() + + // Sanitize the UUIDs for the initial resources. + replace[version.ID.String()] = "00000000-0000-0000-cccc-000000000000" + replace[workspace.LatestBuild.ID.String()] = "00000000-0000-0000-dddd-000000000000" + + // Create a provisioner that's working on a job. + pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-1", + CreatedAt: dbtime.Now().Add(1 * time.Second), + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + }) + w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: memberUser.ID, + TemplateID: template.ID, + }) + wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001") + job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(2 * time.Second), + StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true}, + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb1ID, + JobID: job1.ID, + WorkspaceID: w1.ID, + TemplateVersionID: version.ID, + }) + + // Create a provisioner that completed a job previously and is offline. + pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-2", + CreatedAt: dbtime.Now().Add(2 * time.Second), + LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + }) + w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: memberUser.ID, + TemplateID: template.ID, + }) + wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002") + job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + WorkerID: uuid.NullUUID{UUID: pd2.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(3 * time.Second), + StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-2 * time.Hour), Valid: true}, + CompletedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb2ID, + JobID: job2.ID, + WorkspaceID: w2.ID, + TemplateVersionID: version.ID, + }) + + // Create a pending job. + w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: memberUser.ID, + TemplateID: template.ID, + }) + wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003") + job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`), + CreatedAt: dbtime.Now().Add(4 * time.Second), + Tags: database.StringMap{"owner": "", "scope": "organization"}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb3ID, + JobID: job3.ID, + WorkspaceID: w3.ID, + TemplateVersionID: version.ID, + }) + + // Create a provisioner that is idle. + _ = dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-3", + CreatedAt: dbtime.Now().Add(3 * time.Second), + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + }) + + updateReplaceUUIDs(coderdAPI) + + for id, replaceID := range replace { + t.Logf("replace[%q] = %q", id, replaceID) + } + + t.Run("list", func(t *testing.T) { + t.Parallel() + + var got bytes.Buffer + inv, root := clitest.New(t, + "provisioners", + "list", + "--column", "id,created at,last seen at,name,version,api version,tags,status,current job id,previous job id,previous job status,organization", + ) + inv.Stdout = &got + clitest.SetupConfig(t, member, root) + err := inv.Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace) + }) + + t.Run("jobs list", func(t *testing.T) { + t.Parallel() + + var got bytes.Buffer + inv, root := clitest.New(t, + "provisioners", + "jobs", + "list", + "--column", "id,created at,status,worker id,tags,template version id,workspace build id,type,available workers,organization,queue", + ) + inv.Stdout = &got + clitest.SetupConfig(t, member, root) + err := inv.Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace) + }) +} diff --git a/cli/root.go b/cli/root.go index 3f674db6d2bb5..778cf2c24215f 100644 --- a/cli/root.go +++ b/cli/root.go @@ -132,7 +132,11 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { } func (r *RootCmd) AGPL() []*serpent.Command { - all := append(r.CoreSubcommands(), r.Server( /* Do not import coderd here. */ nil)) + all := append( + r.CoreSubcommands(), + r.Server( /* Do not import coderd here. */ nil), + r.Provisioners(), + ) return all } diff --git a/cli/root_test.go b/cli/root_test.go index 897aea18fec3e..ac1454152672e 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -55,6 +55,22 @@ func TestCommandHelp(t *testing.T) { Name: "coder users list", Cmd: []string{"users", "list"}, }, + clitest.CommandHelpCase{ + Name: "coder provisioner list", + Cmd: []string{"provisioner", "list"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioner list --output json", + Cmd: []string{"provisioner", "list", "--output", "json"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioner jobs list", + Cmd: []string{"provisioner", "jobs", "list"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioner jobs list --output json", + Cmd: []string{"provisioner", "jobs", "list", "--output", "json"}, + }, )) } diff --git a/cli/testdata/TestProvisioners_Golden/jobs_list.golden b/cli/testdata/TestProvisioners_Golden/jobs_list.golden new file mode 100644 index 0000000000000..49c360b3e34c8 --- /dev/null +++ b/cli/testdata/TestProvisioners_Golden/jobs_list.golden @@ -0,0 +1,6 @@ +ID CREATED AT STATUS WORKER ID TAGS TEMPLATE VERSION ID WORKSPACE BUILD ID TYPE AVAILABLE WORKERS ORGANIZATION QUEUE +00000000-0000-0000-bbbb-000000000000 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 00000000-0000-0000-cccc-000000000000 template_version_import [] Coder +00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 00000000-0000-0000-dddd-000000000000 workspace_build [] Coder +00000000-0000-0000-bbbb-000000000002 ====[timestamp]===== running 00000000-0000-0000-aaaa-000000000001 map[owner: scope:organization] 00000000-0000-0000-dddd-000000000001 workspace_build [] Coder +00000000-0000-0000-bbbb-000000000003 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000002 map[owner: scope:organization] 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder +00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending map[owner: scope:organization] 00000000-0000-0000-dddd-000000000003 workspace_build [00000000-0000-0000-aaaa-000000000000, 00000000-0000-0000-aaaa-000000000001, 00000000-0000-0000-aaaa-000000000002, 00000000-0000-0000-aaaa-000000000003] Coder 1/1 diff --git a/cli/testdata/TestProvisioners_Golden/list.golden b/cli/testdata/TestProvisioners_Golden/list.golden new file mode 100644 index 0000000000000..c4de1bee34db7 --- /dev/null +++ b/cli/testdata/TestProvisioners_Golden/list.golden @@ -0,0 +1,5 @@ +ID CREATED AT LAST SEEN AT NAME VERSION API VERSION TAGS STATUS CURRENT JOB ID PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION +00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 1.1 map[owner: scope:organization] busy 00000000-0000-0000-bbbb-000000000002 Coder +00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 1.1 map[owner: scope:organization] offline 00000000-0000-0000-bbbb-000000000003 succeeded Coder +00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 1.1 map[owner: scope:organization] idle Coder +00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel 1.1 map[owner: scope:organization] idle 00000000-0000-0000-bbbb-000000000001 succeeded Coder diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index c25301169002e..4e0a5e92f63b5 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -35,6 +35,7 @@ SUBCOMMANDS: ping Ping a workspace port-forward Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". + provisioner View and manage provisioner daemons and jobs publickey Output your Coder public key used for Git operations rename Rename a workspace reset-password Directly connect to the database to reset a user's 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/cli/testdata/coder_provisioner_--help.golden b/cli/testdata/coder_provisioner_--help.golden new file mode 100644 index 0000000000000..4f4a783dcc477 --- /dev/null +++ b/cli/testdata/coder_provisioner_--help.golden @@ -0,0 +1,15 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner + + View and manage provisioner daemons and jobs + + Aliases: provisioners + +SUBCOMMANDS: + jobs View and manage provisioner jobs + list List provisioner daemons in an organization + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_provisioner_jobs_--help.golden b/cli/testdata/coder_provisioner_jobs_--help.golden new file mode 100644 index 0000000000000..6442c78a03a8e --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner jobs + + View and manage provisioner jobs + + Aliases: job + +SUBCOMMANDS: + list List provisioner jobs + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_provisioner_jobs_list.golden b/cli/testdata/coder_provisioner_jobs_list.golden new file mode 100644 index 0000000000000..8dc1398f2d890 --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list.golden @@ -0,0 +1,3 @@ +ID CREATED AT STATUS TAGS TYPE ORGANIZATION QUEUE +======[workspace build job ID]====== ====[timestamp]===== succeeded map[owner: scope:organization] workspace_build Coder +==========[version job ID]========== ====[timestamp]===== succeeded map[owner: scope:organization] template_version_import Coder diff --git a/cli/testdata/coder_provisioner_jobs_list_--help.golden b/cli/testdata/coder_provisioner_jobs_list_--help.golden new file mode 100644 index 0000000000000..585e918c23e7b --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -0,0 +1,27 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner jobs list [flags] + + List provisioner jobs + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|organization|queue] (default: created at,id,organization,status,type,queue,tags) + Columns to display in table output. + + -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) + Limit the number of jobs returned. + + -o, --output table|json (default: table) + Output format. + + -s, --status [pending|running|succeeded|canceling|canceled|failed|unknown], $CODER_PROVISIONER_JOB_LIST_STATUS + Filter by job status. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden new file mode 100644 index 0000000000000..88dba1ad13d55 --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden @@ -0,0 +1,44 @@ +[ + { + "id": "======[workspace build job ID]======", + "created_at": "====[timestamp]=====", + "started_at": "====[timestamp]=====", + "completed_at": "====[timestamp]=====", + "status": "succeeded", + "worker_id": "====[workspace build worker ID]=====", + "file_id": "=====[workspace build file ID]======", + "tags": { + "owner": "", + "scope": "organization" + }, + "queue_position": 0, + "queue_size": 0, + "organization_id": "===========[first org ID]===========", + "input": { + "workspace_build_id": "========[workspace build ID]========" + }, + "type": "workspace_build", + "organization_name": "Coder" + }, + { + "id": "==========[version job ID]==========", + "created_at": "====[timestamp]=====", + "started_at": "====[timestamp]=====", + "completed_at": "====[timestamp]=====", + "status": "succeeded", + "worker_id": "====[workspace build worker ID]=====", + "file_id": "=====[workspace build file ID]======", + "tags": { + "owner": "", + "scope": "organization" + }, + "queue_position": 0, + "queue_size": 0, + "organization_id": "===========[first org ID]===========", + "input": { + "template_version_id": "============[version ID]============" + }, + "type": "template_version_import", + "organization_name": "Coder" + } +] diff --git a/cli/testdata/coder_provisioner_list.golden b/cli/testdata/coder_provisioner_list.golden new file mode 100644 index 0000000000000..056571547939e --- /dev/null +++ b/cli/testdata/coder_provisioner_list.golden @@ -0,0 +1,2 @@ +CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS ORGANIZATION +====[timestamp]===== ====[timestamp]===== test v0.0.0-devel map[owner: scope:organization] built-in idle Coder diff --git a/cli/testdata/coder_provisioner_list_--help.golden b/cli/testdata/coder_provisioner_list_--help.golden new file mode 100644 index 0000000000000..a9943cb9da392 --- /dev/null +++ b/cli/testdata/coder_provisioner_list_--help.golden @@ -0,0 +1,21 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner list [flags] + + List provisioner daemons in an organization + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) + Columns to display in table output. + + -o, --output table|json (default: table) + Output format. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden new file mode 100644 index 0000000000000..b36f24b79553c --- /dev/null +++ b/cli/testdata/coder_provisioner_list_--output_json.golden @@ -0,0 +1,27 @@ +[ + { + "id": "====[workspace build worker ID]=====", + "organization_id": "===========[first org ID]===========", + "key_id": "00000000-0000-0000-0000-000000000001", + "created_at": "====[timestamp]=====", + "last_seen_at": "====[timestamp]=====", + "name": "test", + "version": "v0.0.0-devel", + "api_version": "1.1", + "provisioners": [ + "echo" + ], + "tags": { + "owner": "", + "scope": "organization" + }, + "key_name": "built-in", + "status": "idle", + "current_job": null, + "previous_job": { + "id": "======[workspace build job ID]======", + "status": "succeeded" + }, + "organization_name": "Coder" + } +] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 791b3c6f145e8..b79ea0e99c6c7 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2955,7 +2955,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemon" + "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" } } } @@ -2991,6 +2991,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": [ @@ -12356,9 +12421,120 @@ const docTemplate = `{ } } }, + "codersdk.ProvisionerDaemonJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerJobStatus" + } + ] + } + } + }, + "codersdk.ProvisionerDaemonStatus": { + "type": "string", + "enum": [ + "offline", + "idle", + "busy" + ], + "x-enum-varnames": [ + "ProvisionerDaemonOffline", + "ProvisionerDaemonIdle", + "ProvisionerDaemonBusy" + ] + }, + "codersdk.ProvisionerDaemonWithStatus": { + "type": "object", + "properties": { + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_name": { + "type": "string" + }, + "last_seen_at": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "provisioners": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": [ + "offline", + "idle", + "busy" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "codersdk.ProvisionerJob": { "type": "object", "properties": { + "available_workers": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, "canceled_at": { "type": "string", "format": "date-time" @@ -12392,6 +12568,13 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -12423,12 +12606,28 @@ const docTemplate = `{ "type": "string" } }, + "type": { + "$ref": "#/definitions/codersdk.ProvisionerJobType" + }, "worker_id": { "type": "string", "format": "uuid" } } }, + "codersdk.ProvisionerJobInput": { + "type": "object", + "properties": { + "template_version_id": { + "type": "string", + "format": "uuid" + }, + "workspace_build_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerJobLog": { "type": "object", "properties": { @@ -12485,6 +12684,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": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index abd329103579e..ae66d9e9294e8 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2593,7 +2593,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.ProvisionerDaemon" + "$ref": "#/definitions/codersdk.ProvisionerDaemonWithStatus" } } } @@ -2627,6 +2627,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": [ @@ -11149,9 +11210,112 @@ } } }, + "codersdk.ProvisionerDaemonJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "enum": [ + "pending", + "running", + "succeeded", + "canceling", + "canceled", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerJobStatus" + } + ] + } + } + }, + "codersdk.ProvisionerDaemonStatus": { + "type": "string", + "enum": ["offline", "idle", "busy"], + "x-enum-varnames": [ + "ProvisionerDaemonOffline", + "ProvisionerDaemonIdle", + "ProvisionerDaemonBusy" + ] + }, + "codersdk.ProvisionerDaemonWithStatus": { + "type": "object", + "properties": { + "api_version": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "current_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "key_id": { + "type": "string", + "format": "uuid" + }, + "key_name": { + "type": "string" + }, + "last_seen_at": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, + "previous_job": { + "$ref": "#/definitions/codersdk.ProvisionerDaemonJob" + }, + "provisioners": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "enum": ["offline", "idle", "busy"], + "allOf": [ + { + "$ref": "#/definitions/codersdk.ProvisionerDaemonStatus" + } + ] + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "codersdk.ProvisionerJob": { "type": "object", "properties": { + "available_workers": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, "canceled_at": { "type": "string", "format": "date-time" @@ -11183,6 +11347,13 @@ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -11214,12 +11385,28 @@ "type": "string" } }, + "type": { + "$ref": "#/definitions/codersdk.ProvisionerJobType" + }, "worker_id": { "type": "string", "format": "uuid" } } }, + "codersdk.ProvisionerJobInput": { + "type": "object", + "properties": { + "template_version_id": { + "type": "string", + "format": "uuid" + }, + "workspace_build_id": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerJobLog": { "type": "object", "properties": { @@ -11270,6 +11457,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": { diff --git a/coderd/coderd.go b/coderd/coderd.go index fd8a10a44f140..063da37932f33 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1007,6 +1007,12 @@ 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 f64dbcc166591..222e6e9ad8b46 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1928,6 +1928,10 @@ func (q *querier) GetProvisionerDaemonsByOrganization(ctx context.Context, organ return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsByOrganization)(ctx, organizationID) } +func (q *querier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsWithStatusByOrganization)(ctx, arg) +} + func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { job, err := q.db.GetProvisionerJobByID(ctx, id) if err != nil { @@ -1976,6 +1980,11 @@ func (q *querier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, return q.db.GetProvisionerJobsByIDsWithQueuePosition(ctx, ids) } +// TODO: we need to add a provisioner job resource +func (q *querier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + return q.db.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, arg) +} + // TODO: We need to create a ProvisionerJob resource type func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index d0bf6a4ce0f50..d57d267e2b524 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -248,12 +248,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)), @@ -505,9 +511,27 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab // ProvisionerDaemon creates a provisioner daemon as far as the database is concerned. It does not run a provisioner daemon. // If no key is provided, it will create one. -func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.ProvisionerDaemon) database.ProvisionerDaemon { +func ProvisionerDaemon(t testing.TB, db database.Store, orig database.ProvisionerDaemon) database.ProvisionerDaemon { t.Helper() + var defOrgID uuid.UUID + if orig.OrganizationID == uuid.Nil { + defOrg, _ := db.GetDefaultOrganization(genCtx) + defOrgID = defOrg.ID + } + + daemon := database.UpsertProvisionerDaemonParams{ + Name: takeFirst(orig.Name, testutil.GetRandomName(t)), + OrganizationID: takeFirst(orig.OrganizationID, defOrgID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), + Provisioners: takeFirstSlice(orig.Provisioners, []database.ProvisionerType{database.ProvisionerTypeEcho}), + Tags: takeFirstMap(orig.Tags, database.StringMap{"owner": "", "scope": "organization"}), + KeyID: takeFirst(orig.KeyID, uuid.Nil), + LastSeenAt: takeFirst(orig.LastSeenAt, sql.NullTime{Time: dbtime.Now(), Valid: true}), + Version: takeFirst(orig.Version, "v0.0.0"), + APIVersion: takeFirst(orig.APIVersion, "1.1"), + } + if daemon.KeyID == uuid.Nil { key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{ ID: uuid.New(), @@ -521,24 +545,7 @@ func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.Provisio daemon.KeyID = key.ID } - if daemon.CreatedAt.IsZero() { - daemon.CreatedAt = dbtime.Now() - } - if daemon.Name == "" { - daemon.Name = "test-daemon" - } - - d, err := db.UpsertProvisionerDaemon(genCtx, database.UpsertProvisionerDaemonParams{ - Name: daemon.Name, - OrganizationID: daemon.OrganizationID, - CreatedAt: daemon.CreatedAt, - Provisioners: daemon.Provisioners, - Tags: daemon.Tags, - KeyID: daemon.KeyID, - LastSeenAt: daemon.LastSeenAt, - Version: daemon.Version, - APIVersion: daemon.APIVersion, - }) + d, err := db.UpsertProvisionerDaemon(genCtx, daemon) require.NoError(t, err) return d } @@ -556,12 +563,10 @@ 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. - if !orig.StartedAt.Time.IsZero() { - if orig.Tags == nil { - orig.Tags = make(database.StringMap) - } + tags := takeFirstMap(orig.Tags, database.StringMap{"owner": "", "scope": "organization"}) + if orig.Tags == nil && !orig.StartedAt.Time.IsZero() { // 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{ @@ -575,7 +580,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") @@ -587,9 +592,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. @@ -597,7 +602,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, @@ -607,7 +612,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, @@ -616,7 +621,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 } @@ -1108,6 +1113,12 @@ func takeFirstSlice[T any](values ...[]T) []T { }) } +func takeFirstMap[T, E comparable](values ...map[T]E) map[T]E { + return takeFirstF(values, func(v map[T]E) bool { + return v != nil + }) +} + // takeFirstF takes the first value that returns true func takeFirstF[Value any](values []Value, take func(v Value) bool) Value { for _, v := range values { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7b19790a6d8ea..88d0462682a40 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1128,6 +1128,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") } @@ -3749,6 +3839,89 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg return daemons, nil } +func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + var rows []database.GetProvisionerDaemonsWithStatusByOrganizationRow + for _, daemon := range q.provisionerDaemons { + if daemon.OrganizationID != arg.OrganizationID { + continue + } + if len(arg.IDs) > 0 && !slices.Contains(arg.IDs, daemon.ID) { + continue + } + if len(arg.Tags) > 0 && !tagsSubset(arg.Tags, daemon.Tags) { + continue + } + + var status database.ProvisionerDaemonStatus + var currentJob database.ProvisionerJob + if !daemon.LastSeenAt.Valid || daemon.LastSeenAt.Time.Before(time.Now().Add(-time.Duration(arg.StaleIntervalMS)*time.Millisecond)) { + status = database.ProvisionerDaemonStatusOffline + } else { + for _, job := range q.provisionerJobs { + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID && !job.CompletedAt.Valid && !job.Error.Valid { + currentJob = job + break + } + } + + if currentJob.ID != uuid.Nil { + status = database.ProvisionerDaemonStatusBusy + } else { + status = database.ProvisionerDaemonStatusIdle + } + } + + var previousJob database.ProvisionerJob + for _, job := range q.provisionerJobs { + if !job.WorkerID.Valid || job.WorkerID.UUID != daemon.ID { + continue + } + + if job.StartedAt.Valid || + job.CanceledAt.Valid || + job.CompletedAt.Valid || + job.Error.Valid { + if job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { + previousJob = job + } + } + } + + // Get the provisioner key name + var keyName string + for _, key := range q.provisionerKeys { + if key.ID == daemon.KeyID { + keyName = key.Name + break + } + } + + rows = append(rows, database.GetProvisionerDaemonsWithStatusByOrganizationRow{ + ProvisionerDaemon: daemon, + Status: status, + KeyName: keyName, + CurrentJobID: uuid.NullUUID{UUID: currentJob.ID, Valid: currentJob.ID != uuid.Nil}, + CurrentJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: currentJob.JobStatus, Valid: currentJob.ID != uuid.Nil}, + PreviousJobID: uuid.NullUUID{UUID: previousJob.ID, Valid: previousJob.ID != uuid.Nil}, + PreviousJobStatus: database.NullProvisionerJobStatus{ProvisionerJobStatus: previousJob.JobStatus, Valid: previousJob.ID != uuid.Nil}, + }) + } + + slices.SortFunc(rows, func(a, b database.GetProvisionerDaemonsWithStatusByOrganizationRow) int { + return slice.Ascending(a.ProvisionerDaemon.Name, b.ProvisionerDaemon.Name) + }) + + return rows, nil +} + func (q *FakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3800,97 +3973,127 @@ 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) + }) + + 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 645357d6f095e..ad628c43d8dc1 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -980,6 +980,13 @@ func (m queryMetricsStore) GetProvisionerDaemonsByOrganization(ctx context.Conte return r0, r1 } +func (m queryMetricsStore) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerDaemonsWithStatusByOrganization(ctx, arg) + m.queryLatencies.WithLabelValues("GetProvisionerDaemonsWithStatusByOrganization").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { start := time.Now() job, err := m.s.GetProvisionerJobByID(ctx, id) @@ -1008,6 +1015,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 73a0e6d60af55..29ef29e4cd8fa 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2016,6 +2016,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerDaemonsByOrganization(arg0, arg1 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerDaemonsByOrganization", reflect.TypeOf((*MockStore)(nil).GetProvisionerDaemonsByOrganization), arg0, arg1) } +// GetProvisionerDaemonsWithStatusByOrganization mocks base method. +func (m *MockStore) GetProvisionerDaemonsWithStatusByOrganization(arg0 context.Context, arg1 database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerDaemonsWithStatusByOrganization", arg0, arg1) + ret0, _ := ret[0].([]database.GetProvisionerDaemonsWithStatusByOrganizationRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerDaemonsWithStatusByOrganization indicates an expected call of GetProvisionerDaemonsWithStatusByOrganization. +func (mr *MockStoreMockRecorder) GetProvisionerDaemonsWithStatusByOrganization(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerDaemonsWithStatusByOrganization", reflect.TypeOf((*MockStore)(nil).GetProvisionerDaemonsWithStatusByOrganization), arg0, arg1) +} + // GetProvisionerJobByID mocks base method. func (m *MockStore) GetProvisionerJobByID(arg0 context.Context, arg1 uuid.UUID) (database.ProvisionerJob, error) { m.ctrl.T.Helper() @@ -2076,6 +2091,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/dump.sql b/coderd/database/dump.sql index 782bc4969d799..a0daef2ff736e 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -137,6 +137,14 @@ CREATE TYPE port_share_protocol AS ENUM ( 'https' ); +CREATE TYPE provisioner_daemon_status AS ENUM ( + 'offline', + 'idle', + 'busy' +); + +COMMENT ON TYPE provisioner_daemon_status IS 'The status of a provisioner daemon.'; + CREATE TYPE provisioner_job_status AS ENUM ( 'pending', 'running', diff --git a/coderd/database/migrations/000280_provisioner_daemon_status.down.sql b/coderd/database/migrations/000280_provisioner_daemon_status.down.sql new file mode 100644 index 0000000000000..f4fd46d4a0658 --- /dev/null +++ b/coderd/database/migrations/000280_provisioner_daemon_status.down.sql @@ -0,0 +1 @@ +DROP TYPE provisioner_daemon_status; diff --git a/coderd/database/migrations/000280_provisioner_daemon_status.up.sql b/coderd/database/migrations/000280_provisioner_daemon_status.up.sql new file mode 100644 index 0000000000000..990113d4f7af0 --- /dev/null +++ b/coderd/database/migrations/000280_provisioner_daemon_status.up.sql @@ -0,0 +1,3 @@ +CREATE TYPE provisioner_daemon_status AS ENUM ('offline', 'idle', 'busy'); + +COMMENT ON TYPE provisioner_daemon_status IS 'The status of a provisioner daemon.'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index ca74c121bc0a6..0836a948d7ad0 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -269,6 +269,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object { InOrg(p.OrganizationID) } +func (p GetProvisionerDaemonsWithStatusByOrganizationRow) RBACObject() rbac.Object { + return p.ProvisionerDaemon.RBACObject() +} + func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object { return p.ProvisionerDaemon.RBACObject() } diff --git a/coderd/database/models.go b/coderd/database/models.go index e5ddebcbc8b9a..443fe22df71a4 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1209,6 +1209,68 @@ func AllPortShareProtocolValues() []PortShareProtocol { } } +// The status of a provisioner daemon. +type ProvisionerDaemonStatus string + +const ( + ProvisionerDaemonStatusOffline ProvisionerDaemonStatus = "offline" + ProvisionerDaemonStatusIdle ProvisionerDaemonStatus = "idle" + ProvisionerDaemonStatusBusy ProvisionerDaemonStatus = "busy" +) + +func (e *ProvisionerDaemonStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ProvisionerDaemonStatus(s) + case string: + *e = ProvisionerDaemonStatus(s) + default: + return fmt.Errorf("unsupported scan type for ProvisionerDaemonStatus: %T", src) + } + return nil +} + +type NullProvisionerDaemonStatus struct { + ProvisionerDaemonStatus ProvisionerDaemonStatus `json:"provisioner_daemon_status"` + Valid bool `json:"valid"` // Valid is true if ProvisionerDaemonStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullProvisionerDaemonStatus) Scan(value interface{}) error { + if value == nil { + ns.ProvisionerDaemonStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ProvisionerDaemonStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullProvisionerDaemonStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ProvisionerDaemonStatus), nil +} + +func (e ProvisionerDaemonStatus) Valid() bool { + switch e { + case ProvisionerDaemonStatusOffline, + ProvisionerDaemonStatusIdle, + ProvisionerDaemonStatusBusy: + return true + } + return false +} + +func AllProvisionerDaemonStatusValues() []ProvisionerDaemonStatus { + return []ProvisionerDaemonStatus{ + ProvisionerDaemonStatusOffline, + ProvisionerDaemonStatusIdle, + ProvisionerDaemonStatusBusy, + } +} + // Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state. type ProvisionerJobStatus string diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 2128315ce6dad..beed34d142e69 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -199,10 +199,12 @@ type sqlcQuerier interface { GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) + GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) 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 1a7911bc64b4d..f162e26f1a66c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5404,6 +5404,118 @@ func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, ar return items, nil } +const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDaemonsWithStatusByOrganization :many +SELECT + pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id, + CASE + WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($1::bigint || ' ms')::interval) + THEN 'offline' + ELSE CASE + WHEN current_job.id IS NOT NULL THEN 'busy' + ELSE 'idle' + END + END::provisioner_daemon_status AS status, + pk.name AS key_name, + -- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them. + current_job.id AS current_job_id, + current_job.job_status AS current_job_status, + previous_job.id AS previous_job_id, + previous_job.job_status AS previous_job_status +FROM + provisioner_daemons pd +JOIN + provisioner_keys pk ON pk.id = pd.key_id +LEFT JOIN + provisioner_jobs current_job ON ( + current_job.worker_id = pd.id + AND current_job.completed_at IS NULL + ) +LEFT JOIN + provisioner_jobs previous_job ON ( + previous_job.id = ( + SELECT + id + FROM + provisioner_jobs + WHERE + worker_id = pd.id + AND completed_at IS NOT NULL + ORDER BY + completed_at DESC + LIMIT 1 + ) + ) +WHERE + pd.organization_id = $2::uuid + AND (COALESCE(array_length($3::uuid[], 1), 1) > 0 OR pd.id = ANY($3::uuid[])) + AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $4::tagset)) +ORDER BY + pd.created_at ASC +` + +type GetProvisionerDaemonsWithStatusByOrganizationParams struct { + StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + IDs []uuid.UUID `db:"ids" json:"ids"` + Tags StringMap `db:"tags" json:"tags"` +} + +type GetProvisionerDaemonsWithStatusByOrganizationRow struct { + ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` + Status ProvisionerDaemonStatus `db:"status" json:"status"` + KeyName string `db:"key_name" json:"key_name"` + CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"` + CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"` + PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"` + PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"` +} + +func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization, + arg.StaleIntervalMS, + arg.OrganizationID, + pq.Array(arg.IDs), + arg.Tags, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProvisionerDaemonsWithStatusByOrganizationRow + for rows.Next() { + var i GetProvisionerDaemonsWithStatusByOrganizationRow + if err := rows.Scan( + &i.ProvisionerDaemon.ID, + &i.ProvisionerDaemon.CreatedAt, + &i.ProvisionerDaemon.Name, + pq.Array(&i.ProvisionerDaemon.Provisioners), + &i.ProvisionerDaemon.ReplicaID, + &i.ProvisionerDaemon.Tags, + &i.ProvisionerDaemon.LastSeenAt, + &i.ProvisionerDaemon.Version, + &i.ProvisionerDaemon.APIVersion, + &i.ProvisionerDaemon.OrganizationID, + &i.ProvisionerDaemon.KeyID, + &i.Status, + &i.KeyName, + &i.CurrentJobID, + &i.CurrentJobStatus, + &i.PreviousJobID, + &i.PreviousJobStatus, + ); 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 updateProvisionerDaemonLastSeenAt = `-- name: UpdateProvisionerDaemonLastSeenAt :exec UPDATE provisioner_daemons SET @@ -5954,6 +6066,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), 1) > 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/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index f76f71f5015bf..e531960f674ab 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -28,6 +28,54 @@ JOIN WHERE provisioner_jobs.id = ANY(@provisioner_job_ids :: uuid[]); +-- name: GetProvisionerDaemonsWithStatusByOrganization :many +SELECT + sqlc.embed(pd), + CASE + WHEN pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - (@stale_interval_ms::bigint || ' ms')::interval) + THEN 'offline' + ELSE CASE + WHEN current_job.id IS NOT NULL THEN 'busy' + ELSE 'idle' + END + END::provisioner_daemon_status AS status, + pk.name AS key_name, + -- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them. + current_job.id AS current_job_id, + current_job.job_status AS current_job_status, + previous_job.id AS previous_job_id, + previous_job.job_status AS previous_job_status +FROM + provisioner_daemons pd +JOIN + provisioner_keys pk ON pk.id = pd.key_id +LEFT JOIN + provisioner_jobs current_job ON ( + current_job.worker_id = pd.id + AND current_job.completed_at IS NULL + ) +LEFT JOIN + provisioner_jobs previous_job ON ( + previous_job.id = ( + SELECT + id + FROM + provisioner_jobs + WHERE + worker_id = pd.id + AND completed_at IS NOT NULL + ORDER BY + completed_at DESC + LIMIT 1 + ) + ) +WHERE + pd.organization_id = @organization_id::uuid + AND (COALESCE(array_length(@ids::uuid[], 1), 1) > 0 OR pd.id = ANY(@ids::uuid[])) + AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, @tags::tagset)) +ORDER BY + pd.created_at ASC; + -- name: DeleteOldProvisionerDaemons :exec -- Delete provisioner daemons that have been created at least a week ago -- and have not connected to coderd since a week. diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index ac246d4e2ef68..b31bf0454a1f9 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), 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; + -- name: GetProvisionerJobsCreatedAfter :many SELECT * FROM provisioner_jobs WHERE created_at > $1; diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index fac159f71ebe3..b43281a3f1051 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -146,6 +146,7 @@ sql: login_type_oauth2_provider_app: LoginTypeOAuth2ProviderApp crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert + stale_interval_ms: StaleIntervalMS rules: - name: do-not-use-public-schema-in-queries message: "do not use public schema in queries" diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go new file mode 100644 index 0000000000000..879ee651bc107 --- /dev/null +++ b/coderd/provisionerdaemons.go @@ -0,0 +1,79 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/codersdk" +) + +// @Summary Get provisioner daemons +// @ID get-provisioner-daemons +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" format(uuid) +// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" +// @Success 200 {array} codersdk.ProvisionerDaemonWithStatus +// @Router /organizations/{organization}/provisionerdaemons [get] +func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + org = httpmw.OrganizationParam(r) + tagParam = r.URL.Query().Get("tags") + tags = database.StringMap{} + err = tags.Scan([]byte(tagParam)) + ) + + if tagParam != "" && err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid tags query parameter", + Detail: err.Error(), + }) + return + } + + daemons, err := api.Database.GetProvisionerDaemonsWithStatusByOrganization( + ctx, + database.GetProvisionerDaemonsWithStatusByOrganizationParams{ + OrganizationID: org.ID, + StaleIntervalMS: provisionerdserver.StaleInterval.Milliseconds(), + Tags: tags, + }, + ) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching provisioner daemons.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, func(dbDaemon database.GetProvisionerDaemonsWithStatusByOrganizationRow) codersdk.ProvisionerDaemonWithStatus { + pd := db2sdk.ProvisionerDaemon(dbDaemon.ProvisionerDaemon) + var currentJob, previousJob *codersdk.ProvisionerDaemonJob + if dbDaemon.CurrentJobID.Valid { + currentJob = &codersdk.ProvisionerDaemonJob{ + ID: dbDaemon.CurrentJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.CurrentJobStatus.ProvisionerJobStatus), + } + } + if dbDaemon.PreviousJobID.Valid { + previousJob = &codersdk.ProvisionerDaemonJob{ + ID: dbDaemon.PreviousJobID.UUID, + Status: codersdk.ProvisionerJobStatus(dbDaemon.PreviousJobStatus.ProvisionerJobStatus), + } + } + return codersdk.ProvisionerDaemonWithStatus{ + ProvisionerDaemon: pd, + KeyName: dbDaemon.KeyName, + Status: codersdk.ProvisionerDaemonStatus(dbDaemon.Status), + CurrentJob: currentJob, + PreviousJob: previousJob, + } + })) +} diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go new file mode 100644 index 0000000000000..74f5559b38fb5 --- /dev/null +++ b/coderd/provisionerdaemons_test.go @@ -0,0 +1,32 @@ +package coderd_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/testutil" +) + +func TestGetProvisionerDaemons(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + db, ps, _ := dbtestutil.NewDBWithSQLDB(t, dbtestutil.WithDumpOnFailure()) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + IncludeProvisionerDaemon: true, + }) + coderdtest.CreateFirstUser(t, client) + + ctx := testutil.Context(t, testutil.WaitMedium) + + daemons, err := client.ProvisionerDaemons(ctx) + require.NoError(t, err) + require.Len(t, daemons, 1) + }) +} diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 4269f9a8dd57f..9646bd8e2fb60 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -19,12 +19,65 @@ 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/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) + + qp := r.URL.Query() + p := httpapi.NewQueryParamParser() + limit := p.PositiveInt32(qp, 0, "limit") + status := p.Strings(qp, []string(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.ToStringEnums[database.ProvisionerJobStatus](status), + Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, + }) + if err != nil { + 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 +289,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 +315,9 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR } job.Status = codersdk.ProvisionerJobStatus(pj.ProvisionerJob.JobStatus) + // Hope this never breaks to avoid changing function signature. + _ = json.Unmarshal(provisionerJob.Input, &job.Input) + return job } diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 7317a801a089f..b427bb187cc78 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -13,6 +13,14 @@ func ToStrings[T ~string](a []T) []string { return tmp } +func ToStringEnums[E ~string](a []string) []E { + 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..48d9eb8aae238 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -315,7 +316,7 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e return daemons, json.NewDecoder(res.Body).Decode(&daemons) } -func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemon, error) { +func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, tags map[string]string) ([]ProvisionerDaemonWithStatus, error) { baseURL := fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()) queryParams := url.Values{} @@ -339,10 +340,51 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio return nil, ReadBodyAsError(res) } - var daemons []ProvisionerDaemon + var daemons []ProvisionerDaemonWithStatus 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 a2c8f2109f414..0e786570b0a76 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -39,17 +39,40 @@ const ( LogLevelError LogLevel = "error" ) +// ProvisionerDaemonStatus represents the status of a provisioner daemon. +type ProvisionerDaemonStatus string + +// ProvisionerDaemonStatus enums. +const ( + ProvisionerDaemonOffline ProvisionerDaemonStatus = "offline" + ProvisionerDaemonIdle ProvisionerDaemonStatus = "idle" + ProvisionerDaemonBusy ProvisionerDaemonStatus = "busy" +) + type ProvisionerDaemon struct { - ID uuid.UUID `json:"id" format:"uuid"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - KeyID uuid.UUID `json:"key_id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time"` - Name string `json:"name"` - Version string `json:"version"` - APIVersion string `json:"api_version"` - Provisioners []ProvisionerType `json:"provisioners"` - Tags map[string]string `json:"tags"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` + KeyID uuid.UUID `json:"key_id" format:"uuid" table:"-"` + CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"` + LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time" table:"last seen at"` + Name string `json:"name" table:"name,default_sort"` + Version string `json:"version" table:"version"` + APIVersion string `json:"api_version" table:"api version"` + Provisioners []ProvisionerType `json:"provisioners" table:"-"` + Tags map[string]string `json:"tags" table:"tags"` +} + +type ProvisionerDaemonWithStatus struct { + ProvisionerDaemon `table:"provisioner daemon,recursive_inline"` + KeyName string `json:"key_name" table:"key name"` + Status ProvisionerDaemonStatus `json:"status" enums:"offline,idle,busy" table:"status"` + CurrentJob *ProvisionerDaemonJob `json:"current_job" table:"current job,recursive"` + PreviousJob *ProvisionerDaemonJob `json:"previous_job" table:"previous job,recursive"` +} + +type ProvisionerDaemonJob struct { + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` } // MatchedProvisioners represents the number of provisioner daemons @@ -91,6 +114,33 @@ 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"` +} + +// 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 @@ -106,19 +156,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/docs/manifest.json b/docs/manifest.json index fcac552ee0496..7767f995e73d0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1140,9 +1140,19 @@ }, { "title": "provisioner", - "description": "Manage provisioner daemons", + "description": "View and manage provisioner daemons and jobs", "path": "reference/cli/provisioner.md" }, + { + "title": "provisioner jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioner_jobs.md" + }, + { + "title": "provisioner jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioner_jobs_list.md" + }, { "title": "provisioner keys", "description": "Manage provisioner keys", @@ -1163,6 +1173,11 @@ "description": "List provisioner keys in an organization", "path": "reference/cli/provisioner_keys_list.md" }, + { + "title": "provisioner list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioner_list.md" + }, { "title": "provisioner start", "description": "Run a provisioner daemon", diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 1cbe384df8778..7dfb2cdb4b8a2 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -35,6 +35,7 @@ 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 +43,11 @@ 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": { + "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 +56,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": { @@ -225,6 +232,7 @@ 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", @@ -232,6 +240,11 @@ 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": { + "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", @@ -240,6 +253,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": { @@ -849,6 +863,7 @@ 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", @@ -856,6 +871,11 @@ 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": { + "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", @@ -864,6 +884,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": { @@ -1112,6 +1133,7 @@ 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", @@ -1119,6 +1141,11 @@ 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": { + "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", @@ -1127,6 +1154,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": { @@ -1283,6 +1311,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 | | | @@ -1290,12 +1319,17 @@ 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 | | | +| `»»» 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. | @@ -1409,6 +1443,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` | @@ -1507,6 +1544,7 @@ 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", @@ -1514,6 +1552,11 @@ 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": { + "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", @@ -1522,6 +1565,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/enterprise.md b/docs/reference/api/enterprise.md index 8913e45257af5..6f881d0e1c4a3 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1494,12 +1494,22 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi { "api_version": "string", "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", "last_seen_at": "2019-08-24T14:15:22Z", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, "provisioners": ["string"], + "status": "offline", "tags": { "property1": "string", "property2": "string" @@ -1511,28 +1521,48 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemon](schemas.md#codersdkprovisionerdaemon) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerDaemonWithStatus](schemas.md#codersdkprovisionerdaemonwithstatus) |

Response Schema

Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ------------------- | ----------------- | -------- | ------------ | ----------- | -| `[array item]` | array | false | | | -| `» api_version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» id` | string(uuid) | false | | | -| `» key_id` | string(uuid) | false | | | -| `» last_seen_at` | string(date-time) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» provisioners` | array | false | | | -| `» tags` | object | false | | | -| `»» [any property]` | string | false | | | -| `» version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» api_version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» current_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `» id` | string(uuid) | false | | | +| `» key_id` | string(uuid) | false | | | +| `» key_name` | string | false | | | +| `» last_seen_at` | string(date-time) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» previous_job` | [codersdk.ProvisionerDaemonJob](schemas.md#codersdkprovisionerdaemonjob) | false | | | +| `» provisioners` | array | false | | | +| `» status` | [codersdk.ProvisionerDaemonStatus](schemas.md#codersdkprovisionerdaemonstatus) | false | | | +| `» tags` | object | false | | | +| `»» [any property]` | string | false | | | +| `» version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +| -------- | ----------- | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/organizations.md b/docs/reference/api/organizations.md index e398d8e7c0105..74859ee490e68 100644 --- a/docs/reference/api/organizations.md +++ b/docs/reference/api/organizations.md @@ -343,3 +343,127 @@ 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": { + "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 | | | +| `»» 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 b124e7be93b26..2e912b52eb778 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4189,10 +4189,112 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | » `[any property]` | string | false | | | | `version` | string | false | | | +## codersdk.ProvisionerDaemonJob + +```json +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------- | -------------------------------------------------------------- | -------- | ------------ | ----------- | +| `id` | string | false | | | +| `status` | [codersdk.ProvisionerJobStatus](#codersdkprovisionerjobstatus) | false | | | + +#### Enumerated Values + +| Property | Value | +| -------- | ----------- | +| `status` | `pending` | +| `status` | `running` | +| `status` | `succeeded` | +| `status` | `canceling` | +| `status` | `canceled` | +| `status` | `failed` | + +## codersdk.ProvisionerDaemonStatus + +```json +"offline" +``` + +### Properties + +#### Enumerated Values + +| Value | +| --------- | +| `offline` | +| `idle` | +| `busy` | + +## codersdk.ProvisionerDaemonWithStatus + +```json +{ + "api_version": "string", + "created_at": "2019-08-24T14:15:22Z", + "current_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5", + "key_name": "string", + "last_seen_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "previous_job": { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "status": "pending" + }, + "provisioners": ["string"], + "status": "offline", + "tags": { + "property1": "string", + "property2": "string" + }, + "version": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | -------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `api_version` | string | false | | | +| `created_at` | string | false | | | +| `current_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `id` | string | false | | | +| `key_id` | string | false | | | +| `key_name` | string | false | | | +| `last_seen_at` | string | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `previous_job` | [codersdk.ProvisionerDaemonJob](#codersdkprovisionerdaemonjob) | false | | | +| `provisioners` | array of string | false | | | +| `status` | [codersdk.ProvisionerDaemonStatus](#codersdkprovisionerdaemonstatus) | false | | | +| `tags` | object | false | | | +| » `[any property]` | string | false | | | +| `version` | string | false | | | + +#### Enumerated Values + +| Property | Value | +| -------- | --------- | +| `status` | `offline` | +| `status` | `idle` | +| `status` | `busy` | + ## codersdk.ProvisionerJob ```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", @@ -4200,6 +4302,11 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "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", @@ -4208,28 +4315,33 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "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 @@ -4243,6 +4355,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `status` | `canceled` | | `status` | `failed` | +## codersdk.ProvisionerJobInput + +```json +{ + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------------------- | ------ | -------- | ------------ | ----------- | +| `template_version_id` | string | false | | | +| `workspace_build_id` | string | false | | | + ## codersdk.ProvisionerJobLog ```json @@ -4297,6 +4425,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `failed` | | `unknown` | +## codersdk.ProvisionerJobType + +```json +"template_version_import" +``` + +### Properties + +#### Enumerated Values + +| Value | +| -------------------------- | +| `template_version_import` | +| `workspace_build` | +| `template_version_dry_run` | + ## codersdk.ProvisionerKey ```json @@ -5573,6 +5717,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "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", @@ -5580,6 +5725,11 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "error_code": "REQUIRED_TEMPLATE_VARIABLES", "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "input": { + "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", @@ -5588,6 +5738,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -6585,6 +6736,7 @@ 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", @@ -6592,6 +6744,11 @@ 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": { + "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", @@ -6600,6 +6757,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": { @@ -7290,6 +7448,7 @@ 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", @@ -7297,6 +7456,11 @@ 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": { + "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", @@ -7305,6 +7469,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": { @@ -7922,6 +8087,7 @@ 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", @@ -7929,6 +8095,11 @@ 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": { + "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", @@ -7937,6 +8108,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 b4f642625dcde..0f2c322fb060a 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -429,6 +429,7 @@ 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", @@ -436,6 +437,11 @@ 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": { + "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", @@ -444,6 +450,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": { @@ -505,6 +512,7 @@ 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", @@ -512,6 +520,11 @@ 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": { + "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", @@ -520,6 +533,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": { @@ -605,6 +619,7 @@ 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", @@ -612,6 +627,11 @@ 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": { + "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", @@ -620,6 +640,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": { @@ -1119,6 +1140,7 @@ 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", @@ -1126,6 +1148,11 @@ 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": { + "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", @@ -1134,6 +1161,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": { @@ -1162,42 +1190,48 @@ 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 | | | +| `»»» 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 @@ -1210,6 +1244,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). @@ -1357,6 +1394,7 @@ 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", @@ -1364,6 +1402,11 @@ 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": { + "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", @@ -1372,6 +1415,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": { @@ -1400,42 +1444,48 @@ 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 | | | +| `»»» 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 @@ -1448,6 +1498,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). @@ -1485,6 +1538,7 @@ 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", @@ -1492,6 +1546,11 @@ 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": { + "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", @@ -1500,6 +1559,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": { @@ -1570,6 +1630,7 @@ 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", @@ -1577,6 +1638,11 @@ 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": { + "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", @@ -1585,6 +1651,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": { @@ -1745,6 +1812,7 @@ 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", @@ -1752,6 +1820,11 @@ 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": { + "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", @@ -1760,6 +1833,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" } ``` @@ -1798,6 +1872,7 @@ 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", @@ -1805,6 +1880,11 @@ 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": { + "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", @@ -1813,6 +1893,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 b855615336c45..4dbc81499c0e9 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -74,6 +74,7 @@ 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", @@ -81,6 +82,11 @@ 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": { + "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", @@ -89,6 +95,7 @@ of the template will be used. "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -298,6 +305,7 @@ 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", @@ -305,6 +313,11 @@ 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": { + "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", @@ -313,6 +326,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": { @@ -546,6 +560,7 @@ 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", @@ -553,6 +568,11 @@ 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": { + "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", @@ -561,6 +581,7 @@ of the template will be used. "property1": "string", "property2": "string" }, + "type": "template_version_import", "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" }, "matched_provisioners": { @@ -773,6 +794,7 @@ 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", @@ -780,6 +802,11 @@ 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": { + "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", @@ -788,6 +815,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": { @@ -994,6 +1022,7 @@ 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", @@ -1001,6 +1030,11 @@ 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": { + "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", @@ -1009,6 +1043,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": { @@ -1334,6 +1369,7 @@ 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", @@ -1341,6 +1377,11 @@ 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": { + "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", @@ -1349,6 +1390,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/docs/reference/cli/index.md b/docs/reference/cli/index.md index 525cb8ac7d183..d21aa3d208f29 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -66,7 +66,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | [features](./features.md) | List Enterprise features | | [licenses](./licenses.md) | Add, delete, and list licenses | | [groups](./groups.md) | Manage groups | -| [provisioner](./provisioner.md) | Manage provisioner daemons | +| [provisioner](./provisioner.md) | View and manage provisioner daemons and jobs | ## Options diff --git a/docs/reference/cli/provisioner.md b/docs/reference/cli/provisioner.md index 54cc28a84bea4..12a70203748e1 100644 --- a/docs/reference/cli/provisioner.md +++ b/docs/reference/cli/provisioner.md @@ -2,7 +2,7 @@ # provisioner -Manage provisioner daemons +View and manage provisioner daemons and jobs Aliases: @@ -16,7 +16,9 @@ coder provisioner ## Subcommands -| Name | Purpose | -| -------------------------------------------- | ------------------------ | -| [start](./provisioner_start.md) | Run a provisioner daemon | -| [keys](./provisioner_keys.md) | Manage provisioner keys | +| Name | Purpose | +| -------------------------------------------- | ------------------------------------------- | +| [list](./provisioner_list.md) | List provisioner daemons in an organization | +| [jobs](./provisioner_jobs.md) | View and manage provisioner jobs | +| [start](./provisioner_start.md) | Run a provisioner daemon | +| [keys](./provisioner_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioner_jobs.md b/docs/reference/cli/provisioner_jobs.md new file mode 100644 index 0000000000000..c8256370686de --- /dev/null +++ b/docs/reference/cli/provisioner_jobs.md @@ -0,0 +1,21 @@ + + +# provisioner jobs + +View and manage provisioner jobs + +Aliases: + +- job + +## Usage + +```console +coder provisioner jobs +``` + +## Subcommands + +| Name | Purpose | +| ----------------------------------------------- | --------------------- | +| [list](./provisioner_jobs_list.md) | List provisioner jobs | diff --git a/docs/reference/cli/provisioner_jobs_list.md b/docs/reference/cli/provisioner_jobs_list.md new file mode 100644 index 0000000000000..6e4dad23442ec --- /dev/null +++ b/docs/reference/cli/provisioner_jobs_list.md @@ -0,0 +1,63 @@ + + +# provisioner jobs list + +List provisioner jobs + +Aliases: + +- ls + +## Usage + +```console +coder provisioner jobs list [flags] +``` + +## Options + +### -s, --status + +| | | +| ----------- | -------------------------------------------------------------------------------- | +| Type | [pending\|running\|succeeded\|canceling\|canceled\|failed\|unknown] | +| Environment | $CODER_PROVISIONER_JOB_LIST_STATUS | + +Filter by job status. + +### -l, --limit + +| | | +| ----------- | ---------------------------------------------- | +| Type | int | +| Environment | $CODER_PROVISIONER_JOB_LIST_LIMIT | +| Default | 50 | + +Limit the number of jobs returned. + +### -O, --org + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. + +### -c, --column + +| | | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Type | [id\|created at\|started at\|completed at\|canceled at\|error\|error code\|status\|worker id\|file id\|tags\|queue position\|queue size\|organization id\|template version id\|workspace build id\|type\|available workers\|organization\|queue] | +| Default | created at,id,organization,status,type,queue,tags | + +Columns to display in table output. + +### -o, --output + +| | | +| ------- | ------------------------ | +| Type | table\|json | +| Default | table | + +Output format. diff --git a/docs/reference/cli/provisioner_keys_list.md b/docs/reference/cli/provisioner_keys_list.md index 366db05fa490f..d2c174d7e6d68 100644 --- a/docs/reference/cli/provisioner_keys_list.md +++ b/docs/reference/cli/provisioner_keys_list.md @@ -24,3 +24,21 @@ coder provisioner keys list [flags] | Environment | $CODER_ORGANIZATION | Select which organization (uuid or name) to use. + +### -c, --column + +| | | +| ------- | ------------------------------------- | +| Type | [created at\|name\|tags] | +| Default | created at,name,tags | + +Columns to display in table output. + +### -o, --output + +| | | +| ------- | ------------------------ | +| Type | table\|json | +| Default | table | + +Output format. diff --git a/docs/reference/cli/provisioner_list.md b/docs/reference/cli/provisioner_list.md new file mode 100644 index 0000000000000..f5c476cb8facf --- /dev/null +++ b/docs/reference/cli/provisioner_list.md @@ -0,0 +1,44 @@ + + +# provisioner list + +List provisioner daemons in an organization + +Aliases: + +- ls + +## Usage + +```console +coder provisioner list [flags] +``` + +## Options + +### -O, --org + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. + +### -c, --column + +| | | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Type | [id\|organization id\|created at\|last seen at\|name\|version\|api version\|tags\|key name\|status\|current job id\|current job status\|previous job id\|previous job status\|organization] | +| Default | name,organization,status,key name,created at,last seen at,version,tags | + +Columns to display in table output. + +### -o, --output + +| | | +| ------- | ------------------------ | +| Type | table\|json | +| Default | table | + +Output format. diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 8d39723a269e3..690762dcc613b 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -1,20 +1,15 @@ package cli -import "github.com/coder/serpent" +import ( + "github.com/coder/serpent" +) func (r *RootCmd) provisionerDaemons() *serpent.Command { - cmd := &serpent.Command{ - Use: "provisioner", - Short: "Manage provisioner daemons", - Handler: func(inv *serpent.Invocation) error { - return inv.Command.HelpHandler(inv) - }, - Aliases: []string{"provisioners"}, - Children: []*serpent.Command{ - r.provisionerDaemonStart(), - r.provisionerKeys(), - }, - } + cmd := r.RootCmd.Provisioners() + cmd.AddSubcommands( + r.provisionerDaemonStart(), + r.provisionerKeys(), + ) return cmd } diff --git a/enterprise/cli/provisionerdaemonstart_slim.go b/enterprise/cli/provisionerdaemonstart_slim.go index aa399e9b9a46c..5e43393480c6d 100644 --- a/enterprise/cli/provisionerdaemonstart_slim.go +++ b/enterprise/cli/provisionerdaemonstart_slim.go @@ -15,7 +15,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command { RawArgs: true, Hidden: true, Handler: func(inv *serpent.Invocation) error { - agplcli.SlimUnsupported(inv.Stderr, "provisionerd start") + agplcli.SlimUnsupported(inv.Stderr, "provisioner start") return nil }, } diff --git a/enterprise/cli/provisionerdaemonstart_test.go b/enterprise/cli/provisionerdaemonstart_test.go index 4829ccc38f23d..2a04b507ed81d 100644 --- a/enterprise/cli/provisionerdaemonstart_test.go +++ b/enterprise/cli/provisionerdaemonstart_test.go @@ -233,7 +233,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { clitest.Start(t, inv) pty.ExpectMatchContext(ctx, "starting provisioner daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus var err error require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) @@ -280,7 +280,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -325,7 +325,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, `tags={"tag1":"value1","tag2":"value2"}`) - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID, nil) if err != nil { @@ -441,7 +441,7 @@ func TestProvisionerDaemon_ProvisionerKey(t *testing.T) { pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon") pty.ExpectMatchContext(ctx, "matt-daemon") - var daemons []codersdk.ProvisionerDaemon + var daemons []codersdk.ProvisionerDaemonWithStatus require.Eventually(t, func() bool { daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID, nil) if err != nil { diff --git a/enterprise/cli/provisionerkeys.go b/enterprise/cli/provisionerkeys.go index 99d8bd8acf9ab..f88a5ffe851e6 100644 --- a/enterprise/cli/provisionerkeys.go +++ b/enterprise/cli/provisionerkeys.go @@ -138,8 +138,8 @@ func (r *RootCmd) provisionerKeysList() *serpent.Command { }, } - cmd.Options = serpent.OptionSet{} orgContext.AttachOptions(cmd) + formatter.AttachOptions(&cmd.Options) return cmd } diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 9b3584a3e48b2..ca5d8c8c886ef 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -17,7 +17,7 @@ SUBCOMMANDS: features List Enterprise features groups Manage groups licenses Add, delete, and list licenses - provisioner Manage provisioner daemons + provisioner View and manage provisioner daemons and jobs server Start a Coder server GLOBAL OPTIONS: diff --git a/enterprise/cli/testdata/coder_provisioner_--help.golden b/enterprise/cli/testdata/coder_provisioner_--help.golden index e6cd69feeceac..79c82987f1311 100644 --- a/enterprise/cli/testdata/coder_provisioner_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_--help.golden @@ -3,12 +3,14 @@ coder v0.0.0-devel USAGE: coder provisioner - Manage provisioner daemons + View and manage provisioner daemons and jobs Aliases: provisioners SUBCOMMANDS: + jobs View and manage provisioner jobs keys Manage provisioner keys + list List provisioner daemons in an organization start Run a provisioner daemon ——— diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden new file mode 100644 index 0000000000000..6442c78a03a8e --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner jobs + + View and manage provisioner jobs + + Aliases: job + +SUBCOMMANDS: + list List provisioner jobs + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden new file mode 100644 index 0000000000000..585e918c23e7b --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -0,0 +1,27 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner jobs list [flags] + + List provisioner jobs + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|organization|queue] (default: created at,id,organization,status,type,queue,tags) + Columns to display in table output. + + -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) + Limit the number of jobs returned. + + -o, --output table|json (default: table) + Output format. + + -s, --status [pending|running|succeeded|canceling|canceled|failed|unknown], $CODER_PROVISIONER_JOB_LIST_STATUS + Filter by job status. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden index 59bddf9f71991..e7bc4c46895c3 100644 --- a/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden @@ -11,5 +11,11 @@ OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. + -c, --column [created at|name|tags] (default: created at,name,tags) + Columns to display in table output. + + -o, --output table|json (default: table) + Output format. + ——— Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioner_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_list_--help.golden new file mode 100644 index 0000000000000..a9943cb9da392 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_list_--help.golden @@ -0,0 +1,21 @@ +coder v0.0.0-devel + +USAGE: + coder provisioner list [flags] + + List provisioner daemons in an organization + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -c, --column [id|organization id|created at|last seen at|name|version|api version|tags|key name|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,key name,created at,last seen at,version,tags) + Columns to display in table output. + + -o, --output table|json (default: table) + Output format. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 71273ff97fd75..cfd7e3c7ab2e1 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -377,7 +377,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { // // We may in future decide to scope provisioner daemons to organizations, so we'll keep the API // route as is. - r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) { + r.Route("/organizations/{organization}/provisionerdaemons/serve", func(r chi.Router) { r.Use( api.provisionerDaemonsEnabledMW, apiKeyMiddlewareOptional, @@ -391,8 +391,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { httpmw.RequireAPIKeyOrProvisionerDaemonAuth(), httpmw.ExtractOrganizationParam(api.Database), ) - r.With(apiKeyMiddleware).Get("/", api.provisionerDaemons) - r.With(apiKeyMiddlewareOptional).Get("/serve", api.provisionerDaemonServe) + r.Get("/", api.provisionerDaemonServe) }) r.Route("/templates/{template}/acl", func(r chi.Router) { r.Use( diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 41991897f6614..f4335438654b5 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -20,7 +20,6 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" @@ -49,50 +48,6 @@ func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler { }) } -// @Summary Get provisioner daemons -// @ID get-provisioner-daemons -// @Security CoderSessionToken -// @Produce json -// @Tags Enterprise -// @Param organization path string true "Organization ID" format(uuid) -// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" -// @Success 200 {array} codersdk.ProvisionerDaemon -// @Router /organizations/{organization}/provisionerdaemons [get] -func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - org = httpmw.OrganizationParam(r) - tagParam = r.URL.Query().Get("tags") - tags = database.StringMap{} - err = tags.Scan([]byte(tagParam)) - ) - - if tagParam != "" && err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid tags query parameter", - Detail: err.Error(), - }) - return - } - - daemons, err := api.Database.GetProvisionerDaemonsByOrganization( - ctx, - database.GetProvisionerDaemonsByOrganizationParams{ - OrganizationID: org.ID, - WantTags: tags, - }, - ) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner daemons.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, db2sdk.ProvisionerDaemon)) -} - type provisiionerDaemonAuthResponse struct { keyID uuid.UUID orgID uuid.UUID diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5d840d763af78..565d6488f3b33 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1239,6 +1239,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; @@ -1345,12 +1351,31 @@ export interface ProvisionerDaemon { readonly tags: Record; } +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemonJob { + readonly id: string; + readonly status: ProvisionerJobStatus; +} + // From codersdk/client.go export const ProvisionerDaemonKey = "Coder-Provisioner-Daemon-Key"; // From codersdk/client.go export const ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"; +// From codersdk/provisionerdaemons.go +export type ProvisionerDaemonStatus = "busy" | "idle" | "offline"; + +export const ProvisionerDaemonStatuses: ProvisionerDaemonStatus[] = ["busy", "idle", "offline"]; + +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemonWithStatus extends ProvisionerDaemon { + readonly key_name: string; + readonly status: ProvisionerDaemonStatus; + readonly current_job: ProvisionerDaemonJob | null; + readonly previous_job: ProvisionerDaemonJob | null; +} + // From healthsdk/healthsdk.go export interface ProvisionerDaemonsReport extends BaseReport { readonly items: readonly ProvisionerDaemonsReportItem[]; @@ -1377,6 +1402,16 @@ 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; } // From codersdk/provisionerdaemons.go @@ -1394,6 +1429,11 @@ export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pendin export const ProvisionerJobStatuses: ProvisionerJobStatus[] = ["canceled", "canceling", "failed", "pending", "running", "succeeded", "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; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5686e503eaf60..4a7b58d5eb38d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -648,6 +648,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 = {