feat(cli): add provisioner job cancel command (#16252)

Fixes #16117
Updates #15084
This commit is contained in:
Mathias Fredriksson
2025-01-27 18:26:56 +02:00
committed by GitHub
parent 84a54c1d7b
commit 75c899ff71
19 changed files with 568 additions and 21 deletions
+58
View File
@@ -4,9 +4,11 @@ import (
"fmt"
"slices"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
@@ -21,6 +23,7 @@ func (r *RootCmd) provisionerJobs() *serpent.Command {
},
Aliases: []string{"job"},
Children: []*serpent.Command{
r.provisionerJobsCancel(),
r.provisionerJobsList(),
},
}
@@ -124,3 +127,58 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
return cmd
}
func (r *RootCmd) provisionerJobsCancel() *serpent.Command {
var (
client = new(codersdk.Client)
orgContext = NewOrganizationContext()
)
cmd := &serpent.Command{
Use: "cancel <job_id>",
Short: "Cancel a provisioner job",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
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)
}
jobID, err := uuid.Parse(inv.Args[0])
if err != nil {
return xerrors.Errorf("invalid job ID: %w", err)
}
job, err := client.OrganizationProvisionerJob(ctx, org.ID, jobID)
if err != nil {
return xerrors.Errorf("get provisioner job: %w", err)
}
switch job.Type {
case codersdk.ProvisionerJobTypeTemplateVersionDryRun:
_, _ = fmt.Fprintf(inv.Stdout, "Canceling template version dry run job %s...\n", job.ID)
err = client.CancelTemplateVersionDryRun(ctx, ptr.NilToEmpty(job.Input.TemplateVersionID), job.ID)
case codersdk.ProvisionerJobTypeTemplateVersionImport:
_, _ = fmt.Fprintf(inv.Stdout, "Canceling template version import job %s...\n", job.ID)
err = client.CancelTemplateVersion(ctx, ptr.NilToEmpty(job.Input.TemplateVersionID))
case codersdk.ProvisionerJobTypeWorkspaceBuild:
_, _ = fmt.Fprintf(inv.Stdout, "Canceling workspace build job %s...\n", job.ID)
err = client.CancelWorkspaceBuild(ctx, ptr.NilToEmpty(job.Input.WorkspaceBuildID))
}
if err != nil {
return xerrors.Errorf("cancel provisioner job: %w", err)
}
_, _ = fmt.Fprintln(inv.Stdout, "Job canceled")
return nil
},
}
orgContext.AttachOptions(cmd)
return cmd
}
+189
View File
@@ -0,0 +1,189 @@
package cli_test
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/aws/smithy-go/ptr"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"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/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
func TestProvisionerJobs(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
Database: db,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
// Create initial resources with a running provisioner.
firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"})
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, func(req *codersdk.CreateTemplateRequest) {
req.AllowUserCancelWorkspaceJobs = ptr.Bool(true)
})
// Stop the provisioner so it doesn't grab any more jobs.
firstProvisioner.Close()
t.Run("Cancel", func(t *testing.T) {
t.Parallel()
// Set up test helpers.
type jobInput struct {
WorkspaceBuildID string `json:"workspace_build_id,omitempty"`
TemplateVersionID string `json:"template_version_id,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
prepareJob := func(t *testing.T, input jobInput) database.ProvisionerJob {
t.Helper()
inputBytes, err := json.Marshal(input)
require.NoError(t, err)
var typ database.ProvisionerJobType
switch {
case input.WorkspaceBuildID != "":
typ = database.ProvisionerJobTypeWorkspaceBuild
case input.TemplateVersionID != "":
if input.DryRun {
typ = database.ProvisionerJobTypeTemplateVersionDryRun
} else {
typ = database.ProvisionerJobTypeTemplateVersionImport
}
default:
t.Fatal("invalid input")
}
var (
tags = database.StringMap{"owner": "", "scope": "organization", "foo": uuid.New().String()}
_ = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{Tags: tags})
job = dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
InitiatorID: member.ID,
Input: json.RawMessage(inputBytes),
Type: typ,
Tags: tags,
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Minute), Valid: true},
})
)
return job
}
prepareWorkspaceBuildJob := func(t *testing.T) database.ProvisionerJob {
t.Helper()
var (
wbID = uuid.New()
job = prepareJob(t, jobInput{WorkspaceBuildID: wbID.String()})
w = dbgen.Workspace(t, db, database.WorkspaceTable{
OrganizationID: owner.OrganizationID,
OwnerID: member.ID,
TemplateID: template.ID,
})
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
ID: wbID,
InitiatorID: member.ID,
WorkspaceID: w.ID,
TemplateVersionID: version.ID,
JobID: job.ID,
})
)
return job
}
prepareTemplateVersionImportJobBuilder := func(t *testing.T, dryRun bool) database.ProvisionerJob {
t.Helper()
var (
tvID = uuid.New()
job = prepareJob(t, jobInput{TemplateVersionID: tvID.String(), DryRun: dryRun})
_ = dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: owner.OrganizationID,
CreatedBy: templateAdmin.ID,
ID: tvID,
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
JobID: job.ID,
})
)
return job
}
prepareTemplateVersionImportJob := func(t *testing.T) database.ProvisionerJob {
return prepareTemplateVersionImportJobBuilder(t, false)
}
prepareTemplateVersionImportJobDryRun := func(t *testing.T) database.ProvisionerJob {
return prepareTemplateVersionImportJobBuilder(t, true)
}
// Run the cancellation test suite.
for _, tt := range []struct {
role string
client *codersdk.Client
name string
prepare func(*testing.T) database.ProvisionerJob
wantCancelled bool
}{
{"Owner", client, "WorkspaceBuild", prepareWorkspaceBuildJob, true},
{"Owner", client, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
{"Owner", client, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, true},
{"TemplateAdmin", templateAdminClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
{"TemplateAdmin", templateAdminClient, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
{"TemplateAdmin", templateAdminClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
{"Member", memberClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
{"Member", memberClient, "TemplateVersionImport", prepareTemplateVersionImportJob, false},
{"Member", memberClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
} {
tt := tt
wantMsg := "OK"
if !tt.wantCancelled {
wantMsg = "FAIL"
}
t.Run(fmt.Sprintf("%s/%s/%v", tt.role, tt.name, wantMsg), func(t *testing.T) {
t.Parallel()
job := tt.prepare(t)
require.False(t, job.CanceledAt.Valid, "job.CanceledAt.Valid")
inv, root := clitest.New(t, "provisioner", "jobs", "cancel", job.ID.String())
clitest.SetupConfig(t, tt.client, root)
var buf bytes.Buffer
inv.Stdout = &buf
err := inv.Run()
if tt.wantCancelled {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
job, err = db.GetProvisionerJobByID(testutil.Context(t, testutil.WaitShort), job.ID)
require.NoError(t, err)
assert.Equal(t, tt.wantCancelled, job.CanceledAt.Valid, "job.CanceledAt.Valid")
assert.Equal(t, tt.wantCancelled, job.CanceledAt.Time.After(job.StartedAt.Time), "job.CanceledAt.Time")
if tt.wantCancelled {
assert.Contains(t, buf.String(), "Job canceled")
} else {
assert.NotContains(t, buf.String(), "Job canceled")
}
})
}
})
}
+2 -1
View File
@@ -8,7 +8,8 @@ USAGE:
Aliases: job
SUBCOMMANDS:
list List provisioner jobs
cancel Cancel a provisioner job
list List provisioner jobs
———
Run `coder --help` for a list of global options.
@@ -0,0 +1,13 @@
coder v0.0.0-devel
USAGE:
coder provisioner jobs cancel [flags] <job_id>
Cancel a provisioner job
OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
———
Run `coder --help` for a list of global options.
+43
View File
@@ -3090,6 +3090,49 @@ const docTemplate = `{
}
}
},
"/organizations/{organization}/provisionerjobs/{job}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Organizations"
],
"summary": "Get provisioner job",
"operationId": "get-provisioner-job",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Job ID",
"name": "job",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
}
}
}
}
},
"/organizations/{organization}/provisionerkeys": {
"get": {
"security": [
+39
View File
@@ -2718,6 +2718,45 @@
}
}
},
"/organizations/{organization}/provisionerjobs/{job}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Organizations"],
"summary": "Get provisioner job",
"operationId": "get-provisioner-job",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Job ID",
"name": "job",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
}
}
}
}
},
"/organizations/{organization}/provisionerkeys": {
"get": {
"security": [
+1
View File
@@ -1011,6 +1011,7 @@ func New(options *Options) *API {
r.Get("/", api.provisionerDaemons)
})
r.Route("/provisionerjobs", func(r chi.Router) {
r.Get("/{job}", api.provisionerJob)
r.Get("/", api.provisionerJobs)
})
})
+3
View File
@@ -4082,6 +4082,9 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition
if len(arg.Status) > 0 && !slices.Contains(arg.Status, job.JobStatus) {
continue
}
if len(arg.IDs) > 0 && !slices.Contains(arg.IDs, job.ID) {
continue
}
row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{
ProvisionerJob: rowQP.ProvisionerJob,
+10 -3
View File
@@ -6274,7 +6274,8 @@ LEFT JOIN
queue_size qs ON TRUE
WHERE
($1::uuid IS NULL OR pj.organization_id = $1)
AND (COALESCE(array_length($2::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($2::provisioner_job_status[]))
AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[]))
AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[]))
GROUP BY
pj.id,
qp.queue_position,
@@ -6282,11 +6283,12 @@ GROUP BY
ORDER BY
pj.created_at DESC
LIMIT
$3::int
$4::int
`
type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct {
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Status []ProvisionerJobStatus `db:"status" json:"status"`
Limit sql.NullInt32 `db:"limit" json:"limit"`
}
@@ -6299,7 +6301,12 @@ type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
}
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)
rows, err := q.db.QueryContext(ctx, getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner,
arg.OrganizationID,
pq.Array(arg.IDs),
pq.Array(arg.Status),
arg.Limit,
)
if err != nil {
return nil, err
}
@@ -139,6 +139,7 @@ LEFT JOIN
queue_size qs ON TRUE
WHERE
(sqlc.narg('organization_id')::uuid IS NULL OR pj.organization_id = @organization_id)
AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pj.id = ANY(@ids::uuid[]))
AND (COALESCE(array_length(@status::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY(@status::provisioner_job_status[]))
GROUP BY
pj.id,
+66 -13
View File
@@ -29,6 +29,42 @@ import (
"github.com/coder/websocket"
)
// @Summary Get provisioner job
// @ID get-provisioner-job
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Param organization path string true "Organization ID" format(uuid)
// @Param job path string true "Job ID" format(uuid)
// @Success 200 {object} codersdk.ProvisionerJob
// @Router /organizations/{organization}/provisionerjobs/{job} [get]
func (api *API) provisionerJob(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
jobID, ok := httpmw.ParseUUIDParam(rw, r, "job")
if !ok {
return
}
jobs, ok := api.handleAuthAndFetchProvisionerJobs(rw, r, []uuid.UUID{jobID})
if !ok {
return
}
if len(jobs) == 0 {
httpapi.ResourceNotFound(rw)
return
}
if len(jobs) > 1 || jobs[0].ProvisionerJob.ID != jobID {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Detail: "Database returned an unexpected job.",
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerJobWithQueuePosition(jobs[0]))
}
// @Summary Get provisioner jobs
// @ID get-provisioner-jobs
// @Security CoderSessionToken
@@ -41,12 +77,26 @@ import (
// @Router /organizations/{organization}/provisionerjobs [get]
func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
jobs, ok := api.handleAuthAndFetchProvisionerJobs(rw, r, nil)
if !ok {
return
}
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(jobs, convertProvisionerJobWithQueuePosition))
}
// handleAuthAndFetchProvisionerJobs is an internal method shared by
// provisionerJob and provisionerJobs. If ok is false the caller should
// return immediately because the response has already been written.
func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *http.Request, ids []uuid.UUID) (_ []database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, ok bool) {
ctx := r.Context()
org := httpmw.OrganizationParam(r)
// For now, only owners and template admins can access provisioner jobs.
if !api.Authorize(r, policy.ActionRead, rbac.ResourceProvisionerJobs.InOrg(org.ID)) {
httpapi.ResourceNotFound(rw)
return
return nil, false
}
qp := r.URL.Query()
@@ -59,36 +109,29 @@ func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) {
Message: "Invalid query parameters.",
Validations: p.Errors,
})
return
return nil, false
}
jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{
OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true},
Status: slice.StringEnums[database.ProvisionerJobStatus](status),
Limit: sql.NullInt32{Int32: limit, Valid: limit > 0},
IDs: ids,
})
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
return nil, false
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner jobs.",
Detail: err.Error(),
})
return
return nil, false
}
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
}))
return jobs, true
}
// Returns provisioner logs based on query parameters.
@@ -338,6 +381,16 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR
return job
}
func convertProvisionerJobWithQueuePosition(pj database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) codersdk.ProvisionerJob {
job := convertProvisionerJob(database.GetProvisionerJobsByIDsWithQueuePositionRow{
ProvisionerJob: pj.ProvisionerJob,
QueuePosition: pj.QueuePosition,
QueueSize: pj.QueueSize,
})
job.AvailableWorkers = pj.AvailableWorkers
return job
}
func fetchAndWriteLogs(ctx context.Context, db database.Store, jobID uuid.UUID, after int64, rw http.ResponseWriter) {
logs, err := db.GetProvisionerLogsAfterID(ctx, database.GetProvisionerLogsAfterIDParams{
JobID: jobID,
+19
View File
@@ -63,6 +63,25 @@ func TestProvisionerJobs(t *testing.T) {
TemplateVersionID: version.ID,
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
// Note this calls the single job endpoint.
job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID)
require.NoError(t, err)
require.Equal(t, job.ID, job2.ID)
})
t.Run("Missing", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
// Note this calls the single job endpoint.
_, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, uuid.New())
require.Error(t, err)
})
})
t.Run("All", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
+16
View File
@@ -377,6 +377,22 @@ func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID
return jobs, json.NewDecoder(res.Body).Decode(&jobs)
}
func (c *Client) OrganizationProvisionerJob(ctx context.Context, organizationID, jobID uuid.UUID) (job ProvisionerJob, err error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/organizations/%s/provisionerjobs/%s", organizationID.String(), jobID.String()),
nil,
)
if err != nil {
return job, xerrors.Errorf("make request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return job, ReadBodyAsError(res)
}
return job, json.NewDecoder(res.Body).Decode(&job)
}
func joinSlice[T ~string](s []T) string {
var ss []string
for _, v := range s {
+5
View File
@@ -1163,6 +1163,11 @@
"description": "View and manage provisioner jobs",
"path": "reference/cli/provisioner_jobs.md"
},
{
"title": "provisioner jobs cancel",
"description": "Cancel a provisioner job",
"path": "reference/cli/provisioner_jobs_cancel.md"
},
{
"title": "provisioner jobs list",
"description": "List provisioner jobs",
+63
View File
@@ -471,3 +471,66 @@ Status Code **200**
| `type` | `template_version_dry_run` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get provisioner job
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerjobs/{job} \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /organizations/{organization}/provisionerjobs/{job}`
### Parameters
| Name | In | Type | Required | Description |
|----------------|------|--------------|----------|-----------------|
| `organization` | path | string(uuid) | true | Organization ID |
| `job` | path | string(uuid) | true | Job ID |
### Example responses
> 200 Response
```json
{
"available_workers": [
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
],
"canceled_at": "2019-08-24T14:15:22Z",
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
"error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"input": {
"error": "string",
"template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1",
"workspace_build_id": "badaf2eb-96c5-4050-9f1d-db2d39ca5478"
},
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"queue_position": 0,
"queue_size": 0,
"started_at": "2019-08-24T14:15:22Z",
"status": "pending",
"tags": {
"property1": "string",
"property2": "string"
},
"type": "template_version_import",
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
}
```
### Responses
| Status | Meaning | Description | Schema |
|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+4 -3
View File
@@ -15,6 +15,7 @@ coder provisioner jobs
## Subcommands
| Name | Purpose |
|-------------------------------------------------|-----------------------|
| [<code>list</code>](./provisioner_jobs_list.md) | List provisioner jobs |
| Name | Purpose |
|-----------------------------------------------------|--------------------------|
| [<code>cancel</code>](./provisioner_jobs_cancel.md) | Cancel a provisioner job |
| [<code>list</code>](./provisioner_jobs_list.md) | List provisioner jobs |
+21
View File
@@ -0,0 +1,21 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner jobs cancel
Cancel a provisioner job
## Usage
```console
coder provisioner jobs cancel [flags] <job_id>
```
## Options
### -O, --org
| | |
|-------------|----------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.
@@ -8,7 +8,8 @@ USAGE:
Aliases: job
SUBCOMMANDS:
list List provisioner jobs
cancel Cancel a provisioner job
list List provisioner jobs
———
Run `coder --help` for a list of global options.
@@ -0,0 +1,13 @@
coder v0.0.0-devel
USAGE:
coder provisioner jobs cancel [flags] <job_id>
Cancel a provisioner job
OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
———
Run `coder --help` for a list of global options.