feat(cli): add provisioner list and provisioner jobs list (#16030)

This commit is contained in:
Mathias Fredriksson
2025-01-20 19:24:22 +02:00
committed by GitHub
parent 91204c2e3c
commit c0db364f3f
34 changed files with 881 additions and 30 deletions

126
cli/provisionerjobs.go Normal file
View File

@ -0,0 +1,126 @@
package cli
import (
"fmt"
"slices"
"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"
)
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,nosort"`
OrganizationName 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
)
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: slice.StringEnums[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,
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),
},
}...)
orgContext.AttachOptions(cmd)
formatter.AttachOptions(&cmd.Options)
return cmd
}

93
cli/provisioners.go Normal file
View File

@ -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: "provisioner",
Short: "View and manage provisioner daemons and jobs",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Aliases: []string{"provisioners"},
Children: []*serpent.Command{
r.provisionerList(),
r.provisionerJobs(),
},
}
return cmd
}
func (r *RootCmd) provisionerList() *serpent.Command {
type provisionerDaemonRow struct {
codersdk.ProvisionerDaemon `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", "key name", "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{
ProvisionerDaemon: 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
}

218
cli/provisioners_test.go Normal file
View File

@ -0,0 +1,218 @@
package cli_test
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"slices"
"testing"
"time"
"github.com/google/uuid"
"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/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
)
func TestProvisioners_Golden(t *testing.T) {
t.Parallel()
// 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,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdminClient, _ := 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.NewProvisionerDaemon(t, coderdAPI)
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)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// 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: dbtime.Now().Add(1 * time.Second),
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"},
})
w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
OwnerID: member.ID,
TemplateID: template.ID,
})
wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001")
job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
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", "foo": "bar"},
})
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
ID: wb1ID,
JobID: job1.ID,
WorkspaceID: w1.ID,
TemplateVersionID: version.ID,
})
// Create a provisioner that completed a job previously and is offline.
pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
Name: "provisioner-2",
CreatedAt: dbtime.Now().Add(2 * time.Second),
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
Tags: database.StringMap{"owner": "", "scope": "organization"},
})
w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
OwnerID: member.ID,
TemplateID: template.ID,
})
wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002")
job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
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"},
})
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
ID: wb2ID,
JobID: job2.ID,
WorkspaceID: w2.ID,
TemplateVersionID: version.ID,
})
// Create a pending job.
w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
OwnerID: member.ID,
TemplateID: template.ID,
})
wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003")
job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
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,
JobID: job3.ID,
WorkspaceID: w3.ID,
TemplateVersionID: version.ID,
})
// Create a provisioner that is idle.
_ = dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
Name: "provisioner-3",
CreatedAt: dbtime.Now().Add(3 * time.Second),
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
Tags: database.StringMap{"owner": "", "scope": "organization"},
})
updateReplaceUUIDs(coderdAPI)
for id, replaceID := range replace {
t.Logf("replace[%q] = %q", id, replaceID)
}
// Test provisioners list with member as members can access
// provisioner daemons.
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,tags,key name,status,current job id,current job status,previous job id,previous job status,organization",
)
inv.Stdout = &got
clitest.SetupConfig(t, memberClient, root)
err := inv.Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})
// Test jobs list with template admin as members are currently
// unable to access provisioner jobs. In the future (with RBAC
// changes), we may allow them to view _their_ jobs.
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, templateAdminClient, root)
err := inv.Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})
}

View File

@ -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
}

View File

@ -55,6 +55,22 @@ func TestCommandHelp(t *testing.T) {
Name: "coder users list",
Cmd: []string{"users", "list"},
},
clitest.CommandHelpCase{
Name: "coder provisioner list",
Cmd: []string{"provisioner", "list"},
},
clitest.CommandHelpCase{
Name: "coder provisioner list --output json",
Cmd: []string{"provisioner", "list", "--output", "json"},
},
clitest.CommandHelpCase{
Name: "coder provisioner jobs list",
Cmd: []string{"provisioner", "jobs", "list"},
},
clitest.CommandHelpCase{
Name: "coder provisioner jobs list --output json",
Cmd: []string{"provisioner", "jobs", "list", "--output", "json"},
},
))
}

View File

@ -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 <nil> template_version_import [] Coder
00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000000 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000002 ====[timestamp]===== running 00000000-0000-0000-aaaa-000000000001 map[00000000-0000-0000-bbbb-000000000002:true foo:bar owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000001 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000003 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000002 map[00000000-0000-0000-bbbb-000000000003:true owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending <nil> map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000003 workspace_build [00000000-0000-0000-aaaa-000000000000, 00000000-0000-0000-aaaa-000000000002, 00000000-0000-0000-aaaa-000000000003] Coder 1/1

View File

@ -0,0 +1,5 @@
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 map[foo:bar owner: scope:organization] built-in busy 00000000-0000-0000-bbbb-000000000002 running <nil> <nil> Coder
00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 map[owner: scope:organization] built-in offline <nil> <nil> 00000000-0000-0000-bbbb-000000000003 succeeded Coder
00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 map[owner: scope:organization] built-in idle <nil> <nil> <nil> <nil> Coder
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel map[owner: scope:organization] built-in idle <nil> <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder

View File

@ -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".
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

View File

@ -0,0 +1,15 @@
coder v0.0.0-devel
USAGE:
coder provisioner
View and manage provisioner daemons and jobs
Aliases: provisioners
SUBCOMMANDS:
jobs View and manage provisioner jobs
list List provisioner daemons in an organization
———
Run `coder --help` for a list of global options.

View File

@ -0,0 +1,14 @@
coder v0.0.0-devel
USAGE:
coder provisioner jobs
View and manage provisioner jobs
Aliases: job
SUBCOMMANDS:
list List provisioner jobs
———
Run `coder --help` for a list of global options.

View File

@ -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

View File

@ -0,0 +1,27 @@
coder v0.0.0-devel
USAGE:
coder provisioner 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.

View File

@ -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"
}
]

View File

@ -0,0 +1,2 @@
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

View File

@ -0,0 +1,21 @@
coder v0.0.0-devel
USAGE:
coder provisioner 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|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)
Output format.
———
Run `coder --help` for a list of global options.

View File

@ -0,0 +1,27 @@
[
{
"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.2",
"provisioners": [
"echo"
],
"tags": {
"owner": "",
"scope": "organization"
},
"key_name": "built-in",
"status": "idle",
"current_job": null,
"previous_job": {
"id": "======[workspace build job ID]======",
"status": "succeeded"
},
"organization_name": "Coder"
}
]

View File

@ -4091,10 +4091,14 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition
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)
if daemon.OrganizationID == job.OrganizationID && slices.Contains(daemon.Provisioners, job.Provisioner) {
if tagsEqual(job.Tags, tagsUntagged) {
if tagsEqual(job.Tags, daemon.Tags) {
availableWorkers = append(availableWorkers, daemon)
}
} else if tagsSubset(job.Tags, daemon.Tags) {
availableWorkers = append(availableWorkers, daemon)
}
}
}
slices.SortFunc(availableWorkers, func(a, b database.ProvisionerDaemon) int {

View File

@ -156,7 +156,7 @@ func JobIsMissingParameterErrorCode(code JobErrorCode) bool {
// ProvisionerJob describes the job executed by the provisioning daemon.
type ProvisionerJob struct {
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at,default_sort"`
CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"`
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"`

View File

@ -1140,9 +1140,19 @@
},
{
"title": "provisioner",
"description": "Manage provisioner daemons",
"description": "View and manage provisioner daemons and jobs",
"path": "reference/cli/provisioner.md"
},
{
"title": "provisioner jobs",
"description": "View and manage provisioner jobs",
"path": "reference/cli/provisioner_jobs.md"
},
{
"title": "provisioner jobs list",
"description": "List provisioner jobs",
"path": "reference/cli/provisioner_jobs_list.md"
},
{
"title": "provisioner keys",
"description": "Manage provisioner keys",
@ -1163,6 +1173,11 @@
"description": "List provisioner keys in an organization",
"path": "reference/cli/provisioner_keys_list.md"
},
{
"title": "provisioner list",
"description": "List provisioner daemons in an organization",
"path": "reference/cli/provisioner_list.md"
},
{
"title": "provisioner start",
"description": "Run a provisioner daemon",

View File

@ -65,7 +65,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
| [<code>features</code>](./features.md) | List Enterprise features |
| [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses |
| [<code>groups</code>](./groups.md) | Manage groups |
| [<code>provisioner</code>](./provisioner.md) | Manage provisioner daemons |
| [<code>provisioner</code>](./provisioner.md) | View and manage provisioner daemons and jobs |
## Options

View File

@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner
Manage provisioner daemons
View and manage provisioner daemons and jobs
Aliases:
@ -15,7 +15,9 @@ coder provisioner
## Subcommands
| Name | Purpose |
|----------------------------------------------|--------------------------|
| [<code>start</code>](./provisioner_start.md) | Run a provisioner daemon |
| [<code>keys</code>](./provisioner_keys.md) | Manage provisioner keys |
| Name | Purpose |
|----------------------------------------------|---------------------------------------------|
| [<code>list</code>](./provisioner_list.md) | List provisioner daemons in an organization |
| [<code>jobs</code>](./provisioner_jobs.md) | View and manage provisioner jobs |
| [<code>start</code>](./provisioner_start.md) | Run a provisioner daemon |
| [<code>keys</code>](./provisioner_keys.md) | Manage provisioner keys |

20
docs/reference/cli/provisioner_jobs.md generated Normal file
View File

@ -0,0 +1,20 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner jobs
View and manage provisioner jobs
Aliases:
* job
## Usage
```console
coder provisioner jobs
```
## Subcommands
| Name | Purpose |
|-------------------------------------------------|-----------------------|
| [<code>list</code>](./provisioner_jobs_list.md) | List provisioner jobs |

View File

@ -0,0 +1,62 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner jobs list
List provisioner jobs
Aliases:
* ls
## Usage
```console
coder provisioner jobs list [flags]
```
## Options
### -s, --status
| | |
|-------------|----------------------------------------------------------------------------------|
| Type | <code>[pending\|running\|succeeded\|canceling\|canceled\|failed\|unknown]</code> |
| Environment | <code>$CODER_PROVISIONER_JOB_LIST_STATUS</code> |
Filter by job status.
### -l, --limit
| | |
|-------------|------------------------------------------------|
| Type | <code>int</code> |
| Environment | <code>$CODER_PROVISIONER_JOB_LIST_LIMIT</code> |
| Default | <code>50</code> |
Limit the number of jobs returned.
### -O, --org
| | |
|-------------|----------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.
### -c, --column
| | |
|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Type | <code>[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]</code> |
| Default | <code>created at,id,organization,status,type,queue,tags</code> |
Columns to display in table output.
### -o, --output
| | |
|---------|--------------------------|
| Type | <code>table\|json</code> |
| Default | <code>table</code> |
Output format.

View File

@ -23,3 +23,21 @@ coder provisioner keys list [flags]
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.
### -c, --column
| | |
|---------|---------------------------------------|
| Type | <code>[created at\|name\|tags]</code> |
| Default | <code>created at,name,tags</code> |
Columns to display in table output.
### -o, --output
| | |
|---------|--------------------------|
| Type | <code>table\|json</code> |
| Default | <code>table</code> |
Output format.

43
docs/reference/cli/provisioner_list.md generated Normal file
View File

@ -0,0 +1,43 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner list
List provisioner daemons in an organization
Aliases:
* ls
## Usage
```console
coder provisioner list [flags]
```
## Options
### -O, --org
| | |
|-------------|----------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.
### -c, --column
| | |
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Type | <code>[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]</code> |
| Default | <code>name,organization,status,key name,created at,last seen at,version,tags</code> |
Columns to display in table output.
### -o, --output
| | |
|---------|--------------------------|
| Type | <code>table\|json</code> |
| Default | <code>table</code> |
Output format.

View File

@ -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
}

View File

@ -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
},
}

View File

@ -138,8 +138,8 @@ func (r *RootCmd) provisionerKeysList() *serpent.Command {
},
}
cmd.Options = serpent.OptionSet{}
orgContext.AttachOptions(cmd)
formatter.AttachOptions(&cmd.Options)
return cmd
}

View File

@ -17,7 +17,7 @@ SUBCOMMANDS:
features List Enterprise features
groups Manage groups
licenses Add, delete, and list licenses
provisioner Manage provisioner daemons
provisioner View and manage provisioner daemons and jobs
server Start a Coder server
GLOBAL OPTIONS:

View File

@ -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
———

View File

@ -0,0 +1,14 @@
coder v0.0.0-devel
USAGE:
coder provisioner jobs
View and manage provisioner jobs
Aliases: job
SUBCOMMANDS:
list List provisioner jobs
———
Run `coder --help` for a list of global options.

View File

@ -0,0 +1,27 @@
coder v0.0.0-devel
USAGE:
coder provisioner 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.

View File

@ -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.

View File

@ -0,0 +1,21 @@
coder v0.0.0-devel
USAGE:
coder provisioner 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|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)
Output format.
———
Run `coder --help` for a list of global options.