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 { 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 return all
} }

View File

@ -55,6 +55,22 @@ func TestCommandHelp(t *testing.T) {
Name: "coder users list", Name: "coder users list",
Cmd: []string{"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 ping Ping a workspace
port-forward Forward ports from a workspace to the local machine. For port-forward Forward ports from a workspace to the local machine. For
reverse port forwarding, use "coder ssh -R". 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 publickey Output your Coder public key used for Git operations
rename Rename a workspace rename Rename a workspace
reset-password Directly connect to the database to reset a user's 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,11 +4091,15 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition
if row.QueuePosition > 0 { if row.QueuePosition > 0 {
var availableWorkers []database.ProvisionerDaemon var availableWorkers []database.ProvisionerDaemon
for _, daemon := range q.provisionerDaemons { for _, daemon := range q.provisionerDaemons {
if daemon.OrganizationID == job.OrganizationID && if daemon.OrganizationID == job.OrganizationID && slices.Contains(daemon.Provisioners, job.Provisioner) {
slices.Contains(daemon.Provisioners, job.Provisioner) && if tagsEqual(job.Tags, tagsUntagged) {
tagsSubset(job.Tags, daemon.Tags) { if tagsEqual(job.Tags, daemon.Tags) {
availableWorkers = append(availableWorkers, daemon) availableWorkers = append(availableWorkers, daemon)
} }
} else if tagsSubset(job.Tags, daemon.Tags) {
availableWorkers = append(availableWorkers, daemon)
}
}
} }
slices.SortFunc(availableWorkers, func(a, b database.ProvisionerDaemon) int { slices.SortFunc(availableWorkers, func(a, b database.ProvisionerDaemon) int {
return a.CreatedAt.Compare(b.CreatedAt) return a.CreatedAt.Compare(b.CreatedAt)

View File

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

View File

@ -1140,9 +1140,19 @@
}, },
{ {
"title": "provisioner", "title": "provisioner",
"description": "Manage provisioner daemons", "description": "View and manage provisioner daemons and jobs",
"path": "reference/cli/provisioner.md" "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", "title": "provisioner keys",
"description": "Manage provisioner keys", "description": "Manage provisioner keys",
@ -1163,6 +1173,11 @@
"description": "List provisioner keys in an organization", "description": "List provisioner keys in an organization",
"path": "reference/cli/provisioner_keys_list.md" "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", "title": "provisioner start",
"description": "Run a provisioner daemon", "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>features</code>](./features.md) | List Enterprise features |
| [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses | | [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses |
| [<code>groups</code>](./groups.md) | Manage groups | | [<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 ## Options

View File

@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT --> <!-- DO NOT EDIT | GENERATED CONTENT -->
# provisioner # provisioner
Manage provisioner daemons View and manage provisioner daemons and jobs
Aliases: Aliases:
@ -16,6 +16,8 @@ coder provisioner
## Subcommands ## Subcommands
| Name | Purpose | | 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>start</code>](./provisioner_start.md) | Run a provisioner daemon |
| [<code>keys</code>](./provisioner_keys.md) | Manage provisioner keys | | [<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> | | Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use. 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 package cli
import "github.com/coder/serpent" import (
"github.com/coder/serpent"
)
func (r *RootCmd) provisionerDaemons() *serpent.Command { func (r *RootCmd) provisionerDaemons() *serpent.Command {
cmd := &serpent.Command{ cmd := r.RootCmd.Provisioners()
Use: "provisioner", cmd.AddSubcommands(
Short: "Manage provisioner daemons",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Aliases: []string{"provisioners"},
Children: []*serpent.Command{
r.provisionerDaemonStart(), r.provisionerDaemonStart(),
r.provisionerKeys(), r.provisionerKeys(),
}, )
}
return cmd return cmd
} }

View File

@ -15,7 +15,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
RawArgs: true, RawArgs: true,
Hidden: true, Hidden: true,
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
agplcli.SlimUnsupported(inv.Stderr, "provisionerd start") agplcli.SlimUnsupported(inv.Stderr, "provisioner start")
return nil return nil
}, },
} }

View File

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

View File

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

View File

@ -3,12 +3,14 @@ coder v0.0.0-devel
USAGE: USAGE:
coder provisioner coder provisioner
Manage provisioner daemons View and manage provisioner daemons and jobs
Aliases: provisioners Aliases: provisioners
SUBCOMMANDS: SUBCOMMANDS:
jobs View and manage provisioner jobs
keys Manage provisioner keys keys Manage provisioner keys
list List provisioner daemons in an organization
start Run a provisioner daemon 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 -O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use. 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. 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.