mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(cli): add provisioner job cancel command (#16252)
Fixes #16117 Updates #15084
This commit is contained in:
committed by
GitHub
parent
84a54c1d7b
commit
75c899ff71
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
Generated
+43
@@ -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": [
|
||||
|
||||
Generated
+39
@@ -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": [
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+63
@@ -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).
|
||||
|
||||
Generated
+4
-3
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user