mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
255 lines
10 KiB
Go
255 lines
10 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"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/testutil"
|
|
)
|
|
|
|
func TestProvisionerDaemons(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, ps := dbtestutil.NewDB(t,
|
|
dbtestutil.WithDumpOnFailure(),
|
|
//nolint:gocritic // Use UTC for consistent timestamp length in golden files.
|
|
dbtestutil.WithTimezone("UTC"),
|
|
)
|
|
client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: false,
|
|
Database: db,
|
|
Pubsub: ps,
|
|
})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Create initial resources with a running provisioner.
|
|
firstProvisioner := coderdtest.NewTaggedProvisionerDaemon(t, coderdAPI, "default-provisioner", map[string]string{"owner": "", "scope": "organization"})
|
|
t.Cleanup(func() { _ = firstProvisioner.Close() })
|
|
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.
|
|
firstProvisioner.Close()
|
|
|
|
// Create a provisioner that's working on a job.
|
|
pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
|
Name: "provisioner-1",
|
|
CreatedAt: dbtime.Now().Add(1 * time.Second),
|
|
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true}, // Stale interval can't be adjusted, keep online.
|
|
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
|
Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"},
|
|
})
|
|
w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
TemplateID: template.ID,
|
|
})
|
|
wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001")
|
|
job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
|
WorkerID: uuid.NullUUID{UUID: pd1.ID, Valid: true},
|
|
Input: json.RawMessage(`{"workspace_build_id":"` + wb1ID.String() + `"}`),
|
|
CreatedAt: dbtime.Now().Add(2 * time.Second),
|
|
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now(), Valid: true},
|
|
Tags: database.StringMap{"owner": "", "scope": "organization", "foo": "bar"},
|
|
})
|
|
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
|
ID: wb1ID,
|
|
JobID: job1.ID,
|
|
WorkspaceID: w1.ID,
|
|
TemplateVersionID: version.ID,
|
|
})
|
|
|
|
// Create a provisioner that completed a job previously and is offline.
|
|
pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
|
Name: "provisioner-2",
|
|
CreatedAt: dbtime.Now().Add(2 * time.Second),
|
|
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
|
|
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
|
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
|
})
|
|
w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
TemplateID: template.ID,
|
|
})
|
|
wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002")
|
|
job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
|
WorkerID: uuid.NullUUID{UUID: pd2.ID, Valid: true},
|
|
Input: json.RawMessage(`{"workspace_build_id":"` + wb2ID.String() + `"}`),
|
|
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
|
StartedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-2 * time.Hour), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
|
|
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
|
})
|
|
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
|
ID: wb2ID,
|
|
JobID: job2.ID,
|
|
WorkspaceID: w2.ID,
|
|
TemplateVersionID: version.ID,
|
|
})
|
|
|
|
// Create a pending job.
|
|
w3 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
TemplateID: template.ID,
|
|
})
|
|
wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003")
|
|
job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
|
|
Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`),
|
|
CreatedAt: dbtime.Now().Add(4 * time.Second),
|
|
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
|
})
|
|
dbgen.WorkspaceBuild(t, coderdAPI.Database, database.WorkspaceBuild{
|
|
ID: wb3ID,
|
|
JobID: job3.ID,
|
|
WorkspaceID: w3.ID,
|
|
TemplateVersionID: version.ID,
|
|
})
|
|
|
|
// Create a provisioner that is idle.
|
|
pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
|
Name: "provisioner-3",
|
|
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
|
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(time.Hour), Valid: true},
|
|
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
|
|
Tags: database.StringMap{"owner": "", "scope": "organization"},
|
|
})
|
|
|
|
// Add more provisioners than the default limit.
|
|
var userDaemons []database.ProvisionerDaemon
|
|
for i := range 50 {
|
|
userDaemons = append(userDaemons, dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
|
|
Name: "user-provisioner-" + strconv.Itoa(i),
|
|
CreatedAt: dbtime.Now().Add(3 * time.Second),
|
|
KeyID: codersdk.ProvisionerKeyUUIDUserAuth,
|
|
Tags: database.StringMap{"count": strconv.Itoa(i)},
|
|
}))
|
|
}
|
|
|
|
t.Run("Default limit", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 50)
|
|
})
|
|
|
|
t.Run("IDs", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
IDs: []uuid.UUID{pd1.ID, pd2.ID},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 2)
|
|
require.Equal(t, pd1.ID, daemons[1].ID)
|
|
require.Equal(t, pd2.ID, daemons[0].ID)
|
|
})
|
|
|
|
t.Run("Tags", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
Tags: map[string]string{"count": "1"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 1)
|
|
require.Equal(t, userDaemons[1].ID, daemons[0].ID)
|
|
})
|
|
|
|
t.Run("Limit", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
Limit: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 1)
|
|
})
|
|
|
|
t.Run("Busy", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
IDs: []uuid.UUID{pd1.ID},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 1)
|
|
// Verify status.
|
|
require.NotNil(t, daemons[0].Status)
|
|
require.Equal(t, codersdk.ProvisionerDaemonBusy, *daemons[0].Status)
|
|
require.NotNil(t, daemons[0].CurrentJob)
|
|
require.Nil(t, daemons[0].PreviousJob)
|
|
// Verify job.
|
|
require.Equal(t, job1.ID, daemons[0].CurrentJob.ID)
|
|
require.Equal(t, codersdk.ProvisionerJobRunning, daemons[0].CurrentJob.Status)
|
|
require.Equal(t, template.Name, daemons[0].CurrentJob.TemplateName)
|
|
require.Equal(t, template.DisplayName, daemons[0].CurrentJob.TemplateDisplayName)
|
|
require.Equal(t, template.Icon, daemons[0].CurrentJob.TemplateIcon)
|
|
})
|
|
|
|
t.Run("Offline", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
IDs: []uuid.UUID{pd2.ID},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 1)
|
|
// Verify status.
|
|
require.NotNil(t, daemons[0].Status)
|
|
require.Equal(t, codersdk.ProvisionerDaemonOffline, *daemons[0].Status)
|
|
require.Nil(t, daemons[0].CurrentJob)
|
|
require.NotNil(t, daemons[0].PreviousJob)
|
|
// Verify job.
|
|
require.Equal(t, job2.ID, daemons[0].PreviousJob.ID)
|
|
require.Equal(t, codersdk.ProvisionerJobSucceeded, daemons[0].PreviousJob.Status)
|
|
require.Equal(t, template.Name, daemons[0].PreviousJob.TemplateName)
|
|
require.Equal(t, template.DisplayName, daemons[0].PreviousJob.TemplateDisplayName)
|
|
require.Equal(t, template.Icon, daemons[0].PreviousJob.TemplateIcon)
|
|
})
|
|
|
|
t.Run("Idle", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
daemons, err := templateAdminClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerDaemonsOptions{
|
|
IDs: []uuid.UUID{pd3.ID},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, daemons, 1)
|
|
// Verify status.
|
|
require.NotNil(t, daemons[0].Status)
|
|
require.Equal(t, codersdk.ProvisionerDaemonIdle, *daemons[0].Status)
|
|
require.Nil(t, daemons[0].CurrentJob)
|
|
require.Nil(t, daemons[0].PreviousJob)
|
|
})
|
|
|
|
// 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)
|
|
daemons, err := memberClient.OrganizationProvisionerDaemons(ctx, owner.OrganizationID, nil)
|
|
require.Error(t, err)
|
|
require.Len(t, daemons, 0)
|
|
})
|
|
}
|