Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add provisioner daemon and jobs endpoints and commands #15940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Prev Previous commit
Next Next commit
dbmem/golden
  • Loading branch information
mafredri committed Jan 2, 2025
commit 876bd8087fc8e9283f68b51caff77875a86b07f0
25 changes: 13 additions & 12 deletions cli/clitest/golden.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -95,32 +96,32 @@ ExtractCommandPathsLoop:

// TestGoldenFile will test the given bytes slice input against the
// golden file with the given file name, optionally using the given replacements.
func TestGoldenFile(t *testing.T, fileName string, actual []byte, replacements map[string]string) {
if len(actual) == 0 {
func TestGoldenFile(t *testing.T, fileName string, got []byte, replacements map[string]string) {
t.Helper()

if len(got) == 0 {
t.Fatal("no output")
}

for k, v := range replacements {
actual = bytes.ReplaceAll(actual, []byte(k), []byte(v))
got = bytes.ReplaceAll(got, []byte(k), []byte(v))
}

actual = normalizeGoldenFile(t, actual)
got = normalizeGoldenFile(t, got)
goldenPath := filepath.Join("testdata", strings.ReplaceAll(fileName, " ", "_")+".golden")
if *UpdateGoldenFiles {
t.Logf("update golden file for: %q: %s", fileName, goldenPath)
err := os.WriteFile(goldenPath, actual, 0o600)
err := os.WriteFile(goldenPath, got, 0o600)
require.NoError(t, err, "update golden file")
}

expected, err := os.ReadFile(goldenPath)
want, err := os.ReadFile(goldenPath)
require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes")

expected = normalizeGoldenFile(t, expected)
require.Equal(
t, string(expected), string(actual),
"golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes",
goldenPath,
)
want = normalizeGoldenFile(t, want)
if diff := cmp.Diff(string(want), string(got)); diff != "" {
require.Failf(t, "golden file mismatch, run \"make update-golden-files\", verify and commit the changes", "%s: diff (-want +got)\n%s", goldenPath, diff)
}
}

// normalizeGoldenFile replaces any strings that are system or timing dependent
Expand Down
2 changes: 1 addition & 1 deletion cli/provisionerjobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func (r *RootCmd) provisionerJobs() *serpent.Command {
cmd := &serpent.Command{
Use: "jobs",
Short: "View and manage provisioner jobs.",
Short: "View and manage provisioner jobs",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Expand Down
101 changes: 75 additions & 26 deletions cli/provisioners_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
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/codersdk"
)

func TestProvisioners(t *testing.T) {
func TestProvisioners_Golden(t *testing.T) {
t.Parallel()

db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
// 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,
Expand All @@ -36,27 +78,32 @@ func TestProvisioners(t *testing.T) {
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)

// Stop the provisioner so it doesn't grab jobs.
// 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: timeParse(t, "2006-01-02", "2024-12-20"),
CreatedAt: dbtime.Now().Add(1 * time.Second),
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
})
w1 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
OwnerID: memberUser.ID,
TemplateID: template.ID,
})
wb1ID := uuid.MustParse("00000000-0000-0000-bbbb-000000000001")
wb1ID := uuid.MustParse("00000000-0000-0000-dddd-000000000001")
job1 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
ID: uuid.MustParse("00000000-0000-0000-cccc-000000000001"),
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"},
})
Expand All @@ -67,22 +114,22 @@ func TestProvisioners(t *testing.T) {
TemplateVersionID: version.ID,
})

// Create another provisioner that completed a job and is offline.
// Create a provisioner that completed a job previously and is offline.
pd2 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
Name: "provisioner-2",
CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"),
CreatedAt: dbtime.Now().Add(2 * time.Second),
LastSeenAt: sql.NullTime{Time: coderdAPI.Clock.Now().Add(-time.Hour), Valid: true},
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
})
w2 := dbgen.Workspace(t, coderdAPI.Database, database.WorkspaceTable{
OwnerID: memberUser.ID,
TemplateID: template.ID,
})
wb2ID := uuid.MustParse("00000000-0000-0000-bbbb-000000000002")
wb2ID := uuid.MustParse("00000000-0000-0000-dddd-000000000002")
job2 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
ID: uuid.MustParse("00000000-0000-0000-cccc-000000000002"),
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"},
Expand All @@ -99,11 +146,11 @@ func TestProvisioners(t *testing.T) {
OwnerID: memberUser.ID,
TemplateID: template.ID,
})
wb3ID := uuid.MustParse("00000000-0000-0000-bbbb-000000000003")
wb3ID := uuid.MustParse("00000000-0000-0000-dddd-000000000003")
job3 := dbgen.ProvisionerJob(t, db, coderdAPI.Pubsub, database.ProvisionerJob{
ID: uuid.MustParse("00000000-0000-0000-cccc-000000000003"),
Input: json.RawMessage(`{"workspace_build_id":"` + wb3ID.String() + `"}`),
Tags: database.StringMap{"owner": "", "scope": "organization"},
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,
Expand All @@ -113,48 +160,50 @@ func TestProvisioners(t *testing.T) {
})

// Create a provisioner that is idle.
pd3 := dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
_ = dbgen.ProvisionerDaemon(t, coderdAPI.Database, database.ProvisionerDaemon{
Name: "provisioner-3",
CreatedAt: timeParse(t, "2006-01-02", "2024-12-20"),
CreatedAt: dbtime.Now().Add(3 * time.Second),
KeyID: uuid.MustParse(codersdk.ProvisionerKeyIDBuiltIn),
})
_ = pd3

updateReplaceUUIDs(coderdAPI)

for id, replaceID := range replace {
t.Logf("replace[%q] = %q", id, replaceID)
}

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,api version,tags,status,current job id,previous job id,previous job status,organization",
)
inv.Stdout = &got
clitest.SetupConfig(t, member, root)
err := inv.Run()
require.NoError(t, err)

