diff --git a/cli/testdata/coder_provisioner_jobs_list_--help.golden b/cli/testdata/coder_provisioner_jobs_list_--help.golden index 7a72605f0c288..f380a0334867c 100644 --- a/cli/testdata/coder_provisioner_jobs_list_--help.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -11,7 +11,7 @@ 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|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,type,template display name,status,queue,tags) + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|worker name|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,type,template display name,status,queue,tags) Columns to display in table output. -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) diff --git a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden index d18e07121f653..e36723765b4df 100644 --- a/cli/testdata/coder_provisioner_jobs_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_jobs_list_--output_json.golden @@ -6,6 +6,7 @@ "completed_at": "====[timestamp]=====", "status": "succeeded", "worker_id": "====[workspace build worker ID]=====", + "worker_name": "test-daemon", "file_id": "=====[workspace build file ID]======", "tags": { "owner": "", @@ -34,6 +35,7 @@ "completed_at": "====[timestamp]=====", "status": "succeeded", "worker_id": "====[workspace build worker ID]=====", + "worker_name": "test-daemon", "file_id": "=====[workspace build file ID]======", "tags": { "owner": "", diff --git a/cli/testdata/coder_provisioner_list.golden b/cli/testdata/coder_provisioner_list.golden index 64941eebf5b89..92ac6e485e68f 100644 --- a/cli/testdata/coder_provisioner_list.golden +++ b/cli/testdata/coder_provisioner_list.golden @@ -1,2 +1,2 @@ -CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS -====[timestamp]===== ====[timestamp]===== built-in test v0.0.0-devel idle map[owner: scope:organization] +CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS +====[timestamp]===== ====[timestamp]===== built-in test-daemon v0.0.0-devel idle map[owner: scope:organization] diff --git a/cli/testdata/coder_provisioner_list_--output_json.golden b/cli/testdata/coder_provisioner_list_--output_json.golden index e8b3637bdffa6..73dd35ff84266 100644 --- a/cli/testdata/coder_provisioner_list_--output_json.golden +++ b/cli/testdata/coder_provisioner_list_--output_json.golden @@ -5,7 +5,7 @@ "key_id": "00000000-0000-0000-0000-000000000001", "created_at": "====[timestamp]=====", "last_seen_at": "====[timestamp]=====", - "name": "test", + "name": "test-daemon", "version": "v0.0.0-devel", "api_version": "1.6", "provisioners": [ diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d55582afbbe8b..075f33aeac02f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14618,6 +14618,9 @@ const docTemplate = `{ "worker_id": { "type": "string", "format": "uuid" + }, + "worker_name": { + "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 00f940737a1d6..e00ab22232483 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13250,6 +13250,9 @@ "worker_id": { "type": "string", "format": "uuid" + }, + "worker_name": { + "type": "string" } } }, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index a25f0576e76be..b395a2cf2afbe 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -96,6 +96,8 @@ import ( "github.com/coder/coder/v2/testutil" ) +const defaultTestDaemonName = "test-daemon" + type Options struct { // AccessURL denotes a custom access URL. By default we use the httptest // server's URL. Setting this may result in unexpected behavior (especially @@ -602,7 +604,7 @@ func NewWithAPI(t testing.TB, options *Options) (*codersdk.Client, io.Closer, *c setHandler(rootHandler) var provisionerCloser io.Closer = nopcloser{} if options.IncludeProvisionerDaemon { - provisionerCloser = NewTaggedProvisionerDaemon(t, coderAPI, "test", options.ProvisionerDaemonTags, coderd.MemoryProvisionerWithVersionOverride(options.ProvisionerDaemonVersion)) + provisionerCloser = NewTaggedProvisionerDaemon(t, coderAPI, defaultTestDaemonName, options.ProvisionerDaemonTags, coderd.MemoryProvisionerWithVersionOverride(options.ProvisionerDaemonVersion)) } client := codersdk.New(serverURL) t.Cleanup(func() { @@ -646,7 +648,7 @@ func (c *ProvisionerdCloser) Close() error { // well with coderd testing. It registers the "echo" provisioner for // quick testing. func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer { - return NewTaggedProvisionerDaemon(t, coderAPI, "test", nil) + return NewTaggedProvisionerDaemon(t, coderAPI, defaultTestDaemonName, nil) } func NewTaggedProvisionerDaemon(t testing.TB, coderAPI *coderd.API, name string, provisionerTags map[string]string, opts ...coderd.MemoryProvisionerDaemonOption) io.Closer { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fc5a10cafc481..7dec84f8aaeb0 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -4848,6 +4848,13 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition row.AvailableWorkers = append(row.AvailableWorkers, worker.ID) } } + + // Add daemon name to provisioner job + for _, daemon := range q.provisionerDaemons { + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID { + row.WorkerName = daemon.Name + } + } rows = append(rows, row) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ac08d72d0e493..fdb9252bf27ee 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7730,7 +7730,9 @@ SELECT COALESCE(t.display_name, '') AS template_display_name, COALESCE(t.icon, '') AS template_icon, w.id AS workspace_id, - COALESCE(w.name, '') AS workspace_name + COALESCE(w.name, '') AS workspace_name, + -- Include the name of the provisioner_daemon associated to the job + COALESCE(pd.name, '') AS worker_name FROM provisioner_jobs pj LEFT JOIN @@ -7755,6 +7757,9 @@ LEFT JOIN t.id = tv.template_id AND t.organization_id = pj.organization_id ) +LEFT JOIN + -- Join to get the daemon name corresponding to the job's worker_id + provisioner_daemons pd ON pd.id = pj.worker_id WHERE pj.organization_id = $1::uuid AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[])) @@ -7770,7 +7775,8 @@ GROUP BY t.display_name, t.icon, w.id, - w.name + w.name, + pd.name ORDER BY pj.created_at DESC LIMIT @@ -7797,6 +7803,7 @@ type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow TemplateIcon string `db:"template_icon" json:"template_icon"` WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` WorkspaceName string `db:"workspace_name" json:"workspace_name"` + WorkerName string `db:"worker_name" json:"worker_name"` } func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) { @@ -7844,6 +7851,7 @@ func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionA &i.TemplateIcon, &i.WorkspaceID, &i.WorkspaceName, + &i.WorkerName, ); err != nil { return nil, err } diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index 2d544aedb9bd8..2ab7774e660b8 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -160,7 +160,9 @@ SELECT COALESCE(t.display_name, '') AS template_display_name, COALESCE(t.icon, '') AS template_icon, w.id AS workspace_id, - COALESCE(w.name, '') AS workspace_name + COALESCE(w.name, '') AS workspace_name, + -- Include the name of the provisioner_daemon associated to the job + COALESCE(pd.name, '') AS worker_name FROM provisioner_jobs pj LEFT JOIN @@ -185,6 +187,9 @@ LEFT JOIN t.id = tv.template_id AND t.organization_id = pj.organization_id ) +LEFT JOIN + -- Join to get the daemon name corresponding to the job's worker_id + provisioner_daemons pd ON pd.id = pj.worker_id WHERE pj.organization_id = @organization_id::uuid AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pj.id = ANY(@ids::uuid[])) @@ -200,7 +205,8 @@ GROUP BY t.display_name, t.icon, w.id, - w.name + w.name, + pd.name ORDER BY pj.created_at DESC LIMIT diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 6d75227a14ccd..5a8a0a5126cc0 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -395,6 +395,7 @@ func convertProvisionerJobWithQueuePosition(pj database.GetProvisionerJobsByOrga QueuePosition: pj.QueuePosition, QueueSize: pj.QueueSize, }) + job.WorkerName = pj.WorkerName job.AvailableWorkers = pj.AvailableWorkers job.Metadata = codersdk.ProvisionerJobMetadata{ TemplateVersionName: pj.TemplateVersionName, diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 6ec8959102fa5..98da3ae5584e6 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -27,162 +27,263 @@ import ( func TestProvisionerJobs(t *testing.T) { t.Parallel() - db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) - client := coderdtest.New(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - 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) - - version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - 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) - - // Create a pending job. - w := dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - TemplateID: template.ID, - }) - wbID := uuid.New() - job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - OrganizationID: w.OrganizationID, - StartedAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: json.RawMessage(`{"workspace_build_id":"` + wbID.String() + `"}`), - }) - dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - ID: wbID, - JobID: job.ID, - WorkspaceID: w.ID, - TemplateVersionID: version.ID, - }) + t.Run("ProvisionerJobs", func(t *testing.T) { + db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + 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) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + 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) - // Add more jobs than the default limit. - for i := range 60 { - dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + // Create a pending job. + w := dbgen.Workspace(t, db, database.WorkspaceTable{ OrganizationID: owner.OrganizationID, - Tags: database.StringMap{"count": strconv.Itoa(i)}, + OwnerID: member.ID, + TemplateID: template.ID, + }) + wbID := uuid.New() + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + OrganizationID: w.OrganizationID, + StartedAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: json.RawMessage(`{"workspace_build_id":"` + wbID.String() + `"}`), + }) + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + ID: wbID, + JobID: job.ID, + WorkspaceID: w.ID, + TemplateVersionID: version.ID, }) - } - t.Run("Single", func(t *testing.T) { - t.Parallel() - t.Run("Workspace", func(t *testing.T) { + // Add more jobs than the default limit. + for i := range 60 { + dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + OrganizationID: owner.OrganizationID, + Tags: database.StringMap{"count": strconv.Itoa(i)}, + }) + } + + t.Run("Single", func(t *testing.T) { t.Parallel() - t.Run("OK", func(t *testing.T) { + t.Run("Workspace", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - // Note this calls the single job endpoint. - job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID) - require.NoError(t, err) - require.Equal(t, job.ID, job2.ID) - - // Verify that job metadata is correct. - assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ - TemplateVersionName: version.Name, - TemplateID: template.ID, - TemplateName: template.Name, - TemplateDisplayName: template.DisplayName, - TemplateIcon: template.Icon, - WorkspaceID: &w.ID, - WorkspaceName: w.Name, + t.Run("OK", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + // Note this calls the single job endpoint. + job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, job.ID) + require.NoError(t, err) + require.Equal(t, job.ID, job2.ID) + + // Verify that job metadata is correct. + assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ + TemplateVersionName: version.Name, + TemplateID: template.ID, + TemplateName: template.Name, + TemplateDisplayName: template.DisplayName, + TemplateIcon: template.Icon, + WorkspaceID: &w.ID, + WorkspaceName: w.Name, + }) }) }) - }) - t.Run("Template Import", func(t *testing.T) { - t.Parallel() - t.Run("OK", func(t *testing.T) { + t.Run("Template Import", func(t *testing.T) { + t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + // Note this calls the single job endpoint. + job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, version.Job.ID) + require.NoError(t, err) + require.Equal(t, version.Job.ID, job2.ID) + + // Verify that job metadata is correct. + assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ + TemplateVersionName: version.Name, + TemplateID: template.ID, + TemplateName: template.Name, + TemplateDisplayName: template.DisplayName, + TemplateIcon: template.Icon, + }) + }) + }) + t.Run("Missing", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) // Note this calls the single job endpoint. - job2, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, version.Job.ID) - require.NoError(t, err) - require.Equal(t, version.Job.ID, job2.ID) - - // Verify that job metadata is correct. - assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{ - TemplateVersionName: version.Name, - TemplateID: template.ID, - TemplateName: template.Name, - TemplateDisplayName: template.DisplayName, - TemplateIcon: template.Icon, - }) + _, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, uuid.New()) + require.Error(t, err) }) }) - t.Run("Missing", func(t *testing.T) { + + t.Run("Default limit", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) - // Note this calls the single job endpoint. - _, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, uuid.New()) - require.Error(t, err) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.NoError(t, err) + require.Len(t, jobs, 50) }) - }) - t.Run("Default limit", func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) - require.NoError(t, err) - require.Len(t, jobs, 50) - }) + t.Run("IDs", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + IDs: []uuid.UUID{workspace.LatestBuild.Job.ID, version.Job.ID}, + }) + require.NoError(t, err) + require.Len(t, jobs, 2) + }) - t.Run("IDs", func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ - IDs: []uuid.UUID{workspace.LatestBuild.Job.ID, version.Job.ID}, + t.Run("Status", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Status: []codersdk.ProvisionerJobStatus{codersdk.ProvisionerJobRunning}, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) }) - require.NoError(t, err) - require.Len(t, jobs, 2) - }) - t.Run("Status", func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ - Status: []codersdk.ProvisionerJobStatus{codersdk.ProvisionerJobRunning}, + t.Run("Tags", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Tags: map[string]string{"count": "1"}, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) }) - require.NoError(t, err) - require.Len(t, jobs, 1) - }) - t.Run("Tags", func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ - Tags: map[string]string{"count": "1"}, + t.Run("Limit", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Limit: 1, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) + }) + + // For now, this is not allowed even though the member has created a + // workspace. Once member-level permissions for jobs are supported + // by RBAC, this test should be updated. + t.Run("MemberDenied", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := memberClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.Error(t, err) + require.Len(t, jobs, 0) }) - require.NoError(t, err) - require.Len(t, jobs, 1) }) - t.Run("Limit", func(t *testing.T) { + // Ensures that when a provisioner job is in the succeeded state, + // the API response includes both worker_id and worker_name fields + t.Run("AssignedProvisionerJob", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ - Limit: 1, + + db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{ + IncludeProvisionerDaemon: false, + Database: db, + Pubsub: ps, }) + provisionerDaemonName := "provisioner_daemon_test" + provisionerDaemon := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, provisionerDaemonName, map[string]string{"owner": "", "scope": "organization"}) + owner := coderdtest.CreateFirstUser(t, client) + templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + 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 + err := provisionerDaemon.Close() require.NoError(t, err) - require.Len(t, jobs, 1) - }) - // For now, this is not allowed even though the member has created a - // workspace. Once member-level permissions for jobs are supported - // by RBAC, this test should be updated. - t.Run("MemberDenied", func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - jobs, err := memberClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) - require.Error(t, err) - require.Len(t, jobs, 0) + t.Run("List_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + if assert.NotEmpty(t, provisionerDaemons) { + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + } + + // Get provisioner jobs + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.NoError(t, err) + require.Equal(t, 2, len(jobs)) + + for _, job := range jobs { + require.Equal(t, owner.OrganizationID, job.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + if assert.NotEmpty(t, provisionerDaemons) { + require.Equal(t, &provisionerDaemons[0].ID, job.WorkerID) + require.Equal(t, provisionerDaemonName, job.WorkerName) + } + } + }) + + t.Run("Get_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + + // Get provisioner daemon responsible for executing the provisioner job + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + if assert.NotEmpty(t, provisionerDaemons) { + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + } + + // Get all provisioner jobs + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, nil) + require.NoError(t, err) + require.Equal(t, 2, len(jobs)) + + // Find workspace_build provisioner job ID + var workspaceProvisionerJobID uuid.UUID + for _, job := range jobs { + if job.Type == codersdk.ProvisionerJobTypeWorkspaceBuild { + workspaceProvisionerJobID = job.ID + } + } + require.NotNil(t, workspaceProvisionerJobID) + + // Get workspace_build provisioner job by ID + workspaceProvisionerJob, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, workspaceProvisionerJobID) + require.NoError(t, err) + + require.Equal(t, owner.OrganizationID, workspaceProvisionerJob.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(workspaceProvisionerJob.Status)) + + // Guarantee that provisioner job contains the provisioner daemon ID and name + if assert.NotEmpty(t, provisionerDaemons) { + require.Equal(t, &provisionerDaemons[0].ID, workspaceProvisionerJob.WorkerID) + require.Equal(t, provisionerDaemonName, workspaceProvisionerJob.WorkerName) + } + }) }) } diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 11345a115e07f..5fbda371b8f3f 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -178,6 +178,7 @@ type ProvisionerJob struct { ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES" table:"error code"` Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed" table:"status"` WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid" table:"worker id"` + WorkerName string `json:"worker_name,omitempty" table:"worker name"` FileID uuid.UUID `json:"file_id" format:"uuid" table:"file id"` Tags map[string]string `json:"tags" table:"tags"` QueuePosition int `json:"queue_position" table:"queue position"` diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 8e88df96c1d29..00417c700cdfd 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -69,7 +69,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -302,7 +303,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1012,7 +1014,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1318,7 +1321,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1527,6 +1531,7 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | | `»» worker_id` | string(uuid) | false | | | +| `»» worker_name` | string | false | | | | `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | | `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | | `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | @@ -1798,7 +1803,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, diff --git a/docs/reference/api/organizations.md b/docs/reference/api/organizations.md index 8c49f33e31ce3..497e3f56d4e47 100644 --- a/docs/reference/api/organizations.md +++ b/docs/reference/api/organizations.md @@ -426,7 +426,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" } ] ``` @@ -473,6 +474,7 @@ Status Code **200** | `»» [any property]` | string | false | | | | `» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | | `» worker_id` | string(uuid) | false | | | +| `» worker_name` | string | false | | | #### Enumerated Values @@ -551,7 +553,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" } ``` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index aa704b0fe6a57..91f70950e989e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -5518,7 +5518,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" } ``` @@ -5545,6 +5546,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | string | false | | | | `type` | [codersdk.ProvisionerJobType](#codersdkprovisionerjobtype) | false | | | | `worker_id` | string | false | | | +| `worker_name` | string | false | | | #### Enumerated Values @@ -7101,7 +7103,8 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -8241,7 +8244,8 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -9205,7 +9209,8 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -9928,7 +9933,8 @@ If the schedule is empty, the user will be updated to use the default schedule.| "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index c662118868656..09fc555c7d39c 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -489,7 +489,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -586,7 +587,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -707,7 +709,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1264,7 +1267,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1334,6 +1338,7 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | | `»» worker_id` | string(uuid) | false | | | +| `»» worker_name` | string | false | | | | `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | | `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | | `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | @@ -1541,7 +1546,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1611,6 +1617,7 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | | `»» worker_id` | string(uuid) | false | | | +| `»» worker_name` | string | false | | | | `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | | `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | | `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | @@ -1708,7 +1715,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1814,7 +1822,8 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion} "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -2010,7 +2019,8 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" } ``` @@ -2082,7 +2092,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" } ``` diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 8e25cd0bd58e6..49377ec14c6fd 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -124,7 +124,8 @@ of the template will be used. "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -405,7 +406,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -712,7 +714,8 @@ of the template will be used. "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -996,7 +999,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1261,7 +1265,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, @@ -1658,7 +1663,8 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "property2": "string" }, "type": "template_version_import", - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b", + "worker_name": "string" }, "matched_provisioners": { "available": 0, diff --git a/docs/reference/cli/provisioner_jobs_list.md b/docs/reference/cli/provisioner_jobs_list.md index a7f2fa74384d2..07ad02f419bde 100644 --- a/docs/reference/cli/provisioner_jobs_list.md +++ b/docs/reference/cli/provisioner_jobs_list.md @@ -45,10 +45,10 @@ 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\|template version name\|template id\|template name\|template display name\|template icon\|workspace id\|workspace name\|organization\|queue] | -| Default | created at,id,type,template display name,status,queue,tags | +| | | +|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Type | [id\|created at\|started at\|completed at\|canceled at\|error\|error code\|status\|worker id\|worker name\|file id\|tags\|queue position\|queue size\|organization id\|template version id\|workspace build id\|type\|available workers\|template version name\|template id\|template name\|template display name\|template icon\|workspace id\|workspace name\|organization\|queue] | +| Default | created at,id,type,template display name,status,queue,tags | Columns to display in table output. diff --git a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden index 7a72605f0c288..f380a0334867c 100644 --- a/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden +++ b/enterprise/cli/testdata/coder_provisioner_jobs_list_--help.golden @@ -11,7 +11,7 @@ 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|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,type,template display name,status,queue,tags) + -c, --column [id|created at|started at|completed at|canceled at|error|error code|status|worker id|worker name|file id|tags|queue position|queue size|organization id|template version id|workspace build id|type|available workers|template version name|template id|template name|template display name|template icon|workspace id|workspace name|organization|queue] (default: created at,id,type,template display name,status,queue,tags) Columns to display in table output. -l, --limit int, $CODER_PROVISIONER_JOB_LIST_LIMIT (default: 50) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 897e5f9012a4f..68cf0940ad8e1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1925,6 +1925,7 @@ export interface ProvisionerJob { readonly error_code?: JobErrorCode; readonly status: ProvisionerJobStatus; readonly worker_id?: string; + readonly worker_name?: string; readonly file_id: string; readonly tags: Record; readonly queue_position: number; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index e97749db3d6f4..2073f75ca3558 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -120,14 +120,16 @@ export const JobRow: FC = ({ job, defaultIsOpen = false }) => { <>
Completed by provisioner:
- {job.worker_id} - + {job.worker_name || "[removed]"} + {job.worker_name && ( + + )}
)}