From 5fc39a0a69e3ec807150b3858cf0c3fd236fd658 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Fri, 16 May 2025 10:10:58 +0000 Subject: [PATCH 1/5] feat(coderd): add provisioner daemon name to provisioner jobs responses --- cli/testdata/coder_list_--output_json.golden | 1 + .../coder_provisioner_jobs_list_--help.golden | 2 +- ...provisioner_jobs_list_--output_json.golden | 2 + cli/testdata/coder_provisioner_list.golden | 4 +- ...oder_provisioner_list_--output_json.golden | 2 +- coderd/apidoc/docs.go | 3 + coderd/apidoc/swagger.json | 3 + coderd/coderdtest/coderdtest.go | 6 +- coderd/database/dbmem/dbmem.go | 16 + coderd/database/queries.sql.go | 19 +- coderd/database/queries/provisionerjobs.sql | 15 +- coderd/provisionerjobs.go | 2 + coderd/provisionerjobs_test.go | 347 +++++++++++------- coderd/templateversions_test.go | 278 ++++++++++++++ codersdk/provisionerdaemons.go | 1 + docs/reference/api/builds.md | 16 +- docs/reference/api/organizations.md | 7 +- docs/reference/api/schemas.md | 16 +- docs/reference/api/templates.md | 29 +- docs/reference/api/workspaces.md | 18 +- docs/reference/cli/provisioner_jobs_list.md | 8 +- .../coder_provisioner_jobs_list_--help.golden | 2 +- site/src/api/typesGenerated.ts | 1 + .../JobRow.tsx | 2 +- 24 files changed, 628 insertions(+), 172 deletions(-) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 5f293787de719..0ef1c6c606e06 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -37,6 +37,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_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 f744b988956e9..2fc82cb1f6302 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14582,6 +14582,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 1859a4f6f6214..3d705d3504921 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13216,6 +13216,9 @@ "worker_id": { "type": "string", "format": "uuid" + }, + "worker_name": { + "type": "string" } } }, diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 85f000939d75e..f6a8bf7c19e86 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 @@ -601,7 +603,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) + provisionerCloser = NewTaggedProvisionerDaemon(t, coderAPI, defaultTestDaemonName, options.ProvisionerDaemonTags) } client := codersdk.New(serverURL) t.Cleanup(func() { @@ -645,7 +647,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) io.Closer { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fc5a10cafc481..9d1f3fec94a78 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1267,6 +1267,14 @@ func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLockedTagBasedQueu // Step 6: Compute the final results with minimal checks var results []database.GetProvisionerJobsByIDsWithQueuePositionRow for _, job := range filteredJobs { + workerName := "" + // Add daemon name to provisioner job + for _, daemon := range q.provisionerDaemons { + if job.WorkerID.Valid && job.WorkerID.UUID == daemon.ID { + workerName = daemon.Name + } + } + // If the job has a computed rank, use it if rank, found := jobQueueStats[job.ID]; found { results = append(results, rank) @@ -1278,6 +1286,7 @@ func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLockedTagBasedQueu ProvisionerJob: job, QueuePosition: 0, QueueSize: 0, + WorkerName: workerName, }) } } @@ -4848,6 +4857,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..748c09e4e3e14 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7614,12 +7614,15 @@ SELECT fj.created_at, pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, fj.queue_position, - fj.queue_size + fj.queue_size, + COALESCE(pd.name, '') AS worker_name FROM final_jobs fj INNER JOIN provisioner_jobs pj ON fj.id = pj.id -- Ensure we retrieve full details from ` + "`" + `provisioner_jobs` + "`" + `. -- JOIN with pj is required for sqlc.embed(pj) to compile successfully. + LEFT JOIN provisioner_daemons pd -- Join to get the daemon name corresponding to the job's worker_id + ON pj.worker_id = pd.id ORDER BY fj.created_at ` @@ -7630,6 +7633,7 @@ type GetProvisionerJobsByIDsWithQueuePositionRow struct { ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"` QueuePosition int64 `db:"queue_position" json:"queue_position"` QueueSize int64 `db:"queue_size" json:"queue_size"` + WorkerName string `db:"worker_name" json:"worker_name"` } func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) { @@ -7665,6 +7669,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex &i.ProvisionerJob.JobStatus, &i.QueuePosition, &i.QueueSize, + &i.WorkerName, ); err != nil { return nil, err } @@ -7730,7 +7735,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 +7762,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 +7780,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 +7808,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 +7856,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..9d4c65c5d4ec6 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -100,12 +100,15 @@ SELECT fj.created_at, sqlc.embed(pj), fj.queue_position, - fj.queue_size + fj.queue_size, + COALESCE(pd.name, '') AS worker_name FROM final_jobs fj INNER JOIN provisioner_jobs pj ON fj.id = pj.id -- Ensure we retrieve full details from `provisioner_jobs`. -- JOIN with pj is required for sqlc.embed(pj) to compile successfully. + LEFT JOIN provisioner_daemons pd -- Join to get the daemon name corresponding to the job's worker_id + ON pj.worker_id = pd.id ORDER BY fj.created_at; @@ -160,7 +163,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 +190,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 +208,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..0b7370514abd2 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -354,6 +354,7 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR provisionerJob := pj.ProvisionerJob job := codersdk.ProvisionerJob{ ID: provisionerJob.ID, + WorkerName: pj.WorkerName, OrganizationID: provisionerJob.OrganizationID, CreatedAt: provisionerJob.CreatedAt, Type: codersdk.ProvisionerJobType(provisionerJob.Type), @@ -392,6 +393,7 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR func convertProvisionerJobWithQueuePosition(pj database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) codersdk.ProvisionerJob { job := convertProvisionerJob(database.GetProvisionerJobsByIDsWithQueuePositionRow{ ProvisionerJob: pj.ProvisionerJob, + WorkerName: pj.WorkerName, QueuePosition: pj.QueuePosition, QueueSize: pj.QueueSize, }) diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 6ec8959102fa5..be4e23953b63e 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -27,162 +27,255 @@ 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) - // Add more jobs than the default limit. - for i := range 60 { - dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + 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, - 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_IncludesWorkerIDAndName", 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_AssignedProvisionerJob_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)) + 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 + require.Equal(t, provisionerDaemons[0].ID, *job.WorkerID) + require.Equal(t, provisionerDaemonName, job.WorkerName) + } + }) + + t.Run("Get_AssignedProvisionerJob_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)) + 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 + require.Equal(t, provisionerDaemons[0].ID, *workspaceProvisionerJob.WorkerID) + require.Equal(t, provisionerDaemonName, workspaceProvisionerJob.WorkerName) + }) }) } diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index e4027a1f14605..ad6463b8992df 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -102,6 +102,48 @@ func TestTemplateVersion(t *testing.T) { assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid) } }) + + // Ensures that the template version response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + // Stop the provisioner so it doesn't grab any more jobs + err := provisionerDaemon.Close() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Get template version + templateVersion, err := client.TemplateVersion(ctx, version.ID) + require.NoError(t, err) + + // Get provisioner daemon responsible for executing the provisioner job + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, templateVersion.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(templateVersion.Job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *templateVersion.Job.WorkerID) + require.Equal(t, provisionerDaemonName, templateVersion.Job.WorkerName) + }) } func TestPostTemplateVersionsByOrganization(t *testing.T) { @@ -923,6 +965,51 @@ func TestTemplateVersionsByTemplate(t *testing.T) { require.NoError(t, err) require.Len(t, versions, 1) }) + + // Ensures that the template versions response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Stop the provisioner so it doesn't grab any more jobs + err := provisionerDaemon.Close() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + require.Len(t, versions, 1) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, versions[0].OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(versions[0].Job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *versions[0].Job.WorkerID) + require.Equal(t, provisionerDaemonName, versions[0].Job.WorkerName) + }) } func TestTemplateVersionByName(t *testing.T) { @@ -963,6 +1050,49 @@ func TestTemplateVersionByName(t *testing.T) { assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid) } }) + + // Ensures that the template version response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Stop the provisioner so it doesn't grab any more jobs + err := provisionerDaemon.Close() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Get template version by name + templateVersion, err := client.TemplateVersionByName(ctx, template.ID, version.Name) + require.NoError(t, err) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, templateVersion.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(templateVersion.Job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *templateVersion.Job.WorkerID) + require.Equal(t, provisionerDaemonName, templateVersion.Job.WorkerName) + }) } func TestPatchActiveTemplateVersion(t *testing.T) { @@ -1371,6 +1501,58 @@ func TestTemplateVersionDryRun(t *testing.T) { require.Equal(t, 0, matched.Available) require.Zero(t, matched.MostRecentlySeen.Time) }) + + // Ensures that the template version dry run response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Create template version dry-run + job, err := client.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{}) + require.NoError(t, err) + + // Wait for the job to complete + require.Eventually(t, func() bool { + job, err := client.TemplateVersionDryRun(ctx, version.ID, job.ID) + return assert.NoError(t, err) && job.Status == codersdk.ProvisionerJobSucceeded + }, testutil.WaitShort, testutil.IntervalFast) + + // Stop the provisioner so it doesn't grab any more jobs + err = provisionerDaemon.Close() + require.NoError(t, err) + + // Get the latest template version dry run + job, err = client.TemplateVersionDryRun(ctx, version.ID, job.ID) + require.NoError(t, err) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, job.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *job.WorkerID) + require.Equal(t, provisionerDaemonName, job.WorkerName) + }) } // TestPaginatedTemplateVersions creates a list of template versions and paginate. @@ -1519,6 +1701,49 @@ func TestTemplateVersionByOrganizationTemplateAndName(t *testing.T) { _, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, template.Name, version.Name) require.NoError(t, err) }) + + // Ensures that the template version response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Stop the provisioner so it doesn't grab any more jobs + err := provisionerDaemon.Close() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Get template version + templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, template.Name, version.Name) + require.NoError(t, err) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, templateVersion.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(templateVersion.Job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *templateVersion.Job.WorkerID) + require.Equal(t, provisionerDaemonName, templateVersion.Job.WorkerName) + }) } func TestPreviousTemplateVersion(t *testing.T) { @@ -1575,6 +1800,59 @@ func TestPreviousTemplateVersion(t *testing.T) { require.NoError(t, err) require.Equal(t, templateBVersion1.ID, result.ID) }) + + // Ensures that the previous template version response includes worker_id and worker_name + // when the associated provisioner job is in a succeeded state + t.Run("Get_AssignedProvisionerJob_IncludesWorkerIDAndName", func(t *testing.T) { + t.Parallel() + 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"}) + + user := coderdtest.CreateFirstUser(t, client) + + // Create two templates to be sure it is not returning a previous version + // from another template + templateAVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, templateAVersion1.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateAVersion1.ID) + // Create two versions for the template B so we can try to get the previous + // version of version 2 + templateBVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + templateB := coderdtest.CreateTemplate(t, client, user.OrganizationID, templateBVersion1.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateBVersion1.ID) + templateBVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, templateB.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateBVersion2.ID) + + // Stop the provisioner so it doesn't grab any more jobs + err := provisionerDaemon.Close() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + templateVersion, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateB.Name, templateBVersion2.Name) + require.NoError(t, err) + require.Equal(t, templateBVersion1.ID, templateVersion.ID) + + // Get provisioner daemon responsible for executing the provisioner jobs + provisionerDaemons, err := db.GetProvisionerDaemons(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(provisionerDaemons)) + require.Equal(t, provisionerDaemonName, provisionerDaemons[0].Name) + + require.Equal(t, user.OrganizationID, templateVersion.OrganizationID) + require.Equal(t, database.ProvisionerJobStatusSucceeded, database.ProvisionerJobStatus(templateVersion.Job.Status)) + + // Guarantee that provisioner jobs contain the provisioner daemon ID and name + require.Equal(t, provisionerDaemons[0].ID, *templateVersion.Job.WorkerID) + require.Equal(t, provisionerDaemonName, templateVersion.Job.WorkerName) + }) } func TestStarterTemplates(t *testing.T) { 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 a001b7210016d..fb28b4e735220 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, @@ -9202,7 +9206,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, @@ -9925,7 +9930,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 6c09014c4ed6f..23d87a956de97 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1917,6 +1917,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..165bb8fadcbfa 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -120,7 +120,7 @@ export const JobRow: FC = ({ job, defaultIsOpen = false }) => { <>
Completed by provisioner:
- {job.worker_id} + {job.worker_name} + {job.worker_name || '[removed]'} + {job.worker_name && ( + + )}
)} From 4569d2e91cdc1a3fa36e6dced4cce9204a8b0f1c Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 19 May 2025 12:38:27 +0000 Subject: [PATCH 5/5] fix: formatting issues --- .../OrganizationProvisionerJobsPage/JobRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index 2ce37fce14c1b..2073f75ca3558 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -120,7 +120,7 @@ export const JobRow: FC = ({ job, defaultIsOpen = false }) => { <>
Completed by provisioner:
- {job.worker_name || '[removed]'} + {job.worker_name || "[removed]"} {job.worker_name && (