Files
coder/cli/provisioners_test.go
2025-02-17 14:34:47 +02:00

222 lines
8.4 KiB
Go

package cli_test
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"slices"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
)
func TestProvisioners_Golden(t *testing.T) {
t.Parallel()
// Replace UUIDs with predictable values for golden files.
replace := make(map[string]string)
updateReplaceUUIDs := func(coderdAPI *coderd.API) {
//nolint:gocritic // This is a test.
systemCtx := dbauthz.AsSystemRestricted(context.Background())
provisioners, err := coderdAPI.Database.GetProvisionerDaemons(systemCtx)
require.NoError(t, err)
slices.SortFunc(provisioners, func(a, b database.ProvisionerDaemon) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
pIdx := 0
for _, p := range provisioners {
if _, ok := replace[p.ID.String()]; !ok {
replace[p.ID.String()] = fmt.Sprintf("00000000-0000-0000-aaaa-%012d", pIdx)
pIdx++
}
}
jobs, err := coderdAPI.Database.GetProvisionerJobsCreatedAfter(systemCtx, time.Time{})
require.NoError(t, err)
slices.SortFunc(jobs, func(a, b database.ProvisionerJob) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
jIdx := 0
for _, j := range jobs {
if _, ok := replace[j.ID.String()]; !ok {
replace[j.ID.String()] = fmt.Sprintf("00000000-0000-0000-bbbb-%012d", jIdx)
jIdx++
}
}
}
db, ps := dbtestutil.NewDB(t,
dbtestutil.WithDumpOnFailure(),
//nolint:gocritic // Use UTC for consistent timestamp length in golden files.
dbtestutil.WithTimezone("UTC"),
)
client, _, coderdAPI := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: false,
Database: db,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
_, 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, completeWithAgent())
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()
// Sanitize the UUIDs for the initial resources.
replace[version.ID.String()] = "00000000-0000-0000-cccc-000000000000"
replace[workspace.LatestBuild.ID.String()] = "00000000-0000-0000-dddd-000000000000"
// Create a provisioner that's working on a job.
pd1 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
Name: "provisioner-1",
CreatedAt: dbtime.Now().Add(1 * time.Second),
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.
_ = 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}, // Stale interval can't be adjusted, keep online.
KeyID: codersdk.ProvisionerKeyUUIDBuiltIn,
Tags: database.StringMap{"owner": "", "scope": "organization"},
})
updateReplaceUUIDs(coderdAPI)
for id, replaceID := range replace {
t.Logf("replace[%q] = %q", id, replaceID)
}
// Test provisioners list with template admin as members are currently
// unable to access provisioner jobs. In the future (with RBAC
// changes), we may allow them to view _their_ jobs.
t.Run("list", func(t *testing.T) {
t.Parallel()
var got bytes.Buffer
inv, root := clitest.New(t,
"provisioners",
"list",
"--column", "id,created at,last seen at,name,version,tags,key name,status,current job id,current job status,previous job id,previous job status,organization",
)
inv.Stdout = &got
clitest.SetupConfig(t, templateAdminClient, root)
err := inv.Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})
// Test jobs list with template admin as members are currently
// unable to access provisioner jobs. In the future (with RBAC
// changes), we may allow them to view _their_ jobs.
t.Run("jobs list", func(t *testing.T) {
t.Parallel()
var got bytes.Buffer
inv, root := clitest.New(t,
"provisioners",
"jobs",
"list",
"--column", "id,created at,status,worker id,tags,template version id,workspace build id,type,available workers,organization,queue",
)
inv.Stdout = &got
clitest.SetupConfig(t, templateAdminClient, root)
err := inv.Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})
}