Files
coder/cli/provisionerjobs.go
T
Sas Swart a7c9e623fe chore: fix api param name (#20172)
In https://github.com/coder/coder/pull/20137, we added a new flag to
`coder provisioner jobs list`, namely `--initiator`.

To make some follow-up worth it, I need to rename an API param used in
the process before it becomes part of our released and tagged API.

Instead of only accepting UUIDs, we accept an arbitrary string.
We still validate it as a UUID now, but we will expand its validation to
allow any string and then resolve that string the same way that we
resolve the user parameter elsewhere in the API.
2025-10-06 13:58:37 +02:00

204 lines
5.7 KiB
Go

package cli
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"
)
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.provisionerJobsCancel(),
r.provisionerJobsList(),
},
}
return cmd
}
func (r *RootCmd) provisionerJobsList() *serpent.Command {
type provisionerJobRow struct {
codersdk.ProvisionerJob `table:"provisioner_job,recursive_inline,nosort"`
OrganizationName string `json:"organization_name" table:"organization"`
Queue string `json:"-" table:"queue"`
}
var (
orgContext = NewOrganizationContext()
formatter = cliui.NewOutputFormatter(
cliui.TableFormat([]provisionerJobRow{}, []string{"created at", "id", "type", "template display name", "status", "queue", "tags"}),
cliui.JSONFormat(),
)
status []string
limit int64
initiator string
)
cmd := &serpent.Command{
Use: "list",
Short: "List provisioner jobs",
Aliases: []string{"ls"},
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
client, err := r.InitClient(inv)
if err != nil {
return err
}
org, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("current organization: %w", err)
}
if initiator != "" {
user, err := client.User(ctx, initiator)
if err != nil {
return xerrors.Errorf("initiator not found: %s", initiator)
}
initiator = user.ID.String()
}
jobs, err := client.OrganizationProvisionerJobs(ctx, org.ID, &codersdk.OrganizationProvisionerJobsOptions{
Status: slice.StringEnums[codersdk.ProvisionerJobStatus](status),
Limit: int(limit),
Initiator: initiator,
})
if err != nil {
return xerrors.Errorf("list provisioner jobs: %w", err)
}
if len(jobs) == 0 {
_, _ = fmt.Fprintln(inv.Stdout, "No provisioner jobs found")
return nil
}
var rows []provisionerJobRow
for _, job := range jobs {
row := provisionerJobRow{
ProvisionerJob: job,
OrganizationName: org.HumanName(),
}
if job.Status == codersdk.ProvisionerJobPending {
row.Queue = fmt.Sprintf("%d/%d", job.QueuePosition, job.QueueSize)
}
rows = append(rows, row)
}
// Sort manually because the cliui table truncates timestamps and
// produces an unstable sort with timestamps that are all the same.
slices.SortStableFunc(rows, func(a provisionerJobRow, b provisionerJobRow) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
out, err := formatter.Format(ctx, rows)
if err != nil {
return xerrors.Errorf("display provisioner daemons: %w", err)
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
},
}
cmd.Options = append(cmd.Options, []serpent.Option{
{
Flag: "status",
FlagShorthand: "s",
Env: "CODER_PROVISIONER_JOB_LIST_STATUS",
Description: "Filter by job status.",
Value: serpent.EnumArrayOf(&status, slice.ToStrings(codersdk.ProvisionerJobStatusEnums())...),
},
{
Flag: "limit",
FlagShorthand: "l",
Env: "CODER_PROVISIONER_JOB_LIST_LIMIT",
Description: "Limit the number of jobs returned.",
Default: "50",
Value: serpent.Int64Of(&limit),
},
{
Flag: "initiator",
FlagShorthand: "i",
Env: "CODER_PROVISIONER_JOB_LIST_INITIATOR",
Description: "Filter by initiator (user ID or username).",
Value: serpent.StringOf(&initiator),
},
}...)
orgContext.AttachOptions(cmd)
formatter.AttachOptions(&cmd.Options)
return cmd
}
func (r *RootCmd) provisionerJobsCancel() *serpent.Command {
orgContext := NewOrganizationContext()
cmd := &serpent.Command{
Use: "cancel <job_id>",
Short: "Cancel a provisioner job",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
client, err := r.InitClient(inv)
if err != nil {
return err
}
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), codersdk.CancelWorkspaceBuildParams{})
}
if err != nil {
return xerrors.Errorf("cancel provisioner job: %w", err)
}
_, _ = fmt.Fprintln(inv.Stdout, "Job canceled")
return nil
},
}
orgContext.AttachOptions(cmd)
return cmd
}