Skip to content

Commit 15ca638

Browse files
committed
Move timings to build and improve tests
1 parent 1c6671e commit 15ca638

7 files changed

+295
-347
lines changed

coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,6 @@ func New(options *Options) *API {
11501150
r.Post("/", api.postWorkspaceAgentPortShare)
11511151
r.Delete("/", api.deleteWorkspaceAgentPortShare)
11521152
})
1153-
r.Get("/timings", api.workspaceTimings)
11541153
})
11551154
})
11561155
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
@@ -1165,6 +1164,7 @@ func New(options *Options) *API {
11651164
r.Get("/parameters", api.workspaceBuildParameters)
11661165
r.Get("/resources", api.workspaceBuildResourcesDeprecated)
11671166
r.Get("/state", api.workspaceBuildState)
1167+
r.Get("/timings", api.workspaceBuildTimings)
11681168
})
11691169
r.Route("/authcheck", func(r chi.Router) {
11701170
r.Use(apiKeyMiddleware)

coderd/workspacebuilds.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,68 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
647647
_, _ = rw.Write(workspaceBuild.ProvisionerState)
648648
}
649649

650+
// @Summary Get workspace build timings by ID
651+
// @ID get-workspace-build-timings-by-id
652+
// @Security CoderSessionToken
653+
// @Produce json
654+
// @Tags Builds
655+
// @Param workspacebuild path string true "Workspace build ID" format(uuid)
656+
// @Success 200 {object} codersdk.WorkspaceBuildTimings
657+
// @Router/workspacebuilds/{workspacebuild}/timings [get]
658+
func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) {
659+
var (
660+
ctx = r.Context()
661+
build = httpmw.WorkspaceBuildParam(r)
662+
)
663+
664+
provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID)
665+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
666+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
667+
Message: "Internal error fetching workspace timings.",
668+
Detail: err.Error(),
669+
})
670+
return
671+
}
672+
673+
agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID)
674+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
675+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
676+
Message: "Internal error fetching workspace agent script timings.",
677+
Detail: err.Error(),
678+
})
679+
return
680+
}
681+
682+
res := codersdk.WorkspaceBuildTimings{
683+
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
684+
AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)),
685+
}
686+
687+
for _, t := range provisionerTimings {
688+
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
689+
JobID: t.JobID,
690+
Stage: string(t.Stage),
691+
Source: t.Source,
692+
Action: t.Action,
693+
Resource: t.Resource,
694+
StartedAt: t.StartedAt,
695+
EndedAt: t.EndedAt,
696+
})
697+
}
698+
for _, t := range agentScriptTimings {
699+
res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{
700+
StartedAt: t.StartedAt,
701+
EndedAt: t.EndedAt,
702+
ExitCode: t.ExitCode,
703+
Stage: string(t.Stage),
704+
Status: string(t.Status),
705+
DisplayName: t.DisplayName,
706+
})
707+
}
708+
709+
httpapi.Write(ctx, rw, http.StatusOK, res)
710+
}
711+
650712
type workspaceBuildsData struct {
651713
users []database.User
652714
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow

coderd/workspacebuilds_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
2424
"github.com/coder/coder/v2/coderd/database"
2525
"github.com/coder/coder/v2/coderd/database/dbauthz"
26+
"github.com/coder/coder/v2/coderd/database/dbgen"
27+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2628
"github.com/coder/coder/v2/coderd/database/dbtime"
2729
"github.com/coder/coder/v2/coderd/externalauth"
2830
"github.com/coder/coder/v2/coderd/rbac"
@@ -1180,3 +1182,195 @@ func TestPostWorkspaceBuild(t *testing.T) {
11801182
require.Len(t, res.Workspaces, 0)
11811183
})
11821184
}
1185+
1186+
func TestWorkspaceBuildTimings(t *testing.T) {
1187+
t.Parallel()
1188+
1189+
// Setup the test environment with a template and version
1190+
db, pubsub := dbtestutil.NewDB(t)
1191+
client := coderdtest.New(t, &coderdtest.Options{
1192+
Database: db,
1193+
Pubsub: pubsub,
1194+
})
1195+
owner := coderdtest.CreateFirstUser(t, client)
1196+
file := dbgen.File(t, db, database.File{
1197+
CreatedBy: owner.UserID,
1198+
})
1199+
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1200+
OrganizationID: owner.OrganizationID,
1201+
InitiatorID: owner.UserID,
1202+
WorkerID: uuid.NullUUID{},
1203+
FileID: file.ID,
1204+
Tags: database.StringMap{
1205+
"custom": "true",
1206+
},
1207+
})
1208+
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1209+
OrganizationID: owner.OrganizationID,
1210+
JobID: versionJob.ID,
1211+
CreatedBy: owner.UserID,
1212+
})
1213+
template := dbgen.Template(t, db, database.Template{
1214+
OrganizationID: owner.OrganizationID,
1215+
ActiveVersionID: version.ID,
1216+
CreatedBy: owner.UserID,
1217+
})
1218+
1219+
makeProvisionerTimings := func(build database.WorkspaceBuild, count int) []database.ProvisionerJobTiming {
1220+
// Use the database.ProvisionerJobTiming struct to mock timings data instead
1221+
// of directly creating database.InsertProvisionerJobTimingsParams. This
1222+
// approach makes the mock data easier to understand, as
1223+
// database.InsertProvisionerJobTimingsParams requires slices of each field
1224+
// for batch inserts.
1225+
timings := make([]database.ProvisionerJobTiming, count)
1226+
now := time.Now()
1227+
for i := range count {
1228+
startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute)
1229+
endedAt := startedAt.Add(time.Minute)
1230+
timings[i] = database.ProvisionerJobTiming{
1231+
StartedAt: startedAt,
1232+
EndedAt: endedAt,
1233+
Stage: database.ProvisionerJobTimingStageInit,
1234+
Action: string(database.AuditActionCreate),
1235+
Source: "source",
1236+
Resource: fmt.Sprintf("resource[%d]", i),
1237+
}
1238+
}
1239+
insertParams := database.InsertProvisionerJobTimingsParams{
1240+
JobID: build.JobID,
1241+
}
1242+
for _, timing := range timings {
1243+
insertParams.StartedAt = append(insertParams.StartedAt, timing.StartedAt)
1244+
insertParams.EndedAt = append(insertParams.EndedAt, timing.EndedAt)
1245+
insertParams.Stage = append(insertParams.Stage, timing.Stage)
1246+
insertParams.Action = append(insertParams.Action, timing.Action)
1247+
insertParams.Source = append(insertParams.Source, timing.Source)
1248+
insertParams.Resource = append(insertParams.Resource, timing.Resource)
1249+
}
1250+
return dbgen.ProvisionerJobTimings(t, db, insertParams)
1251+
}
1252+
1253+
makeAgentScriptTimings := func(build database.WorkspaceBuild, count int) []database.WorkspaceAgentScriptTiming {
1254+
// Create a resource, agent, and script to test the timing of agent scripts
1255+
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
1256+
JobID: build.JobID,
1257+
})
1258+
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
1259+
ResourceID: resource.ID,
1260+
})
1261+
scripts := dbgen.WorkspaceAgentScripts(t, db, database.InsertWorkspaceAgentScriptsParams{
1262+
WorkspaceAgentID: agent.ID,
1263+
CreatedAt: time.Now(),
1264+
LogSourceID: []uuid.UUID{
1265+
uuid.New(),
1266+
},
1267+
LogPath: []string{""},
1268+
Script: []string{""},
1269+
Cron: []string{""},
1270+
StartBlocksLogin: []bool{false},
1271+
RunOnStart: []bool{false},
1272+
RunOnStop: []bool{false},
1273+
TimeoutSeconds: []int32{0},
1274+
DisplayName: []string{""},
1275+
ID: []uuid.UUID{
1276+
uuid.New(),
1277+
},
1278+
})
1279+
1280+
newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count)
1281+
now := time.Now()
1282+
for i := range count {
1283+
startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute)
1284+
endedAt := startedAt.Add(time.Minute)
1285+
newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{
1286+
StartedAt: startedAt,
1287+
EndedAt: endedAt,
1288+
Stage: database.WorkspaceAgentScriptTimingStageStart,
1289+
ScriptID: scripts[0].ID,
1290+
ExitCode: 0,
1291+
Status: database.WorkspaceAgentScriptTimingStatusOk,
1292+
}
1293+
}
1294+
1295+
timings := make([]database.WorkspaceAgentScriptTiming, 0)
1296+
for _, newTiming := range newTimings {
1297+
timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming)
1298+
timings = append(timings, timing)
1299+
}
1300+
1301+
return timings
1302+
}
1303+
1304+
// Given
1305+
testCases := []struct {
1306+
name string
1307+
provisionerTimings int
1308+
actionScriptTimings int
1309+
}{
1310+
{name: "with empty provisioner timings", provisionerTimings: 0},
1311+
{name: "with provisioner timings", provisionerTimings: 5},
1312+
{name: "with empty agent script timings", actionScriptTimings: 0},
1313+
{name: "with agent script timings", actionScriptTimings: 5},
1314+
}
1315+
1316+
for _, tc := range testCases {
1317+
tc := tc
1318+
t.Run(tc.name, func(t *testing.T) {
1319+
t.Parallel()
1320+
1321+
// Create a build to attach provisioner timings
1322+
ws := dbgen.Workspace(t, db, database.Workspace{
1323+
OwnerID: owner.UserID,
1324+
OrganizationID: owner.OrganizationID,
1325+
TemplateID: template.ID,
1326+
// Generate unique name for the workspace
1327+
Name: "test-workspace-" + uuid.New().String(),
1328+
})
1329+
jobID := uuid.New()
1330+
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1331+
ID: jobID,
1332+
OrganizationID: owner.OrganizationID,
1333+
Type: database.ProvisionerJobTypeWorkspaceBuild,
1334+
Tags: database.StringMap{jobID.String(): "true"},
1335+
})
1336+
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
1337+
WorkspaceID: ws.ID,
1338+
TemplateVersionID: version.ID,
1339+
BuildNumber: 1,
1340+
Transition: database.WorkspaceTransitionStart,
1341+
InitiatorID: owner.UserID,
1342+
JobID: job.ID,
1343+
})
1344+
1345+
// Generate timings based on test config
1346+
genProvisionerTimings := makeProvisionerTimings(build, tc.provisionerTimings)
1347+
genAgentScriptTimings := makeAgentScriptTimings(build, tc.provisionerTimings)
1348+
1349+
res, err := client.WorkspaceBuildTimings(context.Background(), build.ID)
1350+
require.NoError(t, err)
1351+
require.Len(t, res.ProvisionerTimings, tc.provisionerTimings)
1352+
1353+
for i := range res.ProvisionerTimings {
1354+
timingRes := res.ProvisionerTimings[i]
1355+
genTiming := genProvisionerTimings[i]
1356+
require.Equal(t, genTiming.Resource, timingRes.Resource)
1357+
require.Equal(t, genTiming.Action, timingRes.Action)
1358+
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
1359+
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
1360+
require.Equal(t, genTiming.Source, timingRes.Source)
1361+
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
1362+
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
1363+
}
1364+
1365+
for i := range res.AgentScriptTimings {
1366+
timingRes := res.AgentScriptTimings[i]
1367+
genTiming := genAgentScriptTimings[i]
1368+
require.Equal(t, genTiming.ExitCode, timingRes.ExitCode)
1369+
require.Equal(t, string(genTiming.Status), timingRes.Status)
1370+
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
1371+
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
1372+
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
1373+
}
1374+
})
1375+
}
1376+
}

