From 650cbaf3ab2775c568dcd973209d4d94ff60fa22 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 16:54:51 +0200 Subject: [PATCH 01/21] feat: add provisioners and jobs endspoints and CLI commands --- cli/provisionerjobs.go | 125 ++++++++++ cli/provisioners.go | 93 ++++++++ cli/root.go | 6 +- coderd/apidoc/docs.go | 209 ++++++++++++++++ coderd/apidoc/swagger.json | 197 +++++++++++++++ coderd/coderd.go | 6 + coderd/database/dbauthz/dbauthz.go | 9 + coderd/database/dbmem/dbmem.go | 209 ++++++++++++++++ coderd/database/dbmetrics/querymetrics.go | 14 ++ coderd/database/dbmock/dbmock.go | 30 +++ coderd/database/dump.sql | 8 + .../000279_provisioner_daemon_status.down.sql | 1 + .../000279_provisioner_daemon_status.up.sql | 3 + coderd/database/modelmethods.go | 4 + coderd/database/models.go | 62 +++++ coderd/database/querier.go | 2 + coderd/database/queries.sql.go | 221 +++++++++++++++++ .../database/queries/provisionerdaemons.sql | 43 ++++ coderd/database/queries/provisionerjobs.sql | 56 +++++ coderd/database/sqlc.yaml | 1 + coderd/provisionerdaemons.go | 78 ++++++ coderd/provisionerdaemons_test.go | 32 +++ coderd/provisionerjobs.go | 80 ++++++- codersdk/organizations.go | 46 +++- codersdk/provisionerdaemons.go | 99 ++++++-- docs/manifest.json | 46 +++- docs/reference/api/builds.md | 44 ++++ docs/reference/api/organizations.md | 124 ++++++++++ docs/reference/api/schemas.md | 202 ++++++++++++++-- docs/reference/api/templates.md | 225 ++++++++++++------ docs/reference/api/workspaces.md | 42 ++++ docs/reference/cli/index.md | 2 +- docs/reference/cli/provisioner.md | 22 -- docs/reference/cli/provisioner_keys.md | 23 -- docs/reference/cli/provisioners.md | 24 ++ docs/reference/cli/provisioners_jobs.md | 22 ++ .../reference/cli/provisioners_jobs_cancel.md | 22 ++ docs/reference/cli/provisioners_jobs_list.md | 62 +++++ docs/reference/cli/provisioners_keys.md | 23 ++ ..._create.md => provisioners_keys_create.md} | 4 +- ..._delete.md => provisioners_keys_delete.md} | 4 +- ...keys_list.md => provisioners_keys_list.md} | 4 +- docs/reference/cli/provisioners_list.md | 44 ++++ ...isioner_start.md => provisioners_start.md} | 4 +- enterprise/cli/provisionerdaemons.go | 21 +- enterprise/cli/provisionerdaemonstart_slim.go | 2 +- enterprise/cli/provisionerdaemonstart_test.go | 8 +- enterprise/coderd/coderd.go | 5 +- site/src/api/typesGenerated.ts | 39 +++ site/src/testHelpers/entities.ts | 5 + 50 files changed, 2447 insertions(+), 210 deletions(-) create mode 100644 cli/provisionerjobs.go create mode 100644 cli/provisioners.go create mode 100644 coderd/database/migrations/000279_provisioner_daemon_status.down.sql create mode 100644 coderd/database/migrations/000279_provisioner_daemon_status.up.sql create mode 100644 coderd/provisionerdaemons.go create mode 100644 coderd/provisionerdaemons_test.go delete mode 100644 docs/reference/cli/provisioner.md delete mode 100644 docs/reference/cli/provisioner_keys.md create mode 100644 docs/reference/cli/provisioners.md create mode 100644 docs/reference/cli/provisioners_jobs.md create mode 100644 docs/reference/cli/provisioners_jobs_cancel.md create mode 100644 docs/reference/cli/provisioners_jobs_list.md create mode 100644 docs/reference/cli/provisioners_keys.md rename docs/reference/cli/{provisioner_keys_create.md => provisioners_keys_create.md} (90%) rename docs/reference/cli/{provisioner_keys_delete.md => provisioners_keys_delete.md} (87%) rename docs/reference/cli/{provisioner_keys_list.md => provisioners_keys_list.md} (86%) create mode 100644 docs/reference/cli/provisioners_list.md rename docs/reference/cli/{provisioner_start.md => provisioners_start.md} (98%) diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go new file mode 100644 index 0000000000000..161c43cb99c01 --- /dev/null +++ b/cli/provisionerjobs.go @@ -0,0 +1,125 @@ +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) 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"` + OrganizaitonName 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 = 50 + ) + + 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: convertSlice([]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, + OrganizaitonName: 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, convertSlice([]string{}, codersdk.ProvisionerJobStatusEnums())...), + }, + { + Flag: "limit", + FlagShorthand: "l", + Env: "CODER_PROVISIONER_JOB_LIST_LIMIT", + Description: "Limit the number of jobs returned.", + Value: serpent.Int64Of(&limit), + }, + }...) + + orgContext.AttachOptions(cmd) + formatter.AttachOptions(&cmd.Options) + + return cmd +} + +func convertSlice[D, S ~string](dstType []D, src []S) []D { + for _, item := range src { + dstType = append(dstType, D(item)) + } + return dstType +} diff --git a/cli/provisioners.go b/cli/provisioners.go new file mode 100644 index 0000000000000..863929c60a4fc --- /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: "provisioners", + Short: "View and manage provisioner daemons and jobs", + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + Aliases: []string{"provisioner"}, + 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" /*"type",*/, "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/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/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 791b3c6f145e8..5eb28ead67e2f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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,117 @@ 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" + }, + "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 +12565,13 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -12423,12 +12603,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 +12681,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..d6406c5ef7a83 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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,109 @@ } } }, + "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" + }, + "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 +11344,13 @@ "type": "string", "format": "uuid" }, + "input": { + "$ref": "#/definitions/codersdk.ProvisionerJobInput" + }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "queue_position": { "type": "integer" }, @@ -11214,12 +11382,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 +11454,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/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7b19790a6d8ea..ae8df415d51ad 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3749,6 +3749,112 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg return daemons, nil } +func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + /* + -- 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, + -- 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 + 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)); + */ + 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 + 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 { + if !job.CompletedAt.Valid { + status = database.ProvisionerDaemonStatusIdle + } else { + status = database.ProvisionerDaemonStatusBusy + } + break + } + } + } + + var currentJob, previousJob database.ProvisionerJob + for _, job := range q.provisionerJobs { + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID { + if currentJob.ID == uuid.Nil || !job.CompletedAt.Valid { + currentJob = job + } + if job.CompletedAt.Valid && job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { + previousJob = job + } + } + } + + rows = append(rows, database.GetProvisionerDaemonsWithStatusByOrganizationRow{ + ProvisionerDaemon: daemon, + Status: status, + 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}, + }) + } + + return rows, nil +} + func (q *FakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3893,6 +3999,109 @@ func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(_ context.Context return jobs, nil } +func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(_ context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + /* + -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many + WITH unstarted_jobs AS ( + SELECT + id, created_at + FROM + provisioner_jobs + WHERE + started_at IS NULL + ), + queue_position AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position + FROM + unstarted_jobs + ), + queue_size AS ( + SELECT COUNT(*) AS count FROM unstarted_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; + */ + var rows []database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow + var queuedJobs []database.ProvisionerJob + for _, job := range q.provisionerJobs { + if arg.OrganizationID.Valid && job.OrganizationID != arg.OrganizationID.UUID { + continue + } + if len(arg.Status) > 0 && !slices.Contains(arg.Status, job.JobStatus) { + continue + } + + if job.JobStatus == database.ProvisionerJobStatusPending { + queuedJobs = append(queuedJobs, job) + } + row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{ + ProvisionerJob: job, + } + rows = append(rows, row) + } + slices.SortFunc(queuedJobs, func(a, b database.ProvisionerJob) int { + return a.CreatedAt.Compare(b.CreatedAt) + }) + for i := range rows { + queuePosition := slices.IndexFunc(queuedJobs, func(job database.ProvisionerJob) bool { + return job.ID == rows[i].ProvisionerJob.ID + }) + if queuePosition >= 0 { + rows[i].QueuePosition = int64(queuePosition + 1) + rows[i].QueueSize = int64(len(queuedJobs)) + + for _, daemon := range q.provisionerDaemons { + job := rows[i].ProvisionerJob + if daemon.OrganizationID == job.OrganizationID && + slices.Contains(daemon.Provisioners, job.Provisioner) && + tagsSubset(job.Tags, daemon.Tags) { + rows[i].AvailableWorkers = append(rows[i].AvailableWorkers, daemon.ID) + } + } + } + } + + return rows, nil +} + func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after time.Time) ([]database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() 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/000279_provisioner_daemon_status.down.sql b/coderd/database/migrations/000279_provisioner_daemon_status.down.sql new file mode 100644 index 0000000000000..f4fd46d4a0658 --- /dev/null +++ b/coderd/database/migrations/000279_provisioner_daemon_status.down.sql @@ -0,0 +1 @@ +DROP TYPE provisioner_daemon_status; diff --git a/coderd/database/migrations/000279_provisioner_daemon_status.up.sql b/coderd/database/migrations/000279_provisioner_daemon_status.up.sql new file mode 100644 index 0000000000000..990113d4f7af0 --- /dev/null +++ b/coderd/database/migrations/000279_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..465ccf97d3516 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5404,6 +5404,111 @@ 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, + -- 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 +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)) +` + +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"` + 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.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 +6059,122 @@ 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, + 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 + ($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..bb21442d2d8b6 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -28,6 +28,49 @@ 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, + -- 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 +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)); + -- 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..9d9fb9628d441 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -87,6 +87,62 @@ 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, + 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; + -- 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..463336a231e05 --- /dev/null +++ b/coderd/provisionerdaemons.go @@ -0,0 +1,78 @@ +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, + 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..34199e560ccdd 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -19,12 +19,71 @@ 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/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: convertSlice([]database.ProvisionerJobStatus(nil), 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 + })) +} + +func convertSlice[D, S ~string](dstType []D, src []S) []D { + for _, item := range src { + dstType = append(dstType, D(item)) + } + return dstType +} + // Returns provisioner logs based on query parameters. // The intended usage for a client to stream all logs (with JS API): // GET /logs @@ -236,14 +295,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 +321,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/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..adac26c36b3a4 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -39,17 +39,39 @@ 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"` + 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 +113,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 +155,23 @@ func JobIsMissingParameterErrorCode(code JobErrorCode) bool { // ProvisionerJob describes the job executed by the provisioning daemon. type ProvisionerJob struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - StartedAt *time.Time `json:"started_at,omitempty" format:"date-time"` - CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time"` - CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time"` - Error string `json:"error,omitempty"` - ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES"` - Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed"` - WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid"` - FileID uuid.UUID `json:"file_id" format:"uuid"` - Tags map[string]string `json:"tags"` - QueuePosition int `json:"queue_position"` - QueueSize int `json:"queue_size"` + ID uuid.UUID `json:"id" format:"uuid" table:"id"` + CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at,default_sort"` + StartedAt *time.Time `json:"started_at,omitempty" format:"date-time" table:"started at"` + CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time" table:"completed at"` + CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time" table:"canceled at"` + Error string `json:"error,omitempty" table:"error"` + ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES" table:"error code"` + Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` + WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid" table:"worker id"` + FileID uuid.UUID `json:"file_id" format:"uuid" table:"file id"` + Tags map[string]string `json:"tags" table:"tags"` + QueuePosition int `json:"queue_position" table:"queue position"` + QueueSize int `json:"queue_size" table:"queue size"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"` + Input ProvisionerJobInput `json:"input" table:"input,recursive_inline"` + Type ProvisionerJobType `json:"type" table:"type"` + AvailableWorkers []uuid.UUID `json:"available_workers,omitempty" format:"uuid" table:"available workers"` } // ProvisionerJobLog represents the provisioner log entry annotated with source and level. diff --git a/docs/manifest.json b/docs/manifest.json index fcac552ee0496..681e687ecbb63 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1139,34 +1139,54 @@ "path": "reference/cli/port-forward.md" }, { - "title": "provisioner", - "description": "Manage provisioner daemons", - "path": "reference/cli/provisioner.md" + "title": "provisioners", + "description": "View and manage provisioner daemons and jobs", + "path": "reference/cli/provisioners.md" }, { - "title": "provisioner keys", + "title": "provisioners jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioners_jobs.md" + }, + { + "title": "provisioners jobs cancel", + "description": "Cancel a provisioner job", + "path": "reference/cli/provisioners_jobs_cancel.md" + }, + { + "title": "provisioners jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioners_jobs_list.md" + }, + { + "title": "provisioners keys", "description": "Manage provisioner keys", - "path": "reference/cli/provisioner_keys.md" + "path": "reference/cli/provisioners_keys.md" }, { - "title": "provisioner keys create", + "title": "provisioners keys create", "description": "Create a new provisioner key", - "path": "reference/cli/provisioner_keys_create.md" + "path": "reference/cli/provisioners_keys_create.md" }, { - "title": "provisioner keys delete", + "title": "provisioners keys delete", "description": "Delete a provisioner key", - "path": "reference/cli/provisioner_keys_delete.md" + "path": "reference/cli/provisioners_keys_delete.md" }, { - "title": "provisioner keys list", + "title": "provisioners keys list", "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioner_keys_list.md" + "path": "reference/cli/provisioners_keys_list.md" + }, + { + "title": "provisioners list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioners_list.md" }, { - "title": "provisioner start", + "title": "provisioners start", "description": "Run a provisioner daemon", - "path": "reference/cli/provisioner_start.md" + "path": "reference/cli/provisioners_start.md" }, { "title": "publickey", 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/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..95b6eae1f773b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4189,10 +4189,110 @@ 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", + "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 | | | +| `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 +4300,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 +4313,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 +4353,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 +4423,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 +5715,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 +5723,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 +5736,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 +6734,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 +6742,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 +6755,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 +7446,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 +7454,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 +7467,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 +8085,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 +8093,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 +8106,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..d7b24de95d309 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 | +| [provisioners](./provisioners.md) | View and manage provisioner daemons and jobs | ## Options diff --git a/docs/reference/cli/provisioner.md b/docs/reference/cli/provisioner.md deleted file mode 100644 index 54cc28a84bea4..0000000000000 --- a/docs/reference/cli/provisioner.md +++ /dev/null @@ -1,22 +0,0 @@ - - -# provisioner - -Manage provisioner daemons - -Aliases: - -- provisioners - -## Usage - -```console -coder provisioner -``` - -## Subcommands - -| Name | Purpose | -| -------------------------------------------- | ------------------------ | -| [start](./provisioner_start.md) | Run a provisioner daemon | -| [keys](./provisioner_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioner_keys.md b/docs/reference/cli/provisioner_keys.md deleted file mode 100644 index 014af6f117c3a..0000000000000 --- a/docs/reference/cli/provisioner_keys.md +++ /dev/null @@ -1,23 +0,0 @@ - - -# provisioner keys - -Manage provisioner keys - -Aliases: - -- key - -## Usage - -```console -coder provisioner keys -``` - -## Subcommands - -| Name | Purpose | -| --------------------------------------------------- | ---------------------------------------- | -| [create](./provisioner_keys_create.md) | Create a new provisioner key | -| [list](./provisioner_keys_list.md) | List provisioner keys in an organization | -| [delete](./provisioner_keys_delete.md) | Delete a provisioner key | diff --git a/docs/reference/cli/provisioners.md b/docs/reference/cli/provisioners.md new file mode 100644 index 0000000000000..e5d80c4f217ab --- /dev/null +++ b/docs/reference/cli/provisioners.md @@ -0,0 +1,24 @@ + + +# provisioners + +View and manage provisioner daemons and jobs + +Aliases: + +- provisioner + +## Usage + +```console +coder provisioners +``` + +## Subcommands + +| Name | Purpose | +| --------------------------------------------- | ------------------------------------------- | +| [list](./provisioners_list.md) | List provisioner daemons in an organization | +| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs | +| [start](./provisioners_start.md) | Run a provisioner daemon | +| [keys](./provisioners_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioners_jobs.md b/docs/reference/cli/provisioners_jobs.md new file mode 100644 index 0000000000000..18f5545b1c236 --- /dev/null +++ b/docs/reference/cli/provisioners_jobs.md @@ -0,0 +1,22 @@ + + +# provisioners jobs + +View and manage provisioner jobs + +Aliases: + +- job + +## Usage + +```console +coder provisioners jobs +``` + +## Subcommands + +| Name | Purpose | +| ---------------------------------------------------- | ------------------------ | +| [list](./provisioners_jobs_list.md) | List provisioner jobs | +| [cancel](./provisioners_jobs_cancel.md) | Cancel a provisioner job | diff --git a/docs/reference/cli/provisioners_jobs_cancel.md b/docs/reference/cli/provisioners_jobs_cancel.md new file mode 100644 index 0000000000000..8152097aa84c4 --- /dev/null +++ b/docs/reference/cli/provisioners_jobs_cancel.md @@ -0,0 +1,22 @@ + + +# provisioners jobs cancel + +Cancel a provisioner job + +## Usage + +```console +coder provisioners jobs cancel [flags] +``` + +## Options + +### -O, --org + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. diff --git a/docs/reference/cli/provisioners_jobs_list.md b/docs/reference/cli/provisioners_jobs_list.md new file mode 100644 index 0000000000000..11b2e20d1d467 --- /dev/null +++ b/docs/reference/cli/provisioners_jobs_list.md @@ -0,0 +1,62 @@ + + +# provisioners jobs list + +List provisioner jobs + +Aliases: + +- ls + +## Usage + +```console +coder provisioners 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 | + +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/provisioners_keys.md b/docs/reference/cli/provisioners_keys.md new file mode 100644 index 0000000000000..2a8418550e39f --- /dev/null +++ b/docs/reference/cli/provisioners_keys.md @@ -0,0 +1,23 @@ + + +# provisioners keys + +Manage provisioner keys + +Aliases: + +- key + +## Usage + +```console +coder provisioners keys +``` + +## Subcommands + +| Name | Purpose | +| ---------------------------------------------------- | ---------------------------------------- | +| [create](./provisioners_keys_create.md) | Create a new provisioner key | +| [list](./provisioners_keys_list.md) | List provisioner keys in an organization | +| [delete](./provisioners_keys_delete.md) | Delete a provisioner key | diff --git a/docs/reference/cli/provisioner_keys_create.md b/docs/reference/cli/provisioners_keys_create.md similarity index 90% rename from docs/reference/cli/provisioner_keys_create.md rename to docs/reference/cli/provisioners_keys_create.md index da6479d15bfc9..4367dfd680d54 100644 --- a/docs/reference/cli/provisioner_keys_create.md +++ b/docs/reference/cli/provisioners_keys_create.md @@ -1,13 +1,13 @@ -# provisioner keys create +# provisioners keys create Create a new provisioner key ## Usage ```console -coder provisioner keys create [flags] +coder provisioners keys create [flags] ``` ## Options diff --git a/docs/reference/cli/provisioner_keys_delete.md b/docs/reference/cli/provisioners_keys_delete.md similarity index 87% rename from docs/reference/cli/provisioner_keys_delete.md rename to docs/reference/cli/provisioners_keys_delete.md index 56e32e57d048b..3d7b6273612c2 100644 --- a/docs/reference/cli/provisioner_keys_delete.md +++ b/docs/reference/cli/provisioners_keys_delete.md @@ -1,6 +1,6 @@ -# provisioner keys delete +# provisioners keys delete Delete a provisioner key @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioner keys delete [flags] +coder provisioners keys delete [flags] ``` ## Options diff --git a/docs/reference/cli/provisioner_keys_list.md b/docs/reference/cli/provisioners_keys_list.md similarity index 86% rename from docs/reference/cli/provisioner_keys_list.md rename to docs/reference/cli/provisioners_keys_list.md index 366db05fa490f..32ab751274cc4 100644 --- a/docs/reference/cli/provisioner_keys_list.md +++ b/docs/reference/cli/provisioners_keys_list.md @@ -1,6 +1,6 @@ -# provisioner keys list +# provisioners keys list List provisioner keys in an organization @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioner keys list [flags] +coder provisioners keys list [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners_list.md b/docs/reference/cli/provisioners_list.md new file mode 100644 index 0000000000000..1750bd3ca3616 --- /dev/null +++ b/docs/reference/cli/provisioners_list.md @@ -0,0 +1,44 @@ + + +# provisioners list + +List provisioner daemons in an organization + +Aliases: + +- ls + +## Usage + +```console +coder provisioners 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\|status\|current job id\|current job status\|previous job id\|previous job status\|organization] | +| Default | name,organization,status,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/docs/reference/cli/provisioner_start.md b/docs/reference/cli/provisioners_start.md similarity index 98% rename from docs/reference/cli/provisioner_start.md rename to docs/reference/cli/provisioners_start.md index 65254d18c0149..df97454f81f12 100644 --- a/docs/reference/cli/provisioner_start.md +++ b/docs/reference/cli/provisioners_start.md @@ -1,13 +1,13 @@ -# provisioner start +# provisioners start Run a provisioner daemon ## Usage ```console -coder provisioner start [flags] +coder provisioners start [flags] ``` ## Options 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/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/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5d840d763af78..4cd0e17b65882 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,30 @@ 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 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 +1401,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 +1428,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 = { From 449df5da84ad38747b25da41e6ced6cb8f6cca4c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 19:28:29 +0200 Subject: [PATCH 02/21] wip --- cli/provisionerjobs.go | 3 +- cli/testdata/coder_--help.golden | 1 + cli/testdata/coder_list_--output_json.golden | 7 ++- cli/testdata/coder_provisioners_--help.golden | 15 +++++ .../coder_provisioners_jobs_--help.golden | 14 +++++ ...coder_provisioners_jobs_list_--help.golden | 27 +++++++++ .../coder_provisioners_list_--help.golden | 21 +++++++ coderd/database/queries.sql.go | 5 ++ .../database/queries/provisionerdaemons.sql | 3 + docs/manifest.json | 5 -- docs/reference/cli/provisioners_jobs.md | 7 +-- .../reference/cli/provisioners_jobs_cancel.md | 22 ------- docs/reference/cli/provisioners_jobs_list.md | 1 + docs/reference/cli/provisioners_keys_list.md | 18 ++++++ enterprise/cli/provisionerkeys.go | 2 +- enterprise/cli/testdata/coder_--help.golden | 2 +- .../testdata/coder_provisioners_--help.golden | 17 ++++++ .../coder_provisioners_jobs_--help.golden | 14 +++++ ...coder_provisioners_jobs_list_--help.golden | 27 +++++++++ .../coder_provisioners_keys_--help.golden | 16 +++++ ...der_provisioners_keys_create_--help.golden | 16 +++++ ...der_provisioners_keys_delete_--help.golden | 18 ++++++ ...coder_provisioners_keys_list_--help.golden | 15 +++++ .../coder_provisioners_list_--help.golden | 21 +++++++ .../coder_provisioners_start_--help.golden | 58 +++++++++++++++++++ 25 files changed, 320 insertions(+), 35 deletions(-) create mode 100644 cli/testdata/coder_provisioners_--help.golden create mode 100644 cli/testdata/coder_provisioners_jobs_--help.golden create mode 100644 cli/testdata/coder_provisioners_jobs_list_--help.golden create mode 100644 cli/testdata/coder_provisioners_list_--help.golden delete mode 100644 docs/reference/cli/provisioners_jobs_cancel.md create mode 100644 enterprise/cli/testdata/coder_provisioners_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_jobs_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_keys_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_list_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioners_start_--help.golden diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 161c43cb99c01..07f23499c8336 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -40,7 +40,7 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { cliui.JSONFormat(), ) status []string - limit int64 = 50 + limit int64 ) cmd := &serpent.Command{ @@ -107,6 +107,7 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { FlagShorthand: "l", Env: "CODER_PROVISIONER_JOB_LIST_LIMIT", Description: "Limit the number of jobs returned.", + Default: "50", Value: serpent.Int64Of(&limit), }, }...) diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index c25301169002e..513aacbc3e6dc 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". + provisioners 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..5520436754ede 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_provisioners_--help.golden b/cli/testdata/coder_provisioners_--help.golden new file mode 100644 index 0000000000000..bf8d220e1d063 --- /dev/null +++ b/cli/testdata/coder_provisioners_--help.golden @@ -0,0 +1,15 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners + + View and manage provisioner daemons and jobs + + Aliases: provisioner + +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_provisioners_jobs_--help.golden b/cli/testdata/coder_provisioners_jobs_--help.golden new file mode 100644 index 0000000000000..1665bb97b81ae --- /dev/null +++ b/cli/testdata/coder_provisioners_jobs_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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_provisioners_jobs_list_--help.golden b/cli/testdata/coder_provisioners_jobs_list_--help.golden new file mode 100644 index 0000000000000..e46e7e3914da0 --- /dev/null +++ b/cli/testdata/coder_provisioners_jobs_list_--help.golden @@ -0,0 +1,27 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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_provisioners_list_--help.golden b/cli/testdata/coder_provisioners_list_--help.golden new file mode 100644 index 0000000000000..58f749be0a8c1 --- /dev/null +++ b/cli/testdata/coder_provisioners_list_--help.golden @@ -0,0 +1,21 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,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/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 465ccf97d3516..9567faa22e264 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5415,6 +5415,7 @@ SELECT ELSE 'idle' END END::provisioner_daemon_status AS status, + pk.name AS type, -- 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, @@ -5422,6 +5423,8 @@ SELECT 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 @@ -5458,6 +5461,7 @@ type GetProvisionerDaemonsWithStatusByOrganizationParams struct { type GetProvisionerDaemonsWithStatusByOrganizationRow struct { ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` Status ProvisionerDaemonStatus `db:"status" json:"status"` + Type string `db:"type" json:"type"` 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"` @@ -5491,6 +5495,7 @@ func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.C &i.ProvisionerDaemon.OrganizationID, &i.ProvisionerDaemon.KeyID, &i.Status, + &i.Type, &i.CurrentJobID, &i.CurrentJobStatus, &i.PreviousJobID, diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index bb21442d2d8b6..1c27f56ddef18 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -39,6 +39,7 @@ SELECT ELSE 'idle' END END::provisioner_daemon_status AS status, + pk.name AS type, -- 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, @@ -46,6 +47,8 @@ SELECT 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 diff --git a/docs/manifest.json b/docs/manifest.json index 681e687ecbb63..8905b519ff193 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1148,11 +1148,6 @@ "description": "View and manage provisioner jobs", "path": "reference/cli/provisioners_jobs.md" }, - { - "title": "provisioners jobs cancel", - "description": "Cancel a provisioner job", - "path": "reference/cli/provisioners_jobs_cancel.md" - }, { "title": "provisioners jobs list", "description": "List provisioner jobs", diff --git a/docs/reference/cli/provisioners_jobs.md b/docs/reference/cli/provisioners_jobs.md index 18f5545b1c236..c4a9112da7c08 100644 --- a/docs/reference/cli/provisioners_jobs.md +++ b/docs/reference/cli/provisioners_jobs.md @@ -16,7 +16,6 @@ coder provisioners jobs ## Subcommands -| Name | Purpose | -| ---------------------------------------------------- | ------------------------ | -| [list](./provisioners_jobs_list.md) | List provisioner jobs | -| [cancel](./provisioners_jobs_cancel.md) | Cancel a provisioner job | +| Name | Purpose | +| ------------------------------------------------ | --------------------- | +| [list](./provisioners_jobs_list.md) | List provisioner jobs | diff --git a/docs/reference/cli/provisioners_jobs_cancel.md b/docs/reference/cli/provisioners_jobs_cancel.md deleted file mode 100644 index 8152097aa84c4..0000000000000 --- a/docs/reference/cli/provisioners_jobs_cancel.md +++ /dev/null @@ -1,22 +0,0 @@ - - -# provisioners jobs cancel - -Cancel a provisioner job - -## Usage - -```console -coder provisioners jobs cancel [flags] -``` - -## Options - -### -O, --org - -| | | -| ----------- | -------------------------------- | -| Type | string | -| Environment | $CODER_ORGANIZATION | - -Select which organization (uuid or name) to use. diff --git a/docs/reference/cli/provisioners_jobs_list.md b/docs/reference/cli/provisioners_jobs_list.md index 11b2e20d1d467..c0952566b5723 100644 --- a/docs/reference/cli/provisioners_jobs_list.md +++ b/docs/reference/cli/provisioners_jobs_list.md @@ -31,6 +31,7 @@ Filter by job status. | ----------- | ---------------------------------------------- | | Type | int | | Environment | $CODER_PROVISIONER_JOB_LIST_LIMIT | +| Default | 50 | Limit the number of jobs returned. diff --git a/docs/reference/cli/provisioners_keys_list.md b/docs/reference/cli/provisioners_keys_list.md index 32ab751274cc4..99ff1f21cbeb7 100644 --- a/docs/reference/cli/provisioners_keys_list.md +++ b/docs/reference/cli/provisioners_keys_list.md @@ -24,3 +24,21 @@ coder provisioners 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/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..81eaad567c190 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 + provisioners View and manage provisioner daemons and jobs server Start a Coder server GLOBAL OPTIONS: diff --git a/enterprise/cli/testdata/coder_provisioners_--help.golden b/enterprise/cli/testdata/coder_provisioners_--help.golden new file mode 100644 index 0000000000000..dca4ddad6b0a2 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_--help.golden @@ -0,0 +1,17 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners + + View and manage provisioner daemons and jobs + + Aliases: provisioner + +SUBCOMMANDS: + jobs View and manage provisioner jobs + keys Manage provisioner keys + list List provisioner daemons in an organization + start Run a provisioner daemon + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_jobs_--help.golden b/enterprise/cli/testdata/coder_provisioners_jobs_--help.golden new file mode 100644 index 0000000000000..1665bb97b81ae --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_jobs_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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_provisioners_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden new file mode 100644 index 0000000000000..e46e7e3914da0 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden @@ -0,0 +1,27 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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_provisioners_keys_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_--help.golden new file mode 100644 index 0000000000000..913c0f6650058 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_keys_--help.golden @@ -0,0 +1,16 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners keys + + Manage provisioner keys + + Aliases: key + +SUBCOMMANDS: + create Create a new provisioner key + delete Delete a provisioner key + list List provisioner keys in an organization + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden new file mode 100644 index 0000000000000..eb20ba516b99b --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden @@ -0,0 +1,16 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners keys create [flags] + + Create a new provisioner key + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -t, --tag string-array, $CODER_PROVISIONERD_TAGS + Tags to filter provisioner jobs by. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden new file mode 100644 index 0000000000000..5d2a7a4859382 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden @@ -0,0 +1,18 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners keys delete [flags] + + Delete a provisioner key + + Aliases: rm + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -y, --yes bool + Bypass prompts. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden new file mode 100644 index 0000000000000..4459d368ed25e --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden @@ -0,0 +1,15 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners keys list [flags] + + List provisioner keys in an organization + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_list_--help.golden new file mode 100644 index 0000000000000..58f749be0a8c1 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_list_--help.golden @@ -0,0 +1,21 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners 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|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,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/cli/testdata/coder_provisioners_start_--help.golden b/enterprise/cli/testdata/coder_provisioners_start_--help.golden new file mode 100644 index 0000000000000..12c4f5cc1a856 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioners_start_--help.golden @@ -0,0 +1,58 @@ +coder v0.0.0-devel + +USAGE: + coder provisioners start [flags] + + Run a provisioner daemon + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -c, --cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir]) + Directory to store cached data. + + --key string, $CODER_PROVISIONER_DAEMON_KEY + Provisioner key to authenticate with Coder server. + + --log-filter string-array, $CODER_PROVISIONER_DAEMON_LOG_FILTER + Filter debug logs by matching against a given regex. Use .* to match + all debug logs. + + --log-human string, $CODER_PROVISIONER_DAEMON_LOGGING_HUMAN (default: /dev/stderr) + Output human-readable logs to a given file. + + --log-json string, $CODER_PROVISIONER_DAEMON_LOGGING_JSON + Output JSON logs to a given file. + + --log-stackdriver string, $CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER + Output Stackdriver compatible logs to a given file. + + --name string, $CODER_PROVISIONER_DAEMON_NAME + Name of this provisioner daemon. Defaults to the current hostname + without FQDN. + + --poll-interval duration, $CODER_PROVISIONERD_POLL_INTERVAL (default: 1s) + Deprecated and ignored. + + --poll-jitter duration, $CODER_PROVISIONERD_POLL_JITTER (default: 100ms) + Deprecated and ignored. + + --prometheus-address string, $CODER_PROMETHEUS_ADDRESS (default: 127.0.0.1:2112) + The bind address to serve prometheus metrics. + + --prometheus-enable bool, $CODER_PROMETHEUS_ENABLE (default: false) + Serve prometheus metrics on the address defined by prometheus address. + + --psk string, $CODER_PROVISIONER_DAEMON_PSK + Pre-shared key to authenticate with Coder server. + DEPRECATED: Use --key instead. + + -t, --tag string-array, $CODER_PROVISIONERD_TAGS + Tags to filter provisioner jobs by. + + --verbose bool, $CODER_PROVISIONER_DAEMON_VERBOSE (default: false) + Output debug-level logs. + +——— +Run `coder --help` for a list of global options. From fa261b19d9e8d5013d53d669b5c410337eb67948 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 19:52:36 +0200 Subject: [PATCH 03/21] key --- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/database/queries.sql.go | 6 +++--- coderd/database/queries/provisionerdaemons.sql | 2 +- coderd/provisionerdaemons.go | 1 + codersdk/provisionerdaemons.go | 1 + docs/reference/api/schemas.md | 2 ++ docs/reference/cli/provisioners_list.md | 8 ++++---- site/src/api/typesGenerated.ts | 1 + 9 files changed, 19 insertions(+), 8 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 5eb28ead67e2f..f91bb0863d19b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12479,6 +12479,9 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "key_name": { + "type": "string" + }, "last_seen_at": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d6406c5ef7a83..0b9e64d5e471a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11264,6 +11264,9 @@ "type": "string", "format": "uuid" }, + "key_name": { + "type": "string" + }, "last_seen_at": { "type": "string", "format": "date-time" diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9567faa22e264..257afe71b722c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5415,7 +5415,7 @@ SELECT ELSE 'idle' END END::provisioner_daemon_status AS status, - pk.name AS type, + 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, @@ -5461,7 +5461,7 @@ type GetProvisionerDaemonsWithStatusByOrganizationParams struct { type GetProvisionerDaemonsWithStatusByOrganizationRow struct { ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"` Status ProvisionerDaemonStatus `db:"status" json:"status"` - Type string `db:"type" json:"type"` + 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"` @@ -5495,7 +5495,7 @@ func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.C &i.ProvisionerDaemon.OrganizationID, &i.ProvisionerDaemon.KeyID, &i.Status, - &i.Type, + &i.KeyName, &i.CurrentJobID, &i.CurrentJobStatus, &i.PreviousJobID, diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index 1c27f56ddef18..f6b291448471e 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -39,7 +39,7 @@ SELECT ELSE 'idle' END END::provisioner_daemon_status AS status, - pk.name AS type, + 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, diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 463336a231e05..879ee651bc107 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -70,6 +70,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { } return codersdk.ProvisionerDaemonWithStatus{ ProvisionerDaemon: pd, + KeyName: dbDaemon.KeyName, Status: codersdk.ProvisionerDaemonStatus(dbDaemon.Status), CurrentJob: currentJob, PreviousJob: previousJob, diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index adac26c36b3a4..0e786570b0a76 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -64,6 +64,7 @@ type ProvisionerDaemon struct { 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"` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 95b6eae1f773b..2e912b52eb778 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4244,6 +4244,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "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", @@ -4270,6 +4271,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `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 | | | diff --git a/docs/reference/cli/provisioners_list.md b/docs/reference/cli/provisioners_list.md index 1750bd3ca3616..cd9fe3b7cb06e 100644 --- a/docs/reference/cli/provisioners_list.md +++ b/docs/reference/cli/provisioners_list.md @@ -27,10 +27,10 @@ Select which organization (uuid or name) to use. ### -c, --column -| | | -| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Type | [id\|organization id\|created at\|last seen at\|name\|version\|api version\|tags\|status\|current job id\|current job status\|previous job id\|previous job status\|organization] | -| Default | name,organization,status,created at,last seen at,version,tags | +| | | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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,created at,last seen at,version,tags | Columns to display in table output. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4cd0e17b65882..565d6488f3b33 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1370,6 +1370,7 @@ export const ProvisionerDaemonStatuses: ProvisionerDaemonStatus[] = ["busy", "id // 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 c5a042996e62dd9e5e940609f6e19b6ef4e4b31b Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 20:01:36 +0200 Subject: [PATCH 04/21] golden --- cli/root_test.go | 16 +++++++ .../coder_provisioners_jobs_list.golden | 3 ++ ...rovisioners_jobs_list_--output_json.golden | 44 +++++++++++++++++++ cli/testdata/coder_provisioners_list.golden | 2 + .../coder_provisioners_list_--help.golden | 2 +- ...der_provisioners_list_--output_json.golden | 30 +++++++++++++ ...coder_provisioners_keys_list_--help.golden | 6 +++ .../coder_provisioners_list_--help.golden | 2 +- 8 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 cli/testdata/coder_provisioners_jobs_list.golden create mode 100644 cli/testdata/coder_provisioners_jobs_list_--output_json.golden create mode 100644 cli/testdata/coder_provisioners_list.golden create mode 100644 cli/testdata/coder_provisioners_list_--output_json.golden diff --git a/cli/root_test.go b/cli/root_test.go index 897aea18fec3e..9fa95c6de8f73 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 provisioners list", + Cmd: []string{"provisioners", "list"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioners list --output json", + Cmd: []string{"provisioners", "list", "--output", "json"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioners jobs list", + Cmd: []string{"provisioners", "jobs", "list"}, + }, + clitest.CommandHelpCase{ + Name: "coder provisioners jobs list --output json", + Cmd: []string{"provisioners", "jobs", "list", "--output", "json"}, + }, )) } diff --git a/cli/testdata/coder_provisioners_jobs_list.golden b/cli/testdata/coder_provisioners_jobs_list.golden new file mode 100644 index 0000000000000..c54d6f063d083 --- /dev/null +++ b/cli/testdata/coder_provisioners_jobs_list.golden @@ -0,0 +1,3 @@ +ID CREATED AT STATUS TAGS TYPE ORGANIZATION QUEUE +[version job ID] [timestamp] succeeded map[owner: scope:organization] template_version_import Coder +[workspace build job ID] [timestamp] succeeded map[owner: scope:organization] workspace_build Coder diff --git a/cli/testdata/coder_provisioners_jobs_list_--output_json.golden b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden new file mode 100644 index 0000000000000..b8479a7eb737a --- /dev/null +++ b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden @@ -0,0 +1,44 @@ +[ + { + "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" + }, + { + "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" + } +] diff --git a/cli/testdata/coder_provisioners_list.golden b/cli/testdata/coder_provisioners_list.golden new file mode 100644 index 0000000000000..6cd07f945d45d --- /dev/null +++ b/cli/testdata/coder_provisioners_list.golden @@ -0,0 +1,2 @@ +CREATED AT LAST SEEN AT NAME VERSION TAGS STATUS ORGANIZATION +[timestamp] [timestamp] test v0.0.0-devel map[owner: scope:organization] offline Coder diff --git a/cli/testdata/coder_provisioners_list_--help.golden b/cli/testdata/coder_provisioners_list_--help.golden index 58f749be0a8c1..d31366bd5034e 100644 --- a/cli/testdata/coder_provisioners_list_--help.golden +++ b/cli/testdata/coder_provisioners_list_--help.golden @@ -11,7 +11,7 @@ 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|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,created at,last seen at,version,tags) + -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,created at,last seen at,version,tags) Columns to display in table output. -o, --output table|json (default: table) diff --git a/cli/testdata/coder_provisioners_list_--output_json.golden b/cli/testdata/coder_provisioners_list_--output_json.golden new file mode 100644 index 0000000000000..96d414a491a2d --- /dev/null +++ b/cli/testdata/coder_provisioners_list_--output_json.golden @@ -0,0 +1,30 @@ +[ + { + "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": "", + "status": "offline", + "current_job": { + "id": "[version job ID]", + "status": "succeeded" + }, + "previous_job": { + "id": "[workspace build job ID]", + "status": "succeeded" + }, + "organization_name": "Coder" + } +] diff --git a/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden index 4459d368ed25e..5691eb2933563 100644 --- a/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioners_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_provisioners_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_list_--help.golden index 58f749be0a8c1..d31366bd5034e 100644 --- a/enterprise/cli/testdata/coder_provisioners_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioners_list_--help.golden @@ -11,7 +11,7 @@ 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|status|current job id|current job status|previous job id|previous job status|organization] (default: name,organization,status,created at,last seen at,version,tags) + -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,created at,last seen at,version,tags) Columns to display in table output. -o, --output table|json (default: table) From b138faf948294909a1cc4631d54b42c03a0d428a Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 18:28:39 +0000 Subject: [PATCH 05/21] name --- cli/provisioners.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/provisioners.go b/cli/provisioners.go index 863929c60a4fc..c1eab83240203 100644 --- a/cli/provisioners.go +++ b/cli/provisioners.go @@ -36,7 +36,7 @@ func (r *RootCmd) provisionerList() *serpent.Command { client = new(codersdk.Client) orgContext = NewOrganizationContext() formatter = cliui.NewOutputFormatter( - cliui.TableFormat([]provisionerDaemonRow{}, []string{"name", "organization", "status" /*"type",*/, "created at", "last seen at", "version", "tags"}), + cliui.TableFormat([]provisionerDaemonRow{}, []string{"name", "organization", "status", "key name", "created at", "last seen at", "version", "tags"}), cliui.JSONFormat(), ) ) From a0af6ebf7bd79bcd436376036e7f22d49fbf96ae Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Dec 2024 19:02:50 +0000 Subject: [PATCH 06/21] gen --- cli/testdata/coder_list_--output_json.golden | 4 +-- .../coder_provisioners_jobs_list.golden | 4 +-- ...rovisioners_jobs_list_--output_json.golden | 32 +++++++++---------- cli/testdata/coder_provisioners_list.golden | 4 +-- .../coder_provisioners_list_--help.golden | 2 +- ...der_provisioners_list_--output_json.golden | 19 +++++------ docs/reference/cli/provisioners_list.md | 2 +- .../coder_provisioners_list_--help.golden | 2 +- 8 files changed, 33 insertions(+), 36 deletions(-) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 5520436754ede..0ef065dd86a81 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -44,9 +44,9 @@ }, "queue_position": 0, "queue_size": 0, - "organization_id": "[first org ID]", + "organization_id": "===========[first org ID]===========", "input": { - "workspace_build_id": "[workspace build ID]" + "workspace_build_id": "========[workspace build ID]========" }, "type": "workspace_build" }, diff --git a/cli/testdata/coder_provisioners_jobs_list.golden b/cli/testdata/coder_provisioners_jobs_list.golden index c54d6f063d083..b41f4fc531316 100644 --- a/cli/testdata/coder_provisioners_jobs_list.golden +++ b/cli/testdata/coder_provisioners_jobs_list.golden @@ -1,3 +1,3 @@ ID CREATED AT STATUS TAGS TYPE ORGANIZATION QUEUE -[version job ID] [timestamp] succeeded map[owner: scope:organization] template_version_import Coder -[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 +======[workspace build job ID]====== ====[timestamp]===== succeeded map[owner: scope:organization] workspace_build Coder diff --git a/cli/testdata/coder_provisioners_jobs_list_--output_json.golden b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden index b8479a7eb737a..a19683573bba2 100644 --- a/cli/testdata/coder_provisioners_jobs_list_--output_json.golden +++ b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden @@ -1,42 +1,42 @@ [ { - "id": "[version job ID]", - "created_at": "[timestamp]", - "started_at": "[timestamp]", - "completed_at": "[timestamp]", + "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]", + "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]", + "organization_id": "===========[first org ID]===========", "input": { - "template_version_id": "[version ID]" + "template_version_id": "============[version ID]============" }, "type": "template_version_import", "organization_name": "Coder" }, { - "id": "[workspace build job ID]", - "created_at": "[timestamp]", - "started_at": "[timestamp]", - "completed_at": "[timestamp]", + "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]", + "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]", + "organization_id": "===========[first org ID]===========", "input": { - "workspace_build_id": "[workspace build ID]" + "workspace_build_id": "========[workspace build ID]========" }, "type": "workspace_build", "organization_name": "Coder" diff --git a/cli/testdata/coder_provisioners_list.golden b/cli/testdata/coder_provisioners_list.golden index 6cd07f945d45d..056571547939e 100644 --- a/cli/testdata/coder_provisioners_list.golden +++ b/cli/testdata/coder_provisioners_list.golden @@ -1,2 +1,2 @@ -CREATED AT LAST SEEN AT NAME VERSION TAGS STATUS ORGANIZATION -[timestamp] [timestamp] test v0.0.0-devel map[owner: scope:organization] offline Coder +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_provisioners_list_--help.golden b/cli/testdata/coder_provisioners_list_--help.golden index d31366bd5034e..c8bc6c84f552e 100644 --- a/cli/testdata/coder_provisioners_list_--help.golden +++ b/cli/testdata/coder_provisioners_list_--help.golden @@ -11,7 +11,7 @@ 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,created at,last seen at,version,tags) + -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) diff --git a/cli/testdata/coder_provisioners_list_--output_json.golden b/cli/testdata/coder_provisioners_list_--output_json.golden index 96d414a491a2d..b36f24b79553c 100644 --- a/cli/testdata/coder_provisioners_list_--output_json.golden +++ b/cli/testdata/coder_provisioners_list_--output_json.golden @@ -1,10 +1,10 @@ [ { - "id": "[workspace build worker ID]", - "organization_id": "[first org ID]", + "id": "====[workspace build worker ID]=====", + "organization_id": "===========[first org ID]===========", "key_id": "00000000-0000-0000-0000-000000000001", - "created_at": "[timestamp]", - "last_seen_at": "[timestamp]", + "created_at": "====[timestamp]=====", + "last_seen_at": "====[timestamp]=====", "name": "test", "version": "v0.0.0-devel", "api_version": "1.1", @@ -15,14 +15,11 @@ "owner": "", "scope": "organization" }, - "key_name": "", - "status": "offline", - "current_job": { - "id": "[version job ID]", - "status": "succeeded" - }, + "key_name": "built-in", + "status": "idle", + "current_job": null, "previous_job": { - "id": "[workspace build job ID]", + "id": "======[workspace build job ID]======", "status": "succeeded" }, "organization_name": "Coder" diff --git a/docs/reference/cli/provisioners_list.md b/docs/reference/cli/provisioners_list.md index cd9fe3b7cb06e..1b3c8a62b74ba 100644 --- a/docs/reference/cli/provisioners_list.md +++ b/docs/reference/cli/provisioners_list.md @@ -30,7 +30,7 @@ Select which organization (uuid or name) to use. | | | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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,created at,last seen at,version,tags | +| Default | name,organization,status,key name,created at,last seen at,version,tags | Columns to display in table output. diff --git a/enterprise/cli/testdata/coder_provisioners_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_list_--help.golden index d31366bd5034e..c8bc6c84f552e 100644 --- a/enterprise/cli/testdata/coder_provisioners_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioners_list_--help.golden @@ -11,7 +11,7 @@ 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,created at,last seen at,version,tags) + -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) From 8975d4160615ee04c9314ceb6a570bae7844dcd5 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 14:17:21 +0000 Subject: [PATCH 07/21] test --- cli/provisioners_test.go | 134 +++++++++++++++++++++++++++++++++ coderd/database/dbgen/dbgen.go | 76 +++++++++++-------- 2 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 cli/provisioners_test.go diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go new file mode 100644 index 0000000000000..5623574c53646 --- /dev/null +++ b/cli/provisioners_test.go @@ -0,0 +1,134 @@ +package cli_test + +import ( + "database/sql" + "encoding/json" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/codersdk" +) + +func TestProvisioners(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Database: db, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent()) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + // Create a provisioner that's working on a job. + pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-1", + CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"), + 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-bbbb-000000000001") + job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + ID: uuid.MustParse("00000000-0000-0000-cccc-000000000001"), + WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`), + StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb1ID, + JobID: job1.ID, + WorkspaceID: w1.ID, + TemplateVersionID: version.ID, + }) + + // Create another provisioner that completed a job and is offline. + pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-2", + CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"), + 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-bbbb-000000000002") + job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + ID: uuid.MustParse("00000000-0000-0000-cccc-000000000002"), + WorkerID: uuid.NullUUID{UUID: pd2.ID, Valid: true}, + Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`), + 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}, + }) + dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ + ID: wb2ID, + JobID: job2.ID, + WorkspaceID: w2.ID, + TemplateVersionID: version.ID, + }) + + // Create a provisioner that is idle. + pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + Name: "provisioner-3", + CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"), + KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + }) + _ = pd3 + + t.Run("list", func(t *testing.T) { + t.Parallel() + + 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", + ) + clitest.SetupConfig(t, member, root) + err := inv.Run() + require.NoError(t, err) + + // TODO(mafredri): Verify golden output. + }) + + t.Run("jobs list", func(t *testing.T) { + t.Parallel() + + 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", + ) + clitest.SetupConfig(t, member, root) + err := inv.Run() + require.NoError(t, err) + + // TODO(mafredri): Verify golden output. + }) +} + +func timeParse(t *testing.T, layout, s string) time.Time { + t.Helper() + tm, err := time.Parse(layout, s) + require.NoError(t, err) + return tm +} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index d0bf6a4ce0f50..c75678cffb80d 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,17 +592,18 @@ 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. require.Equal(t, jobID, job.ID, "acquired incorrect job") + fmt.Printf("%#v\n", job) } 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 +613,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 +622,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 +1114,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 { From 96a278e1d1b89ca3aee3d6bcdb3f300be27aa44a Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 14:18:11 +0000 Subject: [PATCH 08/21] dbmem --- coderd/database/dbmem/dbmem.go | 44 ++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ae8df415d51ad..fae436ac7e011 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3815,36 +3815,50 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context. } var status database.ProvisionerDaemonStatus - if daemon.LastSeenAt.Valid || daemon.LastSeenAt.Time.Before(time.Now().Add(-time.Duration(arg.StaleIntervalMS)*time.Millisecond)) { + if !daemon.LastSeenAt.Valid || daemon.LastSeenAt.Time.Before(time.Now().Add(-time.Duration(arg.StaleIntervalMS)*time.Millisecond)) { status = database.ProvisionerDaemonStatusOffline } else { + var currentJob *database.ProvisionerJob for _, job := range q.provisionerJobs { - if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID { - if !job.CompletedAt.Valid { - status = database.ProvisionerDaemonStatusIdle - } else { - status = database.ProvisionerDaemonStatusBusy - } + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID && !job.CompletedAt.Valid { + currentJob = &job break } } + + if currentJob != nil { + status = database.ProvisionerDaemonStatusBusy + } else { + status = database.ProvisionerDaemonStatusIdle + } } var currentJob, previousJob database.ProvisionerJob for _, job := range q.provisionerJobs { - if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID { - if currentJob.ID == uuid.Nil || !job.CompletedAt.Valid { - currentJob = job - } - if job.CompletedAt.Valid && job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { - previousJob = job - } + if job.WorkerID.Valid && job.WorkerID.UUID != daemon.ID { + continue + } + + if !job.CompletedAt.Valid { + currentJob = job + } else 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}, @@ -4009,7 +4023,7 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition defer q.mutex.RUnlock() /* - -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many + -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many WITH unstarted_jobs AS ( SELECT id, created_at From e9b87257a011bdde2df78025b9e2b98d5513bec1 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 15:01:25 +0000 Subject: [PATCH 09/21] typo --- cli/provisionerjobs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 07f23499c8336..9aa5974f80779 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -28,7 +28,7 @@ func (r *RootCmd) provisionerJobs() *serpent.Command { func (r *RootCmd) provisionerJobsList() *serpent.Command { type provisionerJobRow struct { codersdk.ProvisionerJob `table:"provisioner_job,recursive_inline"` - OrganizaitonName string `json:"organization_name" table:"organization"` + OrganizationName string `json:"organization_name" table:"organization"` Queue string `json:"-" table:"queue"` } @@ -75,7 +75,7 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { for _, job := range jobs { row := provisionerJobRow{ ProvisionerJob: job, - OrganizaitonName: org.HumanName(), + OrganizationName: org.HumanName(), } if job.Status == codersdk.ProvisionerJobPending { row.Queue = fmt.Sprintf("%d/%d", job.QueuePosition, job.QueueSize) From c5211ef440eecea9485ccc392001b3610c3cf9fe Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 15:08:56 +0000 Subject: [PATCH 10/21] fix migrations --- coderd/database/dbmem/dbmem.go | 102 ++++++++++-------- ...000280_provisioner_daemon_status.down.sql} | 0 ...> 000280_provisioner_daemon_status.up.sql} | 0 3 files changed, 58 insertions(+), 44 deletions(-) rename coderd/database/migrations/{000279_provisioner_daemon_status.down.sql => 000280_provisioner_daemon_status.down.sql} (100%) rename coderd/database/migrations/{000279_provisioner_daemon_status.up.sql => 000280_provisioner_daemon_status.up.sql} (100%) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fae436ac7e011..91b2e14d2f4b8 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3758,50 +3758,6 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context. q.mutex.RLock() defer q.mutex.RUnlock() - /* - -- 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, - -- 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 - 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)); - */ var rows []database.GetProvisionerDaemonsWithStatusByOrganizationRow for _, daemon := range q.provisionerDaemons { if daemon.OrganizationID != arg.OrganizationID { @@ -4072,6 +4028,64 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition pj.created_at DESC LIMIT sqlc.narg('limit')::int; + + AMENDED QUERY: + + -- 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; */ var rows []database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow var queuedJobs []database.ProvisionerJob diff --git a/coderd/database/migrations/000279_provisioner_daemon_status.down.sql b/coderd/database/migrations/000280_provisioner_daemon_status.down.sql similarity index 100% rename from coderd/database/migrations/000279_provisioner_daemon_status.down.sql rename to coderd/database/migrations/000280_provisioner_daemon_status.down.sql diff --git a/coderd/database/migrations/000279_provisioner_daemon_status.up.sql b/coderd/database/migrations/000280_provisioner_daemon_status.up.sql similarity index 100% rename from coderd/database/migrations/000279_provisioner_daemon_status.up.sql rename to coderd/database/migrations/000280_provisioner_daemon_status.up.sql From c6e1cc63e8dfdd73ffb4df18027fcf7958d5829e Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 15:20:36 +0000 Subject: [PATCH 11/21] cleanup --- coderd/database/dbmem/dbmem.go | 52 ---------------------------------- 1 file changed, 52 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 91b2e14d2f4b8..a4083cd5def46 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3979,58 +3979,6 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition defer q.mutex.RUnlock() /* - -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many - WITH unstarted_jobs AS ( - SELECT - id, created_at - FROM - provisioner_jobs - WHERE - started_at IS NULL - ), - queue_position AS ( - SELECT - id, - ROW_NUMBER() OVER (ORDER BY created_at ASC) AS queue_position - FROM - unstarted_jobs - ), - queue_size AS ( - SELECT COUNT(*) AS count FROM unstarted_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; - - AMENDED QUERY: - -- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many WITH pending_jobs AS ( SELECT From 9e64cddeb85c8c51b12972720d264e0ffae4caa4 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 15:54:19 +0000 Subject: [PATCH 12/21] improve tests and dbmem --- cli/provisioners_test.go | 28 ++++++++++++++++- coderd/database/dbgen/dbgen.go | 1 - coderd/database/dbmem/dbmem.go | 56 ++++++++++++++++------------------ 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go index 5623574c53646..4903f4c55a379 100644 --- a/cli/provisioners_test.go +++ b/cli/provisioners_test.go @@ -22,13 +22,16 @@ func TestProvisioners(t *testing.T) { db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, + 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) + defer 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) @@ -36,6 +39,9 @@ func TestProvisioners(t *testing.T) { workspace := coderdtest.CreateWorkspace(t, client, template.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + // Stop the provisioner so it doesn't grab jobs. + firstProvisioner.Close() + // Create a provisioner that's working on a job. pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ Name: "provisioner-1", @@ -52,6 +58,7 @@ func TestProvisioners(t *testing.T) { WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true}, Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`), StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true}, + Tags: database.StringMap{"owner": "", "scope": "organization"}, }) dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ ID: wb1ID, @@ -78,6 +85,7 @@ func TestProvisioners(t *testing.T) { Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`), 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, @@ -86,6 +94,24 @@ func TestProvisioners(t *testing.T) { 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-bbbb-000000000003") + job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ + ID: uuid.MustParse("00000000-0000-0000-cccc-000000000003"), + Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`), + 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. pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ Name: "provisioner-3", diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index c75678cffb80d..d57d267e2b524 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -599,7 +599,6 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data require.NoError(t, err) // There is no easy way to make sure we acquire the correct job. require.Equal(t, jobID, job.ID, "acquired incorrect job") - fmt.Printf("%#v\n", job) } if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index a4083cd5def46..85a6fd671a687 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3876,10 +3876,17 @@ 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() + if ids == nil { + ids = []uuid.UUID{} + } + return q.getProvisionerJobsByIDsWithQueuePositionLocked(ctx, ids) +} + +func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLocked(_ context.Context, ids []uuid.UUID) ([]database.GetProvisionerJobsByIDsWithQueuePositionRow, error) { // WITH pending_jobs AS ( // SELECT // id, created_at @@ -3948,7 +3955,7 @@ func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(_ context.Context // pj.id IN (...) jobs := make([]database.GetProvisionerJobsByIDsWithQueuePositionRow, 0) for _, job := range q.provisionerJobs { - if !slices.Contains(ids, job.ID) { + if ids != nil && !slices.Contains(ids, job.ID) { continue } // clone the Tags before appending, since maps are reference types and @@ -3969,7 +3976,7 @@ func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(_ context.Context return jobs, nil } -func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(_ context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { +func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err @@ -4035,9 +4042,15 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition LIMIT sqlc.narg('limit')::int; */ + rowsWithQueuePosition, err := q.getProvisionerJobsByIDsWithQueuePositionLocked(ctx, nil) + if err != nil { + return nil, err + } + var rows []database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow - var queuedJobs []database.ProvisionerJob - for _, job := range q.provisionerJobs { + for _, rowQP := range rowsWithQueuePosition { + job := rowQP.ProvisionerJob + if arg.OrganizationID.Valid && job.OrganizationID != arg.OrganizationID.UUID { continue } @@ -4045,34 +4058,19 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition continue } - if job.JobStatus == database.ProvisionerJobStatusPending { - queuedJobs = append(queuedJobs, job) - } row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{ - ProvisionerJob: job, + ProvisionerJob: rowQP.ProvisionerJob, + QueuePosition: rowQP.QueuePosition, + QueueSize: rowQP.QueueSize, } - rows = append(rows, row) - } - slices.SortFunc(queuedJobs, func(a, b database.ProvisionerJob) int { - return a.CreatedAt.Compare(b.CreatedAt) - }) - for i := range rows { - queuePosition := slices.IndexFunc(queuedJobs, func(job database.ProvisionerJob) bool { - return job.ID == rows[i].ProvisionerJob.ID - }) - if queuePosition >= 0 { - rows[i].QueuePosition = int64(queuePosition + 1) - rows[i].QueueSize = int64(len(queuedJobs)) - - for _, daemon := range q.provisionerDaemons { - job := rows[i].ProvisionerJob - if daemon.OrganizationID == job.OrganizationID && - slices.Contains(daemon.Provisioners, job.Provisioner) && - tagsSubset(job.Tags, daemon.Tags) { - rows[i].AvailableWorkers = append(rows[i].AvailableWorkers, daemon.ID) - } + for _, daemon := range q.provisionerDaemons { + if daemon.OrganizationID == job.OrganizationID && + slices.Contains(daemon.Provisioners, job.Provisioner) && + tagsSubset(job.Tags, daemon.Tags) { + row.AvailableWorkers = append(row.AvailableWorkers, daemon.ID) } } + rows = append(rows, row) } return rows, nil From 8fd1a6ad08a7c9fc1e98a5dcbe2c763dd7846a3f Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 19:23:04 +0200 Subject: [PATCH 13/21] Update cli/provisionerjobs.go Co-authored-by: Steven Masley --- cli/provisionerjobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 9aa5974f80779..e833554d725ca 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -13,7 +13,7 @@ import ( func (r *RootCmd) provisionerJobs() *serpent.Command { cmd := &serpent.Command{ Use: "jobs", - Short: "View and manage provisioner jobs", + Short: "View and manage provisioner jobs.", Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, From b57bc8c9108c95d785879ae8d4dbd5440ff386d8 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 17:42:41 +0000 Subject: [PATCH 14/21] lint --- coderd/database/dbmem/dbmem.go | 2 +- docs/admin/monitoring/logs.md | 2 +- docs/admin/provisioners.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 85a6fd671a687..25755fc3417d1 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3749,7 +3749,7 @@ func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg return daemons, nil } -func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { +func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Context, arg database.GetProvisionerDaemonsWithStatusByOrganizationParams) ([]database.GetProvisionerDaemonsWithStatusByOrganizationRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err diff --git a/docs/admin/monitoring/logs.md b/docs/admin/monitoring/logs.md index 8077a46fe1c73..7a6229c5c9f74 100644 --- a/docs/admin/monitoring/logs.md +++ b/docs/admin/monitoring/logs.md @@ -25,7 +25,7 @@ Connect logs are all captured in the `coderd` logs. ## `provisionerd` Logs Logs for [external provisioners](../provisioners.md) are structured -[and configured](../../reference/cli/provisioner_start.md#--log-human) similarly +[and configured](../../reference/cli/provisioners_start.md#--log-human) similarly to `coderd` logs. Use these logs to troubleshoot and monitor the Terraform operations behind workspaces and templates. diff --git a/docs/admin/provisioners.md b/docs/admin/provisioners.md index f0c8cc5186a21..921a193ebe280 100644 --- a/docs/admin/provisioners.md +++ b/docs/admin/provisioners.md @@ -28,7 +28,7 @@ For example, running 30 provisioner containers will allow 30 users to start workspaces at the same time. Provisioners are started with the -[`coder provisioner start`](../reference/cli/provisioner_start.md) command in +[`coder provisioner start`](../reference/cli/provisioners_start.md) command in the [full Coder binary](https://github.com/coder/coder/releases). Keep reading to learn how to start provisioners via Docker, Kubernetes, Systemd, etc. @@ -235,7 +235,7 @@ This is illustrated in the below table: Provisioners can broadly be categorized by scope: `organization` or `user`. The scope of a provisioner can be specified with -[`-tag=scope=`](../reference/cli/provisioner_start.md#-t---tag) when +[`-tag=scope=`](../reference/cli/provisioners_start.md#-t---tag) when starting the provisioner daemon. Only users with at least the [Template Admin](./users/index.md#roles) role or higher may create organization-scoped provisioner daemons. @@ -245,7 +245,7 @@ There are two exceptions: - [Built-in provisioners](../reference/cli/server.md#--provisioner-daemons) are always organization-scoped. - External provisioners started using a - [pre-shared key (PSK)](../reference/cli/provisioner_start.md#--psk) are always + [pre-shared key (PSK)](../reference/cli/provisioners_start.md#--psk) are always organization-scoped. ### Organization-Scoped Provisioners From 32fde6660891868cc5f551900a9d3267fa0e1cf4 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 17:43:55 +0000 Subject: [PATCH 15/21] remove unused --- enterprise/coderd/provisionerdaemons.go | 45 ------------------------- 1 file changed, 45 deletions(-) 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 From 38116d7759038e540b7f3d827838760b93cfc9de Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 17:44:50 +0000 Subject: [PATCH 16/21] gen, remove unused --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- docs/manifest.json | 2 +- docs/reference/api/enterprise.md | 64 ++++++++++++++++++------- docs/reference/cli/provisioners.md | 2 +- docs/reference/cli/provisioners_jobs.md | 2 +- 6 files changed, 52 insertions(+), 22 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f91bb0863d19b..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" } } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0b9e64d5e471a..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" } } } diff --git a/docs/manifest.json b/docs/manifest.json index 8905b519ff193..d655c06dd5201 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1145,7 +1145,7 @@ }, { "title": "provisioners jobs", - "description": "View and manage provisioner jobs", + "description": "View and manage provisioner jobs.", "path": "reference/cli/provisioners_jobs.md" }, { 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/cli/provisioners.md b/docs/reference/cli/provisioners.md index e5d80c4f217ab..0d86ffac4e28b 100644 --- a/docs/reference/cli/provisioners.md +++ b/docs/reference/cli/provisioners.md @@ -19,6 +19,6 @@ coder provisioners | Name | Purpose | | --------------------------------------------- | ------------------------------------------- | | [list](./provisioners_list.md) | List provisioner daemons in an organization | -| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs | +| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs. | | [start](./provisioners_start.md) | Run a provisioner daemon | | [keys](./provisioners_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioners_jobs.md b/docs/reference/cli/provisioners_jobs.md index c4a9112da7c08..6a97c73467545 100644 --- a/docs/reference/cli/provisioners_jobs.md +++ b/docs/reference/cli/provisioners_jobs.md @@ -2,7 +2,7 @@ # provisioners jobs -View and manage provisioner jobs +View and manage provisioner jobs. Aliases: From e7ac032d11959bef40f2cefaa370a8dab897ea8a Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 20 Dec 2024 17:55:22 +0000 Subject: [PATCH 17/21] slice.ToStringEnums --- cli/provisionerjobs.go | 12 +++--------- coderd/provisionerjobs.go | 10 ++-------- coderd/util/slice/slice.go | 8 ++++++++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index e833554d725ca..0cb1b12b38855 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -6,6 +6,7 @@ import ( "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" ) @@ -59,7 +60,7 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { } jobs, err := client.OrganizationProvisionerJobs(ctx, org.ID, &codersdk.OrganizationProvisionerJobsOptions{ - Status: convertSlice([]codersdk.ProvisionerJobStatus{}, status), + Status: slice.ToStringEnums[codersdk.ProvisionerJobStatus](status), Limit: int(limit), }) if err != nil { @@ -100,7 +101,7 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { FlagShorthand: "s", Env: "CODER_PROVISIONER_JOB_LIST_STATUS", Description: "Filter by job status.", - Value: serpent.EnumArrayOf(&status, convertSlice([]string{}, codersdk.ProvisionerJobStatusEnums())...), + Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerJobStatusEnums())...), }, { Flag: "limit", @@ -117,10 +118,3 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { return cmd } - -func convertSlice[D, S ~string](dstType []D, src []S) []D { - for _, item := range src { - dstType = append(dstType, D(item)) - } - return dstType -} diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 34199e560ccdd..9646bd8e2fb60 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -20,6 +20,7 @@ import ( "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" @@ -55,7 +56,7 @@ func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) { jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true}, - Status: convertSlice([]database.ProvisionerJobStatus(nil), status), + Status: slice.ToStringEnums[database.ProvisionerJobStatus](status), Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, }) if err != nil { @@ -77,13 +78,6 @@ func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) { })) } -func convertSlice[D, S ~string](dstType []D, src []S) []D { - for _, item := range src { - dstType = append(dstType, D(item)) - } - return dstType -} - // Returns provisioner logs based on query parameters. // The intended usage for a client to stream all logs (with JS API): // GET /logs 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)) From f6c2a3fe09646d094be91af543835226f7374fcd Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Sun, 22 Dec 2024 19:17:33 +0000 Subject: [PATCH 18/21] lint & gen --- cli/provisioners_test.go | 2 +- coderd/database/dbmem/dbmem.go | 180 ++++++++++++++++----------------- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go index 4903f4c55a379..5180429f1eb44 100644 --- a/cli/provisioners_test.go +++ b/cli/provisioners_test.go @@ -31,7 +31,7 @@ func TestProvisioners(t *testing.T) { // Create initial resources with a running provisioner. firstProvisioner := coderdtest.NewProvisionerDaemon(t, coderdAPI) - defer firstProvisioner.Close() + 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) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 25755fc3417d1..adc865829dca6 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") } @@ -3886,96 +3976,6 @@ func (q *FakeQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Conte return q.getProvisionerJobsByIDsWithQueuePositionLocked(ctx, ids) } -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 (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { err := validateDatabaseType(arg) if err != nil { From 876bd8087fc8e9283f68b51caff77875a86b07f0 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 31 Dec 2024 10:03:51 +0000 Subject: [PATCH 19/21] dbmem/golden --- cli/clitest/golden.go | 25 ++--- cli/provisionerjobs.go | 2 +- cli/provisioners_test.go | 101 +++++++++++++----- .../TestProvisioners_Golden/jobs_list.golden | 6 ++ .../TestProvisioners_Golden/list.golden | 5 + ...rovisioners_jobs_list_--output_json.golden | 12 +-- coderd/database/dbmem/dbmem.go | 50 ++++++--- coderd/database/queries.sql.go | 26 +++-- .../database/queries/provisionerdaemons.sql | 4 +- coderd/database/queries/provisionerjobs.sql | 24 +++-- docs/manifest.json | 2 +- docs/reference/cli/provisioners.md | 2 +- docs/reference/cli/provisioners_jobs.md | 2 +- 13 files changed, 179 insertions(+), 82 deletions(-) create mode 100644 cli/testdata/TestProvisioners_Golden/jobs_list.golden create mode 100644 cli/testdata/TestProvisioners_Golden/list.golden diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go index 9d82f73f0cc49..b2ec3d9e19365 100644 --- a/cli/clitest/golden.go +++ b/cli/clitest/golden.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -95,32 +96,32 @@ ExtractCommandPathsLoop: // TestGoldenFile will test the given bytes slice input against the // golden file with the given file name, optionally using the given replacements. -func TestGoldenFile(t *testing.T, fileName string, actual []byte, replacements map[string]string) { - if len(actual) == 0 { +func TestGoldenFile(t *testing.T, fileName string, got []byte, replacements map[string]string) { + t.Helper() + + if len(got) == 0 { t.Fatal("no output") } for k, v := range replacements { - actual = bytes.ReplaceAll(actual, []byte(k), []byte(v)) + got = bytes.ReplaceAll(got, []byte(k), []byte(v)) } - actual = normalizeGoldenFile(t, actual) + got = normalizeGoldenFile(t, got) goldenPath := filepath.Join("testdata", strings.ReplaceAll(fileName, " ", "_")+".golden") if *UpdateGoldenFiles { t.Logf("update golden file for: %q: %s", fileName, goldenPath) - err := os.WriteFile(goldenPath, actual, 0o600) + err := os.WriteFile(goldenPath, got, 0o600) require.NoError(t, err, "update golden file") } - expected, err := os.ReadFile(goldenPath) + want, err := os.ReadFile(goldenPath) require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes") - expected = normalizeGoldenFile(t, expected) - require.Equal( - t, string(expected), string(actual), - "golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes", - goldenPath, - ) + want = normalizeGoldenFile(t, want) + if diff := cmp.Diff(string(want), string(got)); diff != "" { + require.Failf(t, "golden file mismatch, run \"make update-golden-files\", verify and commit the changes", "%s: diff (-want +got)\n%s", goldenPath, diff) + } } // normalizeGoldenFile replaces any strings that are system or timing dependent diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 0cb1b12b38855..4f7579fd28f83 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -14,7 +14,7 @@ import ( func (r *RootCmd) provisionerJobs() *serpent.Command { cmd := &serpent.Command{ Use: "jobs", - Short: "View and manage provisioner jobs.", + Short: "View and manage provisioner jobs", Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go index 5180429f1eb44..bb22d63f2e35b 100644 --- a/cli/provisioners_test.go +++ b/cli/provisioners_test.go @@ -1,8 +1,12 @@ package cli_test import ( + "bytes" + "context" "database/sql" "encoding/json" + "fmt" + "slices" "testing" "time" @@ -10,17 +14,55 @@ import ( "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(t *testing.T) { +func TestProvisioners_Golden(t *testing.T) { t.Parallel() - db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + // 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, @@ -36,27 +78,32 @@ func TestProvisioners(t *testing.T) { 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 jobs. + // 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: timeParse(t, "2006-01-02", "2024-12-20"), + 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-bbbb-000000000001") + wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001") job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ - ID: uuid.MustParse("00000000-0000-0000-cccc-000000000001"), 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"}, }) @@ -67,10 +114,10 @@ func TestProvisioners(t *testing.T) { TemplateVersionID: version.ID, }) - // Create another provisioner that completed a job and is offline. + // Create a provisioner that completed a job previously and is offline. pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ Name: "provisioner-2", - CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"), + CreatedAt: dbtime.Now().Add(2 * time.Second), LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true}, KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) @@ -78,11 +125,11 @@ func TestProvisioners(t *testing.T) { OwnerID: memberUser.ID, TemplateID: template.ID, }) - wb2ID := uuid.MustParse("00000000-0000-0000-bbbb-000000000002") + wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002") job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ - ID: uuid.MustParse("00000000-0000-0000-cccc-000000000002"), 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"}, @@ -99,11 +146,11 @@ func TestProvisioners(t *testing.T) { OwnerID: memberUser.ID, TemplateID: template.ID, }) - wb3ID := uuid.MustParse("00000000-0000-0000-bbbb-000000000003") + wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003") job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{ - ID: uuid.MustParse("00000000-0000-0000-cccc-000000000003"), - Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`), - Tags: database.StringMap{"owner": "", "scope": "organization"}, + 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, @@ -113,48 +160,50 @@ func TestProvisioners(t *testing.T) { }) // Create a provisioner that is idle. - pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ + _ = dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{ Name: "provisioner-3", - CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"), + CreatedAt: dbtime.Now().Add(3 * time.Second), KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), }) - _ = pd3 + + 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) - // TODO(mafredri): Verify golden output. + 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) - // TODO(mafredri): Verify golden output. + clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace) }) } - -func timeParse(t *testing.T, layout, s string) time.Time { - t.Helper() - tm, err := time.Parse(layout, s) - require.NoError(t, err) - return tm -} 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_provisioners_jobs_list_--output_json.golden b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden index a19683573bba2..88dba1ad13d55 100644 --- a/cli/testdata/coder_provisioners_jobs_list_--output_json.golden +++ b/cli/testdata/coder_provisioners_jobs_list_--output_json.golden @@ -1,6 +1,6 @@ [ { - "id": "==========[version job ID]==========", + "id": "======[workspace build job ID]======", "created_at": "====[timestamp]=====", "started_at": "====[timestamp]=====", "completed_at": "====[timestamp]=====", @@ -15,13 +15,13 @@ "queue_size": 0, "organization_id": "===========[first org ID]===========", "input": { - "template_version_id": "============[version ID]============" + "workspace_build_id": "========[workspace build ID]========" }, - "type": "template_version_import", + "type": "workspace_build", "organization_name": "Coder" }, { - "id": "======[workspace build job ID]======", + "id": "==========[version job ID]==========", "created_at": "====[timestamp]=====", "started_at": "====[timestamp]=====", "completed_at": "====[timestamp]=====", @@ -36,9 +36,9 @@ "queue_size": 0, "organization_id": "===========[first org ID]===========", "input": { - "workspace_build_id": "========[workspace build ID]========" + "template_version_id": "============[version ID]============" }, - "type": "workspace_build", + "type": "template_version_import", "organization_name": "Coder" } ] diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index adc865829dca6..88d0462682a40 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3861,34 +3861,37 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co } 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 { - var currentJob *database.ProvisionerJob for _, job := range q.provisionerJobs { - if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID && !job.CompletedAt.Valid { - currentJob = &job + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID && !job.CompletedAt.Valid && !job.Error.Valid { + currentJob = job break } } - if currentJob != nil { + if currentJob.ID != uuid.Nil { status = database.ProvisionerDaemonStatusBusy } else { status = database.ProvisionerDaemonStatusIdle } } - var currentJob, previousJob database.ProvisionerJob + var previousJob database.ProvisionerJob for _, job := range q.provisionerJobs { - if job.WorkerID.Valid && job.WorkerID.UUID != daemon.ID { + if !job.WorkerID.Valid || job.WorkerID.UUID != daemon.ID { continue } - if !job.CompletedAt.Valid { - currentJob = job - } else if job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { - previousJob = job + if job.StartedAt.Valid || + job.CanceledAt.Valid || + job.CompletedAt.Valid || + job.Error.Valid { + if job.CompletedAt.Time.After(previousJob.CompletedAt.Time) { + previousJob = job + } } } @@ -3912,6 +3915,10 @@ func (q *FakeQuerier) GetProvisionerDaemonsWithStatusByOrganization(_ context.Co }) } + slices.SortFunc(rows, func(a, b database.GetProvisionerDaemonsWithStatusByOrganizationRow) int { + return slice.Ascending(a.ProvisionerDaemon.Name, b.ProvisionerDaemon.Name) + }) + return rows, nil } @@ -4063,16 +4070,29 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition QueuePosition: rowQP.QueuePosition, QueueSize: rowQP.QueueSize, } - for _, daemon := range q.provisionerDaemons { - if daemon.OrganizationID == job.OrganizationID && - slices.Contains(daemon.Provisioners, job.Provisioner) && - tagsSubset(job.Tags, daemon.Tags) { - row.AvailableWorkers = append(row.AvailableWorkers, daemon.ID) + 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) } + slices.SortFunc(rows, func(a, b database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) int { + return b.ProvisionerJob.CreatedAt.Compare(a.ProvisionerJob.CreatedAt) + }) + return rows, nil } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 257afe71b722c..f162e26f1a66c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5449,6 +5449,8 @@ 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 { @@ -6093,21 +6095,27 @@ 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, - array_agg(DISTINCT pd.id) FILTER (WHERE pd.id IS NOT NULL)::uuid[] AS available_workers + -- 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 -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 ($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[])) diff --git a/coderd/database/queries/provisionerdaemons.sql b/coderd/database/queries/provisionerdaemons.sql index f6b291448471e..e531960f674ab 100644 --- a/coderd/database/queries/provisionerdaemons.sql +++ b/coderd/database/queries/provisionerdaemons.sql @@ -72,7 +72,9 @@ LEFT JOIN 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)); + 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 diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index 9d9fb9628d441..b31bf0454a1f9 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -116,21 +116,27 @@ 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 + -- 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 -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[])) diff --git a/docs/manifest.json b/docs/manifest.json index d655c06dd5201..8905b519ff193 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1145,7 +1145,7 @@ }, { "title": "provisioners jobs", - "description": "View and manage provisioner jobs.", + "description": "View and manage provisioner jobs", "path": "reference/cli/provisioners_jobs.md" }, { diff --git a/docs/reference/cli/provisioners.md b/docs/reference/cli/provisioners.md index 0d86ffac4e28b..e5d80c4f217ab 100644 --- a/docs/reference/cli/provisioners.md +++ b/docs/reference/cli/provisioners.md @@ -19,6 +19,6 @@ coder provisioners | Name | Purpose | | --------------------------------------------- | ------------------------------------------- | | [list](./provisioners_list.md) | List provisioner daemons in an organization | -| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs. | +| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs | | [start](./provisioners_start.md) | Run a provisioner daemon | | [keys](./provisioners_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioners_jobs.md b/docs/reference/cli/provisioners_jobs.md index 6a97c73467545..c4a9112da7c08 100644 --- a/docs/reference/cli/provisioners_jobs.md +++ b/docs/reference/cli/provisioners_jobs.md @@ -2,7 +2,7 @@ # provisioners jobs -View and manage provisioner jobs. +View and manage provisioner jobs Aliases: From 0788d5602b5ad8952b89378498e731eecb52ea7c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 31 Dec 2024 12:50:44 +0000 Subject: [PATCH 20/21] revert --- cli/clitest/golden.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go index b2ec3d9e19365..9d82f73f0cc49 100644 --- a/cli/clitest/golden.go +++ b/cli/clitest/golden.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -96,32 +95,32 @@ ExtractCommandPathsLoop: // TestGoldenFile will test the given bytes slice input against the // golden file with the given file name, optionally using the given replacements. -func TestGoldenFile(t *testing.T, fileName string, got []byte, replacements map[string]string) { - t.Helper() - - if len(got) == 0 { +func TestGoldenFile(t *testing.T, fileName string, actual []byte, replacements map[string]string) { + if len(actual) == 0 { t.Fatal("no output") } for k, v := range replacements { - got = bytes.ReplaceAll(got, []byte(k), []byte(v)) + actual = bytes.ReplaceAll(actual, []byte(k), []byte(v)) } - got = normalizeGoldenFile(t, got) + actual = normalizeGoldenFile(t, actual) goldenPath := filepath.Join("testdata", strings.ReplaceAll(fileName, " ", "_")+".golden") if *UpdateGoldenFiles { t.Logf("update golden file for: %q: %s", fileName, goldenPath) - err := os.WriteFile(goldenPath, got, 0o600) + err := os.WriteFile(goldenPath, actual, 0o600) require.NoError(t, err, "update golden file") } - want, err := os.ReadFile(goldenPath) + expected, err := os.ReadFile(goldenPath) require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes") - want = normalizeGoldenFile(t, want) - if diff := cmp.Diff(string(want), string(got)); diff != "" { - require.Failf(t, "golden file mismatch, run \"make update-golden-files\", verify and commit the changes", "%s: diff (-want +got)\n%s", goldenPath, diff) - } + expected = normalizeGoldenFile(t, expected) + require.Equal( + t, string(expected), string(actual), + "golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes", + goldenPath, + ) } // normalizeGoldenFile replaces any strings that are system or timing dependent From 24d49da07312bfaac5b3e2702fd46f5e093de2db Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 31 Dec 2024 12:58:30 +0000 Subject: [PATCH 21/21] undo rename --- cli/provisioners.go | 4 +- cli/root_test.go | 16 ++--- cli/testdata/coder_--help.golden | 2 +- ...golden => coder_provisioner_--help.golden} | 4 +- ...n => coder_provisioner_jobs_--help.golden} | 2 +- ...den => coder_provisioner_jobs_list.golden} | 2 +- .../coder_provisioner_jobs_list_--help.golden | 2 +- ...rovisioner_jobs_list_--output_json.golden} | 0 ...t.golden => coder_provisioner_list.golden} | 0 ...n => coder_provisioner_list_--help.golden} | 2 +- ...der_provisioner_list_--output_json.golden} | 0 docs/admin/monitoring/logs.md | 2 +- docs/admin/provisioners.md | 6 +- docs/manifest.json | 36 ++++++------ docs/reference/cli/index.md | 2 +- docs/reference/cli/provisioner.md | 24 ++++++++ docs/reference/cli/provisioner_jobs.md | 21 +++++++ ..._jobs_list.md => provisioner_jobs_list.md} | 4 +- docs/reference/cli/provisioner_keys.md | 23 ++++++++ ...s_create.md => provisioner_keys_create.md} | 4 +- ...s_delete.md => provisioner_keys_delete.md} | 4 +- ..._keys_list.md => provisioner_keys_list.md} | 4 +- ...ovisioners_list.md => provisioner_list.md} | 4 +- ...isioners_start.md => provisioner_start.md} | 4 +- docs/reference/cli/provisioners.md | 24 -------- docs/reference/cli/provisioners_jobs.md | 21 ------- docs/reference/cli/provisioners_keys.md | 23 -------- enterprise/cli/testdata/coder_--help.golden | 2 +- .../testdata/coder_provisioner_--help.golden | 4 +- ...n => coder_provisioner_jobs_--help.golden} | 2 +- .../coder_provisioner_jobs_list_--help.golden | 2 +- .../coder_provisioner_keys_list_--help.golden | 6 ++ ...n => coder_provisioner_list_--help.golden} | 2 +- .../testdata/coder_provisioners_--help.golden | 17 ------ .../coder_provisioners_keys_--help.golden | 16 ----- ...der_provisioners_keys_create_--help.golden | 16 ----- ...der_provisioners_keys_delete_--help.golden | 18 ------ ...coder_provisioners_keys_list_--help.golden | 21 ------- .../coder_provisioners_start_--help.golden | 58 ------------------- 39 files changed, 133 insertions(+), 271 deletions(-) rename cli/testdata/{coder_provisioners_--help.golden => coder_provisioner_--help.golden} (85%) rename cli/testdata/{coder_provisioners_jobs_--help.golden => coder_provisioner_jobs_--help.golden} (87%) rename cli/testdata/{coder_provisioners_jobs_list.golden => coder_provisioner_jobs_list.golden} (100%) rename enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden => cli/testdata/coder_provisioner_jobs_list_--help.golden (95%) rename cli/testdata/{coder_provisioners_jobs_list_--output_json.golden => coder_provisioner_jobs_list_--output_json.golden} (100%) rename cli/testdata/{coder_provisioners_list.golden => coder_provisioner_list.golden} (100%) rename cli/testdata/{coder_provisioners_list_--help.golden => coder_provisioner_list_--help.golden} (94%) rename cli/testdata/{coder_provisioners_list_--output_json.golden => coder_provisioner_list_--output_json.golden} (100%) create mode 100644 docs/reference/cli/provisioner.md create mode 100644 docs/reference/cli/provisioner_jobs.md rename docs/reference/cli/{provisioners_jobs_list.md => provisioner_jobs_list.md} (97%) create mode 100644 docs/reference/cli/provisioner_keys.md rename docs/reference/cli/{provisioners_keys_create.md => provisioner_keys_create.md} (90%) rename docs/reference/cli/{provisioners_keys_delete.md => provisioner_keys_delete.md} (87%) rename docs/reference/cli/{provisioners_keys_list.md => provisioner_keys_list.md} (93%) rename docs/reference/cli/{provisioners_list.md => provisioner_list.md} (96%) rename docs/reference/cli/{provisioners_start.md => provisioner_start.md} (98%) delete mode 100644 docs/reference/cli/provisioners.md delete mode 100644 docs/reference/cli/provisioners_jobs.md delete mode 100644 docs/reference/cli/provisioners_keys.md rename enterprise/cli/testdata/{coder_provisioners_jobs_--help.golden => coder_provisioner_jobs_--help.golden} (87%) rename cli/testdata/coder_provisioners_jobs_list_--help.golden => enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden (95%) rename enterprise/cli/testdata/{coder_provisioners_list_--help.golden => coder_provisioner_list_--help.golden} (94%) delete mode 100644 enterprise/cli/testdata/coder_provisioners_--help.golden delete mode 100644 enterprise/cli/testdata/coder_provisioners_keys_--help.golden delete mode 100644 enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden delete mode 100644 enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden delete mode 100644 enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden delete mode 100644 enterprise/cli/testdata/coder_provisioners_start_--help.golden diff --git a/cli/provisioners.go b/cli/provisioners.go index c1eab83240203..af6869b89f566 100644 --- a/cli/provisioners.go +++ b/cli/provisioners.go @@ -12,12 +12,12 @@ import ( func (r *RootCmd) Provisioners() *serpent.Command { cmd := &serpent.Command{ - Use: "provisioners", + Use: "provisioner", Short: "View and manage provisioner daemons and jobs", Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, - Aliases: []string{"provisioner"}, + Aliases: []string{"provisioners"}, Children: []*serpent.Command{ r.provisionerList(), r.provisionerJobs(), diff --git a/cli/root_test.go b/cli/root_test.go index 9fa95c6de8f73..ac1454152672e 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -56,20 +56,20 @@ func TestCommandHelp(t *testing.T) { Cmd: []string{"users", "list"}, }, clitest.CommandHelpCase{ - Name: "coder provisioners list", - Cmd: []string{"provisioners", "list"}, + Name: "coder provisioner list", + Cmd: []string{"provisioner", "list"}, }, clitest.CommandHelpCase{ - Name: "coder provisioners list --output json", - Cmd: []string{"provisioners", "list", "--output", "json"}, + Name: "coder provisioner list --output json", + Cmd: []string{"provisioner", "list", "--output", "json"}, }, clitest.CommandHelpCase{ - Name: "coder provisioners jobs list", - Cmd: []string{"provisioners", "jobs", "list"}, + Name: "coder provisioner jobs list", + Cmd: []string{"provisioner", "jobs", "list"}, }, clitest.CommandHelpCase{ - Name: "coder provisioners jobs list --output json", - Cmd: []string{"provisioners", "jobs", "list", "--output", "json"}, + Name: "coder provisioner jobs list --output json", + Cmd: []string{"provisioner", "jobs", "list", "--output", "json"}, }, )) } diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index 513aacbc3e6dc..4e0a5e92f63b5 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -35,7 +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". - provisioners View and manage provisioner daemons and jobs + 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_provisioners_--help.golden b/cli/testdata/coder_provisioner_--help.golden similarity index 85% rename from cli/testdata/coder_provisioners_--help.golden rename to cli/testdata/coder_provisioner_--help.golden index bf8d220e1d063..4f4a783dcc477 100644 --- a/cli/testdata/coder_provisioners_--help.golden +++ b/cli/testdata/coder_provisioner_--help.golden @@ -1,11 +1,11 @@ coder v0.0.0-devel USAGE: - coder provisioners + coder provisioner View and manage provisioner daemons and jobs - Aliases: provisioner + Aliases: provisioners SUBCOMMANDS: jobs View and manage provisioner jobs diff --git a/cli/testdata/coder_provisioners_jobs_--help.golden b/cli/testdata/coder_provisioner_jobs_--help.golden similarity index 87% rename from cli/testdata/coder_provisioners_jobs_--help.golden rename to cli/testdata/coder_provisioner_jobs_--help.golden index 1665bb97b81ae..6442c78a03a8e 100644 --- a/cli/testdata/coder_provisioners_jobs_--help.golden +++ b/cli/testdata/coder_provisioner_jobs_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners jobs + coder provisioner jobs View and manage provisioner jobs diff --git a/cli/testdata/coder_provisioners_jobs_list.golden b/cli/testdata/coder_provisioner_jobs_list.golden similarity index 100% rename from cli/testdata/coder_provisioners_jobs_list.golden rename to cli/testdata/coder_provisioner_jobs_list.golden index b41f4fc531316..8dc1398f2d890 100644 --- a/cli/testdata/coder_provisioners_jobs_list.golden +++ b/cli/testdata/coder_provisioner_jobs_list.golden @@ -1,3 +1,3 @@ ID CREATED AT STATUS TAGS TYPE ORGANIZATION QUEUE -==========[version job ID]========== ====[timestamp]===== succeeded map[owner: scope:organization] template_version_import Coder ======[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/enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden b/cli/testdata/coder_provisioner_jobs_list_--help.golden similarity index 95% rename from enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden rename to cli/testdata/coder_provisioner_jobs_list_--help.golden index e46e7e3914da0..585e918c23e7b 100644 --- a/enterprise/cli/testdata/coder_provisioners_jobs_list_--help.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners jobs list [flags] + coder provisioner jobs list [flags] List provisioner jobs diff --git a/cli/testdata/coder_provisioners_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden similarity index 100% rename from cli/testdata/coder_provisioners_jobs_list_--output_json.golden rename to cli/testdata/coder_provisioner_jobs_list_--output_json.golden diff --git a/cli/testdata/coder_provisioners_list.golden b/cli/testdata/coder_provisioner_list.golden similarity index 100% rename from cli/testdata/coder_provisioners_list.golden rename to cli/testdata/coder_provisioner_list.golden diff --git a/cli/testdata/coder_provisioners_list_--help.golden b/cli/testdata/coder_provisioner_list_--help.golden similarity index 94% rename from cli/testdata/coder_provisioners_list_--help.golden rename to cli/testdata/coder_provisioner_list_--help.golden index c8bc6c84f552e..a9943cb9da392 100644 --- a/cli/testdata/coder_provisioners_list_--help.golden +++ b/cli/testdata/coder_provisioner_list_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners list [flags] + coder provisioner list [flags] List provisioner daemons in an organization diff --git a/cli/testdata/coder_provisioners_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden similarity index 100% rename from cli/testdata/coder_provisioners_list_--output_json.golden rename to cli/testdata/coder_provisioner_list_--output_json.golden diff --git a/docs/admin/monitoring/logs.md b/docs/admin/monitoring/logs.md index 7a6229c5c9f74..8077a46fe1c73 100644 --- a/docs/admin/monitoring/logs.md +++ b/docs/admin/monitoring/logs.md @@ -25,7 +25,7 @@ Connect logs are all captured in the `coderd` logs. ## `provisionerd` Logs Logs for [external provisioners](../provisioners.md) are structured -[and configured](../../reference/cli/provisioners_start.md#--log-human) similarly +[and configured](../../reference/cli/provisioner_start.md#--log-human) similarly to `coderd` logs. Use these logs to troubleshoot and monitor the Terraform operations behind workspaces and templates. diff --git a/docs/admin/provisioners.md b/docs/admin/provisioners.md index 921a193ebe280..f0c8cc5186a21 100644 --- a/docs/admin/provisioners.md +++ b/docs/admin/provisioners.md @@ -28,7 +28,7 @@ For example, running 30 provisioner containers will allow 30 users to start workspaces at the same time. Provisioners are started with the -[`coder provisioner start`](../reference/cli/provisioners_start.md) command in +[`coder provisioner start`](../reference/cli/provisioner_start.md) command in the [full Coder binary](https://github.com/coder/coder/releases). Keep reading to learn how to start provisioners via Docker, Kubernetes, Systemd, etc. @@ -235,7 +235,7 @@ This is illustrated in the below table: Provisioners can broadly be categorized by scope: `organization` or `user`. The scope of a provisioner can be specified with -[`-tag=scope=`](../reference/cli/provisioners_start.md#-t---tag) when +[`-tag=scope=`](../reference/cli/provisioner_start.md#-t---tag) when starting the provisioner daemon. Only users with at least the [Template Admin](./users/index.md#roles) role or higher may create organization-scoped provisioner daemons. @@ -245,7 +245,7 @@ There are two exceptions: - [Built-in provisioners](../reference/cli/server.md#--provisioner-daemons) are always organization-scoped. - External provisioners started using a - [pre-shared key (PSK)](../reference/cli/provisioners_start.md#--psk) are always + [pre-shared key (PSK)](../reference/cli/provisioner_start.md#--psk) are always organization-scoped. ### Organization-Scoped Provisioners diff --git a/docs/manifest.json b/docs/manifest.json index 8905b519ff193..7767f995e73d0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1139,49 +1139,49 @@ "path": "reference/cli/port-forward.md" }, { - "title": "provisioners", + "title": "provisioner", "description": "View and manage provisioner daemons and jobs", - "path": "reference/cli/provisioners.md" + "path": "reference/cli/provisioner.md" }, { - "title": "provisioners jobs", + "title": "provisioner jobs", "description": "View and manage provisioner jobs", - "path": "reference/cli/provisioners_jobs.md" + "path": "reference/cli/provisioner_jobs.md" }, { - "title": "provisioners jobs list", + "title": "provisioner jobs list", "description": "List provisioner jobs", - "path": "reference/cli/provisioners_jobs_list.md" + "path": "reference/cli/provisioner_jobs_list.md" }, { - "title": "provisioners keys", + "title": "provisioner keys", "description": "Manage provisioner keys", - "path": "reference/cli/provisioners_keys.md" + "path": "reference/cli/provisioner_keys.md" }, { - "title": "provisioners keys create", + "title": "provisioner keys create", "description": "Create a new provisioner key", - "path": "reference/cli/provisioners_keys_create.md" + "path": "reference/cli/provisioner_keys_create.md" }, { - "title": "provisioners keys delete", + "title": "provisioner keys delete", "description": "Delete a provisioner key", - "path": "reference/cli/provisioners_keys_delete.md" + "path": "reference/cli/provisioner_keys_delete.md" }, { - "title": "provisioners keys list", + "title": "provisioner keys list", "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioners_keys_list.md" + "path": "reference/cli/provisioner_keys_list.md" }, { - "title": "provisioners list", + "title": "provisioner list", "description": "List provisioner daemons in an organization", - "path": "reference/cli/provisioners_list.md" + "path": "reference/cli/provisioner_list.md" }, { - "title": "provisioners start", + "title": "provisioner start", "description": "Run a provisioner daemon", - "path": "reference/cli/provisioners_start.md" + "path": "reference/cli/provisioner_start.md" }, { "title": "publickey", diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index d7b24de95d309..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 | -| [provisioners](./provisioners.md) | View and manage provisioner daemons and jobs | +| [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 new file mode 100644 index 0000000000000..12a70203748e1 --- /dev/null +++ b/docs/reference/cli/provisioner.md @@ -0,0 +1,24 @@ + + +# provisioner + +View and manage provisioner daemons and jobs + +Aliases: + +- provisioners + +## Usage + +```console +coder provisioner +``` + +## Subcommands + +| 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/provisioners_jobs_list.md b/docs/reference/cli/provisioner_jobs_list.md similarity index 97% rename from docs/reference/cli/provisioners_jobs_list.md rename to docs/reference/cli/provisioner_jobs_list.md index c0952566b5723..6e4dad23442ec 100644 --- a/docs/reference/cli/provisioners_jobs_list.md +++ b/docs/reference/cli/provisioner_jobs_list.md @@ -1,6 +1,6 @@ -# provisioners jobs list +# provisioner jobs list List provisioner jobs @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioners jobs list [flags] +coder provisioner jobs list [flags] ``` ## Options diff --git a/docs/reference/cli/provisioner_keys.md b/docs/reference/cli/provisioner_keys.md new file mode 100644 index 0000000000000..014af6f117c3a --- /dev/null +++ b/docs/reference/cli/provisioner_keys.md @@ -0,0 +1,23 @@ + + +# provisioner keys + +Manage provisioner keys + +Aliases: + +- key + +## Usage + +```console +coder provisioner keys +``` + +## Subcommands + +| Name | Purpose | +| --------------------------------------------------- | ---------------------------------------- | +| [create](./provisioner_keys_create.md) | Create a new provisioner key | +| [list](./provisioner_keys_list.md) | List provisioner keys in an organization | +| [delete](./provisioner_keys_delete.md) | Delete a provisioner key | diff --git a/docs/reference/cli/provisioners_keys_create.md b/docs/reference/cli/provisioner_keys_create.md similarity index 90% rename from docs/reference/cli/provisioners_keys_create.md rename to docs/reference/cli/provisioner_keys_create.md index 4367dfd680d54..da6479d15bfc9 100644 --- a/docs/reference/cli/provisioners_keys_create.md +++ b/docs/reference/cli/provisioner_keys_create.md @@ -1,13 +1,13 @@ -# provisioners keys create +# provisioner keys create Create a new provisioner key ## Usage ```console -coder provisioners keys create [flags] +coder provisioner keys create [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners_keys_delete.md b/docs/reference/cli/provisioner_keys_delete.md similarity index 87% rename from docs/reference/cli/provisioners_keys_delete.md rename to docs/reference/cli/provisioner_keys_delete.md index 3d7b6273612c2..56e32e57d048b 100644 --- a/docs/reference/cli/provisioners_keys_delete.md +++ b/docs/reference/cli/provisioner_keys_delete.md @@ -1,6 +1,6 @@ -# provisioners keys delete +# provisioner keys delete Delete a provisioner key @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioners keys delete [flags] +coder provisioner keys delete [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners_keys_list.md b/docs/reference/cli/provisioner_keys_list.md similarity index 93% rename from docs/reference/cli/provisioners_keys_list.md rename to docs/reference/cli/provisioner_keys_list.md index 99ff1f21cbeb7..d2c174d7e6d68 100644 --- a/docs/reference/cli/provisioners_keys_list.md +++ b/docs/reference/cli/provisioner_keys_list.md @@ -1,6 +1,6 @@ -# provisioners keys list +# provisioner keys list List provisioner keys in an organization @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioners keys list [flags] +coder provisioner keys list [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners_list.md b/docs/reference/cli/provisioner_list.md similarity index 96% rename from docs/reference/cli/provisioners_list.md rename to docs/reference/cli/provisioner_list.md index 1b3c8a62b74ba..f5c476cb8facf 100644 --- a/docs/reference/cli/provisioners_list.md +++ b/docs/reference/cli/provisioner_list.md @@ -1,6 +1,6 @@ -# provisioners list +# provisioner list List provisioner daemons in an organization @@ -11,7 +11,7 @@ Aliases: ## Usage ```console -coder provisioners list [flags] +coder provisioner list [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners_start.md b/docs/reference/cli/provisioner_start.md similarity index 98% rename from docs/reference/cli/provisioners_start.md rename to docs/reference/cli/provisioner_start.md index df97454f81f12..65254d18c0149 100644 --- a/docs/reference/cli/provisioners_start.md +++ b/docs/reference/cli/provisioner_start.md @@ -1,13 +1,13 @@ -# provisioners start +# provisioner start Run a provisioner daemon ## Usage ```console -coder provisioners start [flags] +coder provisioner start [flags] ``` ## Options diff --git a/docs/reference/cli/provisioners.md b/docs/reference/cli/provisioners.md deleted file mode 100644 index e5d80c4f217ab..0000000000000 --- a/docs/reference/cli/provisioners.md +++ /dev/null @@ -1,24 +0,0 @@ - - -# provisioners - -View and manage provisioner daemons and jobs - -Aliases: - -- provisioner - -## Usage - -```console -coder provisioners -``` - -## Subcommands - -| Name | Purpose | -| --------------------------------------------- | ------------------------------------------- | -| [list](./provisioners_list.md) | List provisioner daemons in an organization | -| [jobs](./provisioners_jobs.md) | View and manage provisioner jobs | -| [start](./provisioners_start.md) | Run a provisioner daemon | -| [keys](./provisioners_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioners_jobs.md b/docs/reference/cli/provisioners_jobs.md deleted file mode 100644 index c4a9112da7c08..0000000000000 --- a/docs/reference/cli/provisioners_jobs.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# provisioners jobs - -View and manage provisioner jobs - -Aliases: - -- job - -## Usage - -```console -coder provisioners jobs -``` - -## Subcommands - -| Name | Purpose | -| ------------------------------------------------ | --------------------- | -| [list](./provisioners_jobs_list.md) | List provisioner jobs | diff --git a/docs/reference/cli/provisioners_keys.md b/docs/reference/cli/provisioners_keys.md deleted file mode 100644 index 2a8418550e39f..0000000000000 --- a/docs/reference/cli/provisioners_keys.md +++ /dev/null @@ -1,23 +0,0 @@ - - -# provisioners keys - -Manage provisioner keys - -Aliases: - -- key - -## Usage - -```console -coder provisioners keys -``` - -## Subcommands - -| Name | Purpose | -| ---------------------------------------------------- | ---------------------------------------- | -| [create](./provisioners_keys_create.md) | Create a new provisioner key | -| [list](./provisioners_keys_list.md) | List provisioner keys in an organization | -| [delete](./provisioners_keys_delete.md) | Delete a provisioner key | diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 81eaad567c190..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 - provisioners View and manage provisioner daemons and jobs + 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_provisioners_jobs_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden similarity index 87% rename from enterprise/cli/testdata/coder_provisioners_jobs_--help.golden rename to enterprise/cli/testdata/coder_provisioner_jobs_--help.golden index 1665bb97b81ae..6442c78a03a8e 100644 --- a/enterprise/cli/testdata/coder_provisioners_jobs_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners jobs + coder provisioner jobs View and manage provisioner jobs diff --git a/cli/testdata/coder_provisioners_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden similarity index 95% rename from cli/testdata/coder_provisioners_jobs_list_--help.golden rename to enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden index e46e7e3914da0..585e918c23e7b 100644 --- a/cli/testdata/coder_provisioners_jobs_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners jobs list [flags] + coder provisioner jobs list [flags] List provisioner jobs 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_provisioners_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_list_--help.golden similarity index 94% rename from enterprise/cli/testdata/coder_provisioners_list_--help.golden rename to enterprise/cli/testdata/coder_provisioner_list_--help.golden index c8bc6c84f552e..a9943cb9da392 100644 --- a/enterprise/cli/testdata/coder_provisioners_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_list_--help.golden @@ -1,7 +1,7 @@ coder v0.0.0-devel USAGE: - coder provisioners list [flags] + coder provisioner list [flags] List provisioner daemons in an organization diff --git a/enterprise/cli/testdata/coder_provisioners_--help.golden b/enterprise/cli/testdata/coder_provisioners_--help.golden deleted file mode 100644 index dca4ddad6b0a2..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_--help.golden +++ /dev/null @@ -1,17 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners - - View and manage provisioner daemons and jobs - - Aliases: provisioner - -SUBCOMMANDS: - jobs View and manage provisioner jobs - keys Manage provisioner keys - list List provisioner daemons in an organization - start Run a provisioner daemon - -——— -Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_--help.golden deleted file mode 100644 index 913c0f6650058..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_keys_--help.golden +++ /dev/null @@ -1,16 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners keys - - Manage provisioner keys - - Aliases: key - -SUBCOMMANDS: - create Create a new provisioner key - delete Delete a provisioner key - list List provisioner keys in an organization - -——— -Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden deleted file mode 100644 index eb20ba516b99b..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_keys_create_--help.golden +++ /dev/null @@ -1,16 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners keys create [flags] - - Create a new provisioner key - -OPTIONS: - -O, --org string, $CODER_ORGANIZATION - Select which organization (uuid or name) to use. - - -t, --tag string-array, $CODER_PROVISIONERD_TAGS - Tags to filter provisioner jobs by. - -——— -Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden deleted file mode 100644 index 5d2a7a4859382..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_keys_delete_--help.golden +++ /dev/null @@ -1,18 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners keys delete [flags] - - Delete a provisioner key - - Aliases: rm - -OPTIONS: - -O, --org string, $CODER_ORGANIZATION - Select which organization (uuid or name) to use. - - -y, --yes bool - Bypass prompts. - -——— -Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden deleted file mode 100644 index 5691eb2933563..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_keys_list_--help.golden +++ /dev/null @@ -1,21 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners keys list [flags] - - List provisioner keys in an organization - - Aliases: ls - -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_provisioners_start_--help.golden b/enterprise/cli/testdata/coder_provisioners_start_--help.golden deleted file mode 100644 index 12c4f5cc1a856..0000000000000 --- a/enterprise/cli/testdata/coder_provisioners_start_--help.golden +++ /dev/null @@ -1,58 +0,0 @@ -coder v0.0.0-devel - -USAGE: - coder provisioners start [flags] - - Run a provisioner daemon - -OPTIONS: - -O, --org string, $CODER_ORGANIZATION - Select which organization (uuid or name) to use. - - -c, --cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir]) - Directory to store cached data. - - --key string, $CODER_PROVISIONER_DAEMON_KEY - Provisioner key to authenticate with Coder server. - - --log-filter string-array, $CODER_PROVISIONER_DAEMON_LOG_FILTER - Filter debug logs by matching against a given regex. Use .* to match - all debug logs. - - --log-human string, $CODER_PROVISIONER_DAEMON_LOGGING_HUMAN (default: /dev/stderr) - Output human-readable logs to a given file. - - --log-json string, $CODER_PROVISIONER_DAEMON_LOGGING_JSON - Output JSON logs to a given file. - - --log-stackdriver string, $CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER - Output Stackdriver compatible logs to a given file. - - --name string, $CODER_PROVISIONER_DAEMON_NAME - Name of this provisioner daemon. Defaults to the current hostname - without FQDN. - - --poll-interval duration, $CODER_PROVISIONERD_POLL_INTERVAL (default: 1s) - Deprecated and ignored. - - --poll-jitter duration, $CODER_PROVISIONERD_POLL_JITTER (default: 100ms) - Deprecated and ignored. - - --prometheus-address string, $CODER_PROMETHEUS_ADDRESS (default: 127.0.0.1:2112) - The bind address to serve prometheus metrics. - - --prometheus-enable bool, $CODER_PROMETHEUS_ENABLE (default: false) - Serve prometheus metrics on the address defined by prometheus address. - - --psk string, $CODER_PROVISIONER_DAEMON_PSK - Pre-shared key to authenticate with Coder server. - DEPRECATED: Use --key instead. - - -t, --tag string-array, $CODER_PROVISIONERD_TAGS - Tags to filter provisioner jobs by. - - --verbose bool, $CODER_PROVISIONER_DAEMON_VERBOSE (default: false) - Output debug-level logs. - -——— -Run `coder --help` for a list of global options.