mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
# Description This PR adds the `worker_name` field to the provisioner jobs endpoint. To achieve this, the following SQL query was updated: - `GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner` As a result, the `codersdk.ProvisionerJob` type, which represents the provisioner job API response, was modified to include the new field. **Notes:** * As mentioned in [comment](https://github.com/coder/coder/pull/17877#discussion_r2093218206), the `GetProvisionerJobsByIDsWithQueuePosition` query was not changed due to load concerns. This means that for template and template version endpoints, `worker_id` will still be returned, but `worker_name` will not. * Similar to `worker_id`, the `worker_name` is only present once a job is assigned to a provisioner daemon. For jobs in a pending state (not yet assigned), neither `worker_id` nor `worker_name` will be returned. --- # Affected Endpoints - `/organizations/{organization}/provisionerjobs` - `/organizations/{organization}/provisionerjobs/{job}` --- # Testing - Added new tests verifying that both `worker_id` and `worker_name` are returned once a provisioner job reaches the **succeeded** state. - Existing tests covering state transitions and other logic remain unchanged, as they test different scenarios. --- # Front-end Changes Admin provisioner jobs dashboard: <img width="1088" alt="Screenshot 2025-05-16 at 11 51 33" src="https://github.com/user-attachments/assets/0e20e360-c615-4497-84b7-693777c5443e" /> Fixes: https://github.com/coder/coder/issues/16982
368 lines
13 KiB
Go
368 lines
13 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestProvisionerJobs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
|
|
// 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,
|
|
})
|
|
|
|
// 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("Workspace", 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, 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.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.
|
|
_, err := templateAdminClient.OrganizationProvisionerJob(ctx, owner.OrganizationID, uuid.New())
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
|
|
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("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)
|
|
})
|
|
|
|
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)
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
// 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()
|
|
|
|
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)
|
|
|
|
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)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestProvisionerJobLogs(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("StreamAfterComplete", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_INFO,
|
|
Output: "log-output",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, 0)
|
|
require.NoError(t, err)
|
|
defer closer.Close()
|
|
for {
|
|
log, ok := <-logs
|
|
t.Logf("got log: [%s] %s %s", log.Level, log.Stage, log.Output)
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("StreamWhileRunning", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_INFO,
|
|
Output: "log-output",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, 0)
|
|
require.NoError(t, err)
|
|
defer closer.Close()
|
|
for {
|
|
_, ok := <-logs
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|