coderd/workspaces.go

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,77 +1740,6 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
17401740
}
17411741
}
17421742

1743-
// @Summary Get workspace timings by ID
1744-
// @ID get-workspace-timings-by-id
1745-
// @Security CoderSessionToken
1746-
// @Produce json
1747-
// @Tags Workspaces
1748-
// @Param workspace path string true "Workspace ID" format(uuid)
1749-
// @Success 200 {object} codersdk.WorkspaceTimings
1750-
// @Router /workspaces/{workspace}/timings [get]
1751-
func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) {
1752-
var (
1753-
ctx = r.Context()
1754-
workspace = httpmw.WorkspaceParam(r)
1755-
)
1756-
1757-
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
1758-
if err != nil {
1759-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1760-
Message: "Internal error fetching workspace build.",
1761-
Detail: err.Error(),
1762-
})
1763-
return
1764-
}
1765-
1766-
provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID)
1767-
if err != nil && !errors.Is(err, sql.ErrNoRows) {
1768-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1769-
Message: "Internal error fetching workspace timings.",
1770-
Detail: err.Error(),
1771-
})
1772-
return
1773-
}
1774-
1775-
agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID)
1776-
if err != nil && !errors.Is(err, sql.ErrNoRows) {
1777-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1778-
Message: "Internal error fetching workspace agent script timings.",
1779-
Detail: err.Error(),
1780-
})
1781-
return
1782-
}
1783-
1784-
res := codersdk.WorkspaceTimings{
1785-
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
1786-
AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)),
1787-
}
1788-
1789-
for _, t := range provisionerTimings {
1790-
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
1791-
JobID: t.JobID,
1792-
Stage: string(t.Stage),
1793-
Source: t.Source,
1794-
Action: t.Action,
1795-
Resource: t.Resource,
1796-
StartedAt: t.StartedAt,
1797-
EndedAt: t.EndedAt,
1798-
})
1799-
}
1800-
for _, t := range agentScriptTimings {
1801-
res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{
1802-
StartedAt: t.StartedAt,
1803-
EndedAt: t.EndedAt,
1804-
ExitCode: t.ExitCode,
1805-
Stage: string(t.Stage),
1806-
Status: string(t.Status),
1807-
DisplayName: t.DisplayName,
1808-
})
1809-
}
1810-
1811-
httpapi.Write(ctx, rw, http.StatusOK, res)
1812-
}
1813-
18141743
type workspaceData struct {
18151744
templates []database.Template
18161745
builds []codersdk.WorkspaceBuild

0 commit comments

Comments
 (0)