From 0f505dec05d54f5b6c41c8cef1299c262fef2d2e Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 16 Jan 2025 13:22:51 +0200 Subject: [PATCH 1/3] feat(cli): add provisioner list and provisioner jobs list Closes #15191 Updates #15084 Supercedes #15940 --- cli/provisionerjobs.go | 120 ++++++++++ cli/provisioners.go | 93 ++++++++ cli/provisioners_test.go | 209 ++++++++++++++++++ cli/root.go | 6 +- cli/root_test.go | 16 ++ .../TestProvisioners_Golden/jobs_list.golden | 6 + .../TestProvisioners_Golden/list.golden | 5 + cli/testdata/coder_--help.golden | 1 + cli/testdata/coder_provisioner_--help.golden | 15 ++ .../coder_provisioner_jobs_--help.golden | 14 ++ .../coder_provisioner_jobs_list.golden | 3 + .../coder_provisioner_jobs_list_--help.golden | 27 +++ ...provisioner_jobs_list_--output_json.golden | 44 ++++ cli/testdata/coder_provisioner_list.golden | 2 + .../coder_provisioner_list_--help.golden | 21 ++ ...oder_provisioner_list_--output_json.golden | 27 +++ docs/manifest.json | 17 +- docs/reference/cli/index.md | 2 +- docs/reference/cli/provisioner.md | 12 +- docs/reference/cli/provisioner_jobs.md | 20 ++ docs/reference/cli/provisioner_jobs_list.md | 62 ++++++ docs/reference/cli/provisioner_keys_list.md | 18 ++ docs/reference/cli/provisioner_list.md | 43 ++++ enterprise/cli/provisionerdaemons.go | 21 +- enterprise/cli/provisionerdaemonstart_slim.go | 2 +- enterprise/cli/provisionerkeys.go | 2 +- enterprise/cli/testdata/coder_--help.golden | 2 +- .../testdata/coder_provisioner_--help.golden | 4 +- .../coder_provisioner_jobs_--help.golden | 14 ++ .../coder_provisioner_jobs_list_--help.golden | 27 +++ .../coder_provisioner_keys_list_--help.golden | 6 + .../coder_provisioner_list_--help.golden | 21 ++ 32 files changed, 857 insertions(+), 25 deletions(-) create mode 100644 cli/provisionerjobs.go create mode 100644 cli/provisioners.go create mode 100644 cli/provisioners_test.go create mode 100644 cli/testdata/TestProvisioners_Golden/jobs_list.golden create mode 100644 cli/testdata/TestProvisioners_Golden/list.golden create mode 100644 cli/testdata/coder_provisioner_--help.golden create mode 100644 cli/testdata/coder_provisioner_jobs_--help.golden create mode 100644 cli/testdata/coder_provisioner_jobs_list.golden create mode 100644 cli/testdata/coder_provisioner_jobs_list_--help.golden create mode 100644 cli/testdata/coder_provisioner_jobs_list_--output_json.golden create mode 100644 cli/testdata/coder_provisioner_list.golden create mode 100644 cli/testdata/coder_provisioner_list_--help.golden create mode 100644 cli/testdata/coder_provisioner_list_--output_json.golden create mode 100644 docs/reference/cli/provisioner_jobs.md create mode 100644 docs/reference/cli/provisioner_jobs_list.md create mode 100644 docs/reference/cli/provisioner_list.md create mode 100644 enterprise/cli/testdata/coder_provisioner_jobs_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden create mode 100644 enterprise/cli/testdata/coder_provisioner_list_--help.golden diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go new file mode 100644 index 0000000000000..7c5e05296e8f0 --- /dev/null +++ b/cli/provisionerjobs.go @@ -0,0 +1,120 @@ +package cli + +import ( + "fmt" + + "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"` + 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) + } + + 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 +} diff --git a/cli/provisioners.go b/cli/provisioners.go new file mode 100644 index 0000000000000..08d96493b87aa --- /dev/null +++ b/cli/provisioners.go @@ -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 +} diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go new file mode 100644 index 0000000000000..bb22d63f2e35b --- /dev/null +++ b/cli/provisioners_test.go @@ -0,0 +1,209 @@ +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/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) + member, memberUser := 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) + + time.Sleep(1500 * time.Millisecond) // Ensure the workspace build job has a different timestamp for sorting. + 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), + }) + w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: memberUser.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"}, + }) + 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), + }) + w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ + OwnerID: memberUser.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: memberUser.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), + }) + + updateReplaceUUIDs(coderdAPI) + + for id, replaceID := range replace { + t.Logf("replace[%q] = %q", id, replaceID) + } + + 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,api version,tags,status,current job id,previous job id,previous job status,organization", + ) + inv.Stdout = &got + clitest.SetupConfig(t, member, root) + err := inv.Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace) + }) + + 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, member, root) + err := inv.Run() + require.NoError(t, err) + + clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace) + }) +} diff --git a/cli/root.go b/cli/root.go index 3f674db6d2bb5..778cf2c24215f 100644 --- a/cli/root.go +++ b/cli/root.go @@ -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 } diff --git a/cli/root_test.go b/cli/root_test.go index 897aea18fec3e..ac1454152672e 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -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"}, + }, )) } diff --git a/cli/testdata/TestProvisioners_Golden/jobs_list.golden b/cli/testdata/TestProvisioners_Golden/jobs_list.golden new file mode 100644 index 0000000000000..bb68fb80a5605 --- /dev/null +++ b/cli/testdata/TestProvisioners_Golden/jobs_list.golden @@ -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 template_version_import [] Coder +00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 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 owner: scope:organization] 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] 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder +00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending map[owner: scope:organization] 00000000-0000-0000-dddd-000000000003 workspace_build [00000000-0000-0000-aaaa-000000000000] Coder 1/1 diff --git a/cli/testdata/TestProvisioners_Golden/list.golden b/cli/testdata/TestProvisioners_Golden/list.golden new file mode 100644 index 0000000000000..dbb3feb04c091 --- /dev/null +++ b/cli/testdata/TestProvisioners_Golden/list.golden @@ -0,0 +1,5 @@ +ID CREATED AT LAST SEEN AT NAME VERSION API VERSION TAGS STATUS CURRENT JOB ID PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION +00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 1.1 map[] busy 00000000-0000-0000-bbbb-000000000002 Coder +00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 1.1 map[] offline 00000000-0000-0000-bbbb-000000000003 succeeded Coder +00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 1.1 map[] idle Coder +00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel 1.2 map[owner: scope:organization] idle 00000000-0000-0000-bbbb-000000000001 succeeded Coder diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index c25301169002e..4e0a5e92f63b5 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -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 diff --git a/cli/testdata/coder_provisioner_--help.golden b/cli/testdata/coder_provisioner_--help.golden new file mode 100644 index 0000000000000..4f4a783dcc477 --- /dev/null +++ b/cli/testdata/coder_provisioner_--help.golden @@ -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. diff --git a/cli/testdata/coder_provisioner_jobs_--help.golden b/cli/testdata/coder_provisioner_jobs_--help.golden new file mode 100644 index 0000000000000..6442c78a03a8e --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_--help.golden @@ -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. diff --git a/cli/testdata/coder_provisioner_jobs_list.golden b/cli/testdata/coder_provisioner_jobs_list.golden new file mode 100644 index 0000000000000..b41f4fc531316 --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list.golden @@ -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 diff --git a/cli/testdata/coder_provisioner_jobs_list_--help.golden b/cli/testdata/coder_provisioner_jobs_list_--help.golden new file mode 100644 index 0000000000000..585e918c23e7b --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -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. diff --git a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden new file mode 100644 index 0000000000000..88dba1ad13d55 --- /dev/null +++ b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden @@ -0,0 +1,44 @@ +[ + { + "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" + }, + { + "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" + } +] diff --git a/cli/testdata/coder_provisioner_list.golden b/cli/testdata/coder_provisioner_list.golden new file mode 100644 index 0000000000000..056571547939e --- /dev/null +++ b/cli/testdata/coder_provisioner_list.golden @@ -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 diff --git a/cli/testdata/coder_provisioner_list_--help.golden b/cli/testdata/coder_provisioner_list_--help.golden new file mode 100644 index 0000000000000..a9943cb9da392 --- /dev/null +++ b/cli/testdata/coder_provisioner_list_--help.golden @@ -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. diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden new file mode 100644 index 0000000000000..bb8e2fd0d09c9 --- /dev/null +++ b/cli/testdata/coder_provisioner_list_--output_json.golden @@ -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" + } +] diff --git a/docs/manifest.json b/docs/manifest.json index 171dd8ac38b9c..f1f9f2808f0a4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 71a1a2b4e2c68..9ad8f5590e727 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -65,7 +65,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | [features](./features.md) | List Enterprise features | | [licenses](./licenses.md) | Add, delete, and list licenses | | [groups](./groups.md) | Manage groups | -| [provisioner](./provisioner.md) | Manage provisioner daemons | +| [provisioner](./provisioner.md) | View and manage provisioner daemons and jobs | ## Options diff --git a/docs/reference/cli/provisioner.md b/docs/reference/cli/provisioner.md index 08f4918ec1cf0..20acfd4fa5c69 100644 --- a/docs/reference/cli/provisioner.md +++ b/docs/reference/cli/provisioner.md @@ -1,7 +1,7 @@ # provisioner -Manage provisioner daemons +View and manage provisioner daemons and jobs Aliases: @@ -15,7 +15,9 @@ coder provisioner ## Subcommands -| Name | Purpose | -|----------------------------------------------|--------------------------| -| [start](./provisioner_start.md) | Run a provisioner daemon | -| [keys](./provisioner_keys.md) | Manage provisioner keys | +| Name | Purpose | +|----------------------------------------------|---------------------------------------------| +| [list](./provisioner_list.md) | List provisioner daemons in an organization | +| [jobs](./provisioner_jobs.md) | View and manage provisioner jobs | +| [start](./provisioner_start.md) | Run a provisioner daemon | +| [keys](./provisioner_keys.md) | Manage provisioner keys | diff --git a/docs/reference/cli/provisioner_jobs.md b/docs/reference/cli/provisioner_jobs.md new file mode 100644 index 0000000000000..359c5d8c0f7b5 --- /dev/null +++ b/docs/reference/cli/provisioner_jobs.md @@ -0,0 +1,20 @@ + +# provisioner jobs + +View and manage provisioner jobs + +Aliases: + +* job + +## Usage + +```console +coder provisioner jobs +``` + +## Subcommands + +| Name | Purpose | +|-------------------------------------------------|-----------------------| +| [list](./provisioner_jobs_list.md) | List provisioner jobs | diff --git a/docs/reference/cli/provisioner_jobs_list.md b/docs/reference/cli/provisioner_jobs_list.md new file mode 100644 index 0000000000000..03e187b1c6720 --- /dev/null +++ b/docs/reference/cli/provisioner_jobs_list.md @@ -0,0 +1,62 @@ + +# provisioner jobs list + +List provisioner jobs + +Aliases: + +* ls + +## Usage + +```console +coder provisioner jobs list [flags] +``` + +## Options + +### -s, --status + +| | | +|-------------|----------------------------------------------------------------------------------| +| Type | [pending\|running\|succeeded\|canceling\|canceled\|failed\|unknown] | +| Environment | $CODER_PROVISIONER_JOB_LIST_STATUS | + +Filter by job status. + +### -l, --limit + +| | | +|-------------|------------------------------------------------| +| Type | int | +| Environment | $CODER_PROVISIONER_JOB_LIST_LIMIT | +| Default | 50 | + +Limit the number of jobs returned. + +### -O, --org + +| | | +|-------------|----------------------------------| +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. + +### -c, --column + +| | | +|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Type | [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. + +### -o, --output + +| | | +|---------|--------------------------| +| Type | table\|json | +| Default | table | + +Output format. diff --git a/docs/reference/cli/provisioner_keys_list.md b/docs/reference/cli/provisioner_keys_list.md index 61e00dde759a9..4f05a5e9b5dcc 100644 --- a/docs/reference/cli/provisioner_keys_list.md +++ b/docs/reference/cli/provisioner_keys_list.md @@ -23,3 +23,21 @@ coder provisioner keys list [flags] | Environment | $CODER_ORGANIZATION | Select which organization (uuid or name) to use. + +### -c, --column + +| | | +|---------|---------------------------------------| +| Type | [created at\|name\|tags] | +| Default | created at,name,tags | + +Columns to display in table output. + +### -o, --output + +| | | +|---------|--------------------------| +| Type | table\|json | +| Default | table | + +Output format. diff --git a/docs/reference/cli/provisioner_list.md b/docs/reference/cli/provisioner_list.md new file mode 100644 index 0000000000000..11abd7dcc3d75 --- /dev/null +++ b/docs/reference/cli/provisioner_list.md @@ -0,0 +1,43 @@ + +# provisioner list + +List provisioner daemons in an organization + +Aliases: + +* ls + +## Usage + +```console +coder provisioner list [flags] +``` + +## Options + +### -O, --org + +| | | +|-------------|----------------------------------| +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. + +### -c, --column + +| | | +|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Type | [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 + +| | | +|---------|--------------------------| +| Type | table\|json | +| Default | table | + +Output format. diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 8d39723a269e3..690762dcc613b 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -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 } diff --git a/enterprise/cli/provisionerdaemonstart_slim.go b/enterprise/cli/provisionerdaemonstart_slim.go index aa399e9b9a46c..5e43393480c6d 100644 --- a/enterprise/cli/provisionerdaemonstart_slim.go +++ b/enterprise/cli/provisionerdaemonstart_slim.go @@ -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 }, } diff --git a/enterprise/cli/provisionerkeys.go b/enterprise/cli/provisionerkeys.go index 99d8bd8acf9ab..f88a5ffe851e6 100644 --- a/enterprise/cli/provisionerkeys.go +++ b/enterprise/cli/provisionerkeys.go @@ -138,8 +138,8 @@ func (r *RootCmd) provisionerKeysList() *serpent.Command { }, } - cmd.Options = serpent.OptionSet{} orgContext.AttachOptions(cmd) + formatter.AttachOptions(&cmd.Options) return cmd } diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 9b3584a3e48b2..ca5d8c8c886ef 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -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: diff --git a/enterprise/cli/testdata/coder_provisioner_--help.golden b/enterprise/cli/testdata/coder_provisioner_--help.golden index e6cd69feeceac..79c82987f1311 100644 --- a/enterprise/cli/testdata/coder_provisioner_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_--help.golden @@ -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 ——— diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden new file mode 100644 index 0000000000000..6442c78a03a8e --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_jobs_--help.golden @@ -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. diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden new file mode 100644 index 0000000000000..585e918c23e7b --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -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. diff --git a/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden index 59bddf9f71991..e7bc4c46895c3 100644 --- a/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_keys_list_--help.golden @@ -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. diff --git a/enterprise/cli/testdata/coder_provisioner_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_list_--help.golden new file mode 100644 index 0000000000000..a9943cb9da392 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisioner_list_--help.golden @@ -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. From b9fc3a47c41cdbe306090bd1cfe59c41d2648fa4 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 20 Jan 2025 15:14:58 +0000 Subject: [PATCH 2/3] fix after rebase, improve golden stability --- cli/provisionerjobs.go | 8 +++++- cli/provisioners_test.go | 27 ++++++++++++------- .../TestProvisioners_Golden/jobs_list.golden | 12 ++++----- .../TestProvisioners_Golden/list.golden | 10 +++---- ...provisioner_jobs_list_--output_json.golden | 12 ++++----- codersdk/provisionerdaemons.go | 2 +- 6 files changed, 43 insertions(+), 28 deletions(-) diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 7c5e05296e8f0..b3e0a861ba4d3 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "slices" "golang.org/x/xerrors" @@ -28,7 +29,7 @@ func (r *RootCmd) provisionerJobs() *serpent.Command { func (r *RootCmd) provisionerJobsList() *serpent.Command { type provisionerJobRow struct { - codersdk.ProvisionerJob `table:"provisioner_job,recursive_inline"` + codersdk.ProvisionerJob `table:"provisioner_job,recursive_inline,nosort"` OrganizationName string `json:"organization_name" table:"organization"` Queue string `json:"-" table:"queue"` } @@ -83,6 +84,11 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { } 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 { diff --git a/cli/provisioners_test.go b/cli/provisioners_test.go index bb22d63f2e35b..54e9f03851855 100644 --- a/cli/provisioners_test.go +++ b/cli/provisioners_test.go @@ -21,6 +21,7 @@ import ( "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" ) @@ -69,7 +70,8 @@ func TestProvisioners_Golden(t *testing.T) { Pubsub: ps, }) owner := coderdtest.CreateFirstUser(t, client) - member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + 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) @@ -78,7 +80,6 @@ func TestProvisioners_Golden(t *testing.T) { coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) - time.Sleep(1500 * time.Millisecond) // Ensure the workspace build job has a different timestamp for sorting. workspace := coderdtest.CreateWorkspace(t, client, template.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) @@ -94,9 +95,10 @@ func TestProvisioners_Golden(t *testing.T) { 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: memberUser.ID, + OwnerID: member.ID, TemplateID: template.ID, }) wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001") @@ -105,7 +107,7 @@ func TestProvisioners_Golden(t *testing.T) { 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"}, + Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"}, }) dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{ ID: wb1ID, @@ -120,9 +122,10 @@ func TestProvisioners_Golden(t *testing.T) { 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: memberUser.ID, + OwnerID: member.ID, TemplateID: template.ID, }) wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002") @@ -143,7 +146,7 @@ func TestProvisioners_Golden(t *testing.T) { // Create a pending job. w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{ - OwnerID: memberUser.ID, + OwnerID: member.ID, TemplateID: template.ID, }) wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003") @@ -164,6 +167,7 @@ func TestProvisioners_Golden(t *testing.T) { Name: "provisioner-3", CreatedAt: dbtime.Now().Add(3 * time.Second), KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn), + Tags: database.StringMap{"owner": "", "scope": "organization"}, }) updateReplaceUUIDs(coderdAPI) @@ -172,6 +176,8 @@ func TestProvisioners_Golden(t *testing.T) { 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() @@ -179,16 +185,19 @@ func TestProvisioners_Golden(t *testing.T) { inv, root := clitest.New(t, "provisioners", "list", - "--column", "id,created at,last seen at,name,version,api version,tags,status,current job id,previous job id,previous job status,organization", + "--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, member, root) + 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() @@ -200,7 +209,7 @@ func TestProvisioners_Golden(t *testing.T) { "--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, member, root) + clitest.SetupConfig(t, templateAdminClient, root) err := inv.Run() require.NoError(t, err) diff --git a/cli/testdata/TestProvisioners_Golden/jobs_list.golden b/cli/testdata/TestProvisioners_Golden/jobs_list.golden index bb68fb80a5605..3f446de71db35 100644 --- a/cli/testdata/TestProvisioners_Golden/jobs_list.golden +++ b/cli/testdata/TestProvisioners_Golden/jobs_list.golden @@ -1,6 +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 template_version_import [] Coder -00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 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 owner: scope:organization] 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] 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder -00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending map[owner: scope:organization] 00000000-0000-0000-dddd-000000000003 workspace_build [00000000-0000-0000-aaaa-000000000000] Coder 1/1 +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 template_version_import [] Coder +00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 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] 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] 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder +00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending map[owner: scope:organization] 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 diff --git a/cli/testdata/TestProvisioners_Golden/list.golden b/cli/testdata/TestProvisioners_Golden/list.golden index dbb3feb04c091..be18fcd491e3b 100644 --- a/cli/testdata/TestProvisioners_Golden/list.golden +++ b/cli/testdata/TestProvisioners_Golden/list.golden @@ -1,5 +1,5 @@ -ID CREATED AT LAST SEEN AT NAME VERSION API VERSION TAGS STATUS CURRENT JOB ID PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION -00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 1.1 map[] busy 00000000-0000-0000-bbbb-000000000002 Coder -00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 1.1 map[] offline 00000000-0000-0000-bbbb-000000000003 succeeded Coder -00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 1.1 map[] idle Coder -00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel 1.2 map[owner: scope:organization] idle 00000000-0000-0000-bbbb-000000000001 succeeded Coder +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 Coder +00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 map[owner: scope:organization] built-in offline 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 Coder +00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel map[owner: scope:organization] built-in idle 00000000-0000-0000-bbbb-000000000001 succeeded Coder diff --git a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden index 88dba1ad13d55..a19683573bba2 100644 --- a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden @@ -1,6 +1,6 @@ [ { - "id": "======[workspace build job ID]======", + "id": "==========[version job ID]==========", "created_at": "====[timestamp]=====", "started_at": "====[timestamp]=====", "completed_at": "====[timestamp]=====", @@ -15,13 +15,13 @@ "queue_size": 0, "organization_id": "===========[first org ID]===========", "input": { - "workspace_build_id": "========[workspace build ID]========" + "template_version_id": "============[version ID]============" }, - "type": "workspace_build", + "type": "template_version_import", "organization_name": "Coder" }, { - "id": "==========[version job ID]==========", + "id": "======[workspace build job ID]======", "created_at": "====[timestamp]=====", "started_at": "====[timestamp]=====", "completed_at": "====[timestamp]=====", @@ -36,9 +36,9 @@ "queue_size": 0, "organization_id": "===========[first org ID]===========", "input": { - "template_version_id": "============[version ID]============" + "workspace_build_id": "========[workspace build ID]========" }, - "type": "template_version_import", + "type": "workspace_build", "organization_name": "Coder" } ] diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 808cc14298cce..33177c52bcf6b 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -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"` From 9c2a875003e377726438751f24caccca53e0ea95 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 20 Jan 2025 16:50:58 +0000 Subject: [PATCH 3/3] fix dbmem available workers --- coderd/database/dbmem/dbmem.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 25997cafd736f..986932268e5eb 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -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 {