// TODO(mafredri): Verify golden output.
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})

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, member, root)
err := inv.Run()
require.NoError(t, err)

// TODO(mafredri): Verify golden output.
clitest.TestGoldenFile(t, t.Name(), got.Bytes(), replace)
})
}

func timeParse(t *testing.T, layout, s string) time.Time {
t.Helper()
tm, err := time.Parse(layout, s)
require.NoError(t, err)
return tm
}
6 changes: 6 additions & 0 deletions cli/testdata/TestProvisioners_Golden/jobs_list.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ID CREATED AT STATUS WORKER ID TAGS TEMPLATE VERSION ID WORKSPACE BUILD ID TYPE AVAILABLE WORKERS ORGANIZATION QUEUE
00000000-0000-0000-bbbb-000000000000 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] 00000000-0000-0000-cccc-000000000000 <nil> template_version_import [] Coder
00000000-0000-0000-bbbb-000000000001 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000000 map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000000 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000002 ====[timestamp]===== running 00000000-0000-0000-aaaa-000000000001 map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000001 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000003 ====[timestamp]===== succeeded 00000000-0000-0000-aaaa-000000000002 map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000002 workspace_build [] Coder
00000000-0000-0000-bbbb-000000000004 ====[timestamp]===== pending <nil> map[owner: scope:organization] <nil> 00000000-0000-0000-dddd-000000000003 workspace_build [00000000-0000-0000-aaaa-000000000000, 00000000-0000-0000-aaaa-000000000001, 00000000-0000-0000-aaaa-000000000002, 00000000-0000-0000-aaaa-000000000003] Coder 1/1
5 changes: 5 additions & 0 deletions cli/testdata/TestProvisioners_Golden/list.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ID CREATED AT LAST SEEN AT NAME VERSION API VERSION TAGS STATUS CURRENT JOB ID PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 1.1 map[owner: scope:organization] busy 00000000-0000-0000-bbbb-000000000002 <nil> <nil> Coder
00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 1.1 map[owner: scope:organization] offline <nil> 00000000-0000-0000-bbbb-000000000003 succeeded Coder
00000000-0000-0000-aaaa-000000000003 ====[timestamp]===== ====[timestamp]===== provisioner-3 v0.0.0 1.1 map[owner: scope:organization] idle <nil> <nil> <nil> Coder
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== test v0.0.0-devel 1.1 map[owner: scope:organization] idle <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder
12 changes: 6 additions & 6 deletions cli/testdata/coder_provisioners_jobs_list_--output_json.golden
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
{
"id": "==========[version job ID]==========",
"id": "======[workspace build job ID]======",
"created_at": "====[timestamp]=====",
"started_at": "====[timestamp]=====",
"completed_at": "====[timestamp]=====",
Expand All @@ -15,13 +15,13 @@
"queue_size": 0,
"organization_id": "===========[first org ID]===========",
"input": {
"template_version_id": "============[version ID]============"
"workspace_build_id": "========[workspace build ID]========"
},
"type": "template_version_import",
"type": "workspace_build",
"organization_name": "Coder"
},
{
"id": "======[workspace build job ID]======",
"id": "==========[version job ID]==========",
"created_at": "====[timestamp]=====",
"started_at": "====[timestamp]=====",
"completed_at": "====[timestamp]=====",
Expand All @@ -36,9 +36,9 @@
"queue_size": 0,
"organization_id": "===========[first org ID]===========",
"input": {
"workspace_build_id": "========[workspace build ID]========"
"template_version_id": "============[version ID]============"
},
"type": "workspace_build",
"type": "template_version_import",
"organization_name": "Coder"
}
]
Loading