Skip to content

Commit efdcbba

Browse files
committed
feat(cli): add provisioner job cancel command
1 parent 84081e9 commit efdcbba

18 files changed

+544
-21
lines changed

cli/provisionerjobs.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"fmt"
55
"slices"
66

7+
"github.com/google/uuid"
78
"golang.org/x/xerrors"
89

910
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/coderd/util/ptr"
1012
"github.com/coder/coder/v2/coderd/util/slice"
1113
"github.com/coder/coder/v2/codersdk"
1214
"github.com/coder/serpent"
@@ -21,6 +23,7 @@ func (r *RootCmd) provisionerJobs() *serpent.Command {
2123
},
2224
Aliases: []string{"job"},
2325
Children: []*serpent.Command{
26+
r.provisionerJobsCancel(),
2427
r.provisionerJobsList(),
2528
},
2629
}
@@ -124,3 +127,58 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
124127

125128
return cmd
126129
}
130+
131+
func (r *RootCmd) provisionerJobsCancel() *serpent.Command {
132+
var (
133+
client = new(codersdk.Client)
134+
orgContext = NewOrganizationContext()
135+
)
136+
cmd := &serpent.Command{
137+
Use: "cancel <job_id>",
138+
Short: "Cancel a provisioner job",
139+
Middleware: serpent.Chain(
140+
serpent.RequireNArgs(1),
141+
r.InitClient(client),
142+
),
143+
Handler: func(inv *serpent.Invocation) error {
144+
ctx := inv.Context()
145+
org, err := orgContext.Selected(inv, client)
146+
if err != nil {
147+
return xerrors.Errorf("current organization: %w", err)
148+
}
149+
150+
jobID, err := uuid.Parse(inv.Args[0])
151+
if err != nil {
152+
return xerrors.Errorf("invalid job ID: %w", err)
153+
}
154+
155+
job, err := client.OrganizationProvisionerJob(ctx, org.ID, jobID)
156+
if err != nil {
157+
return xerrors.Errorf("get provisioner job: %w", err)
158+
}
159+
160+
switch job.Type {
161+
case codersdk.ProvisionerJobTypeTemplateVersionDryRun:
162+
_, _ = fmt.Fprintf(inv.Stdout, "Canceling template version dry run job %s...\n", job.ID)
163+
err = client.CancelTemplateVersionDryRun(ctx, ptr.NilToEmpty(job.Input.TemplateVersionID), job.ID)
164+
case codersdk.ProvisionerJobTypeTemplateVersionImport:
165+
_, _ = fmt.Fprintf(inv.Stdout, "Canceling template version import job %s...\n", job.ID)
166+
err = client.CancelTemplateVersion(ctx, ptr.NilToEmpty(job.Input.TemplateVersionID))
167+
case codersdk.ProvisionerJobTypeWorkspaceBuild:
168+
_, _ = fmt.Fprintf(inv.Stdout, "Canceling workspace build job %s...\n", job.ID)
169+
err = client.CancelWorkspaceBuild(ctx, ptr.NilToEmpty(job.Input.WorkspaceBuildID))
170+
}
171+
if err != nil {
172+
return xerrors.Errorf("cancel provisioner job: %w", err)
173+
}
174+
175+
_, _ = fmt.Fprintln(inv.Stdout, "Job canceled")
176+
177+
return nil
178+
},
179+
}
180+
181+
orgContext.AttachOptions(cmd)
182+
183+
return cmd
184+
}

cli/provisionerjobs_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"database/sql"
7+
"encoding/json"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/aws/smithy-go/ptr"
13+
"github.com/google/uuid"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/coder/coder/v2/cli/clitest"
18+
"github.com/coder/coder/v2/coderd/coderdtest"
19+
"github.com/coder/coder/v2/coderd/database"
20+
"github.com/coder/coder/v2/coderd/database/dbgen"
21+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
22+
"github.com/coder/coder/v2/coderd/rbac"
23+
"github.com/coder/coder/v2/codersdk"
24+
)
25+
26+
func TestProvisionerJobs(t *testing.T) {
27+
t.Parallel()
28+
29+
db, ps := dbtestutil.NewDB(t)
30+
client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
31+
IncludeProvisionerDaemon: false,
32+
Database: db,
33+
Pubsub: ps,
34+
})
35+
owner := coderdtest.CreateFirstUser(t, client)
36+
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
37+
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
38+
39+
// Create initial resources with a running provisioner.
40+
firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"})
41+
t.Cleanup(func() { _ = firstProvisioner.Close() })
42+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
43+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
44+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(req *codersdk.CreateTemplateRequest) {
45+
req.AllowUserCancelWorkspaceJobs = ptr.Bool(true)
46+
})
47+
48+
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
49+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
50+
51+
// Stop the provisioner so it doesn't grab any more jobs.
52+
firstProvisioner.Close()
53+
54+
t.Run("Cancel", func(t *testing.T) {
55+
t.Parallel()
56+
57+
// Set up test helpers.
58+
type jobInput struct {
59+
WorkspaceBuildID string `json:"workspace_build_id,omitempty"`
60+
TemplateVersionID string `json:"template_version_id,omitempty"`
61+
DryRun bool `json:"dry_run,omitempty"`
62+
}
63+
prepareJob := func(t *testing.T, input jobInput) database.ProvisionerJob {
64+
t.Helper()
65+
66+
inputBytes, err := json.Marshal(input)
67+
require.NoError(t, err)
68+
69+
var typ database.ProvisionerJobType
70+
switch {
71+
case input.WorkspaceBuildID != "":
72+
typ = database.ProvisionerJobTypeWorkspaceBuild
73+
case input.TemplateVersionID != "":
74+
if input.DryRun {
75+
typ = database.ProvisionerJobTypeTemplateVersionDryRun
76+
} else {
77+
typ = database.ProvisionerJobTypeTemplateVersionImport
78+
}
79+
default:
80+
t.Fatal("invalid input")
81+
}
82+
83+
var (
84+
tags = database.StringMap{"owner": "", "scope": "organization", "foo": uuid.New().String()}
85+
pd = dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{Tags: tags})
86+
job = dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
87+
WorkerID: uuid.NullUUID{UUID: pd.ID, Valid: true},
88+
Input: json.RawMessage(inputBytes),
89+
Type: typ,
90+
Tags: tags,
91+
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Minute), Valid: true},
92+
})
93+
)
94+
return job
95+
}
96+
97+
prepareWorkspaceBuildJob := func(t *testing.T) database.ProvisionerJob {
98+
t.Helper()
99+
var (
100+
wbID = uuid.New()
101+
job = prepareJob(t, jobInput{WorkspaceBuildID: wbID.String()})
102+
w = dbgen.Workspace(t, db, database.WorkspaceTable{OwnerID: member.ID, TemplateID: template.ID})
103+
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
104+
ID: wbID,
105+
WorkspaceID: w.ID,
106+
TemplateVersionID: version.ID,
107+
JobID: job.ID,
108+
})
109+
)
110+
return job
111+
}
112+
113+
prepareTemplateVersionImportJobBuilder := func(t *testing.T, dryRun bool) database.ProvisionerJob {
114+
t.Helper()
115+
var (
116+
tvID = uuid.New()
117+
job = prepareJob(t, jobInput{TemplateVersionID: tvID.String(), DryRun: dryRun})
118+
_ = dbgen.TemplateVersion(t, db, database.TemplateVersion{
119+
OrganizationID: owner.OrganizationID,
120+
CreatedBy: templateAdmin.ID,
121+
ID: tvID,
122+
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
123+
JobID: job.ID,
124+
})
125+
)
126+
return job
127+
}
128+
prepareTemplateVersionImportJob := func(t *testing.T) database.ProvisionerJob {
129+
return prepareTemplateVersionImportJobBuilder(t, false)
130+
}
131+
prepareTemplateVersionImportJobDryRun := func(t *testing.T) database.ProvisionerJob {
132+
return prepareTemplateVersionImportJobBuilder(t, true)
133+
}
134+
135+
// Run the cancellation test suite.
136+
for _, tt := range []struct {
137+
role string
138+
client *codersdk.Client
139+
name string
140+
prepare func(*testing.T) database.ProvisionerJob
141+
wantCancelled bool
142+
}{
143+
{"Owner", client, "WorkspaceBuild", prepareWorkspaceBuildJob, true},
144+
{"Owner", client, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
145+
{"Owner", client, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, true},
146+
{"TemplateAdmin", templateAdminClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
147+
{"TemplateAdmin", templateAdminClient, "TemplateVersionImport", prepareTemplateVersionImportJob, true},
148+
{"TemplateAdmin", templateAdminClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
149+
{"Member", memberClient, "WorkspaceBuild", prepareWorkspaceBuildJob, false},
150+
{"Member", memberClient, "TemplateVersionImport", prepareTemplateVersionImportJob, false},
151+
{"Member", memberClient, "TemplateVersionImportDryRun", prepareTemplateVersionImportJobDryRun, false},
152+
} {
153+
tt := tt
154+
wantMsg := "OK"
155+
if !tt.wantCancelled {
156+
wantMsg = "FAIL"
157+
}
158+
t.Run(fmt.Sprintf("%s/%s/%v", tt.role, tt.name, wantMsg), func(t *testing.T) {
159+
t.Parallel()
160+
161+
job := tt.prepare(t)
162+
require.False(t, job.CanceledAt.Valid, "job.CanceledAt.Valid")
163+
164+
inv, root := clitest.New(t, "provisioner", "jobs", "cancel", job.ID.String())
165+
clitest.SetupConfig(t, tt.client, root)
166+
var buf bytes.Buffer
167+
inv.Stdout = &buf
168+
err := inv.Run()
169+
if !tt.wantCancelled {
170+
assert.Error(t, err)
171+
} else {
172+
assert.NoError(t, err)
173+
}
174+
175+
job, err = db.GetProvisionerJobByID(context.Background(), job.ID)
176+
require.NoError(t, err)
177+
assert.Equal(t, tt.wantCancelled, job.CanceledAt.Valid, "job.CanceledAt.Valid")
178+
assert.Equal(t, tt.wantCancelled, job.CanceledAt.Time.After(job.StartedAt.Time), "job.CanceledAt.Time")
179+
if tt.wantCancelled {
180+
assert.Contains(t, buf.String(), "Job canceled")
181+
} else {
182+
assert.NotContains(t, buf.String(), "Job canceled")
183+
}
184+
})
185+
}
186+
})
187+
}

cli/testdata/coder_provisioner_jobs_--help.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ USAGE:
88
Aliases: job
99

1010
SUBCOMMANDS:
11-
list List provisioner jobs
11+
cancel Cancel a provisioner job
12+
list List provisioner jobs
1213

1314
———
1415
Run `coder --help` for a list of global options.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder provisioner jobs cancel [flags] <job_id>
5+
6+
Cancel a provisioner job
7+
8+
OPTIONS:
9+
-O, --org string, $CODER_ORGANIZATION
10+
Select which organization (uuid or name) to use.
11+
12+
———
13+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ func New(options *Options) *API {
10111011
r.Get("/", api.provisionerDaemons)
10121012
})
10131013
r.Route("/provisionerjobs", func(r chi.Router) {
1014+
r.Get("/{job}", api.provisionerJob)
10141015
r.Get("/", api.provisionerJobs)
10151016
})
10161017
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy