feat(coderd): return agent script timings (#14923)

Add the agent script timings into the
`/workspacebuilds/:workspacebuild/timings` response.

Close https://github.com/coder/coder/issues/14876
This commit is contained in:
Bruno Quaresma
2024-10-14 09:31:03 -03:00
committed by GitHub
parent 79d24d2101
commit 9c8ecb82a3
23 changed files with 982 additions and 296 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ func (s *ScriptsAPI) ScriptCompleted(ctx context.Context, req *agentproto.Worksp
//nolint:gocritic // We need permissions to write to the DB here and we are in the context of the agent.
ctx = dbauthz.AsProvisionerd(ctx)
err = s.Database.InsertWorkspaceAgentScriptTimings(ctx, database.InsertWorkspaceAgentScriptTimingsParams{
_, err = s.Database.InsertWorkspaceAgentScriptTimings(ctx, database.InsertWorkspaceAgentScriptTimingsParams{
ScriptID: scriptID,
Stage: stage,
Status: status,
+78 -12
View File
@@ -7473,6 +7473,41 @@ const docTemplate = `{
}
}
},
"/workspacebuilds/{workspacebuild}/timings": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Builds"
],
"summary": "Get workspace build timings by ID",
"operationId": "get-workspace-build-timings-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace build ID",
"name": "workspacebuild",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.WorkspaceBuildTimings"
}
}
}
}
},
"/workspaceproxies": {
"get": {
"security": [
@@ -8506,7 +8541,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.WorkspaceTimings"
"$ref": "#/definitions/codersdk.WorkspaceBuildTimings"
}
}
}
@@ -8940,6 +8975,31 @@ const docTemplate = `{
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"ended_at": {
"type": "string",
"format": "date-time"
},
"exit_code": {
"type": "integer"
},
"stage": {
"type": "string"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string"
}
}
},
"codersdk.AgentSubsystem": {
"type": "string",
"enum": [
@@ -14684,6 +14744,23 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceBuildTimings": {
"type": "object",
"properties": {
"agent_script_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentScriptTiming"
}
},
"provisioner_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerTiming"
}
}
}
},
"codersdk.WorkspaceConnectionLatencyMS": {
"type": "object",
"properties": {
@@ -14927,17 +15004,6 @@ const docTemplate = `{
"WorkspaceStatusDeleted"
]
},
"codersdk.WorkspaceTimings": {
"type": "object",
"properties": {
"provisioner_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerTiming"
}
}
}
},
"codersdk.WorkspaceTransition": {
"type": "string",
"enum": [
+74 -12
View File
@@ -6609,6 +6609,37 @@
}
}
},
"/workspacebuilds/{workspacebuild}/timings": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Builds"],
"summary": "Get workspace build timings by ID",
"operationId": "get-workspace-build-timings-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace build ID",
"name": "workspacebuild",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.WorkspaceBuildTimings"
}
}
}
}
},
"/workspaceproxies": {
"get": {
"security": [
@@ -7524,7 +7555,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.WorkspaceTimings"
"$ref": "#/definitions/codersdk.WorkspaceBuildTimings"
}
}
}
@@ -7922,6 +7953,31 @@
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"ended_at": {
"type": "string",
"format": "date-time"
},
"exit_code": {
"type": "integer"
},
"stage": {
"type": "string"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string"
}
}
},
"codersdk.AgentSubsystem": {
"type": "string",
"enum": ["envbox", "envbuilder", "exectrace"],
@@ -13359,6 +13415,23 @@
}
}
},
"codersdk.WorkspaceBuildTimings": {
"type": "object",
"properties": {
"agent_script_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentScriptTiming"
}
},
"provisioner_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerTiming"
}
}
}
},
"codersdk.WorkspaceConnectionLatencyMS": {
"type": "object",
"properties": {
@@ -13598,17 +13671,6 @@
"WorkspaceStatusDeleted"
]
},
"codersdk.WorkspaceTimings": {
"type": "object",
"properties": {
"provisioner_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ProvisionerTiming"
}
}
}
},
"codersdk.WorkspaceTransition": {
"type": "string",
"enum": ["start", "stop", "delete"],
+1
View File
@@ -1173,6 +1173,7 @@ func New(options *Options) *API {
r.Get("/parameters", api.workspaceBuildParameters)
r.Get("/resources", api.workspaceBuildResourcesDeprecated)
r.Get("/state", api.workspaceBuildState)
r.Get("/timings", api.workspaceBuildTimings)
})
r.Route("/authcheck", func(r chi.Router) {
r.Use(apiKeyMiddleware)
+9 -2
View File
@@ -2421,6 +2421,13 @@ func (q *querier) GetWorkspaceAgentPortShare(ctx context.Context, arg database.G
return q.db.GetWorkspaceAgentPortShare(ctx, arg)
}
func (q *querier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentScriptTimingsByBuildID(ctx, id)
}
func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
@@ -3034,9 +3041,9 @@ func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database
return q.db.InsertWorkspaceAgentMetadata(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) error {
func (q *querier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) (database.WorkspaceAgentScriptTiming, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
return err
return database.WorkspaceAgentScriptTiming{}, err
}
return q.db.InsertWorkspaceAgentScriptTimings(ctx, arg)
}
+40 -20
View File
@@ -551,26 +551,6 @@ func (s *MethodTestSuite) TestProvisionerJob() {
check.Args(database.UpdateProvisionerJobWithCancelByIDParams{ID: j.ID}).
Asserts(v.RBACObject(tpl), []policy.Action{policy.ActionRead, policy.ActionUpdate}).Returns()
}))
s.Run("GetProvisionerJobTimingsByJobID", s.Subtest(func(db database.Store, check *expects) {
w := dbgen.Workspace(s.T(), db, database.Workspace{})
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
_ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID})
t := dbgen.ProvisionerJobTimings(s.T(), db, database.InsertProvisionerJobTimingsParams{
JobID: j.ID,
StartedAt: []time.Time{dbtime.Now(), dbtime.Now()},
EndedAt: []time.Time{dbtime.Now(), dbtime.Now()},
Stage: []database.ProvisionerJobTimingStage{
database.ProvisionerJobTimingStageInit,
database.ProvisionerJobTimingStagePlan,
},
Source: []string{"source1", "source2"},
Action: []string{"action1", "action2"},
Resource: []string{"resource1", "resource2"},
})
check.Args(j.ID).Asserts(w, policy.ActionRead).Returns(t)
}))
s.Run("GetProvisionerJobsByIDs", s.Subtest(func(db database.Store, check *expects) {
a := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{})
b := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{})
@@ -2867,6 +2847,46 @@ func (s *MethodTestSuite) TestSystemFunctions() {
LastGeneratedAt: dbtime.Now(),
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
}))
s.Run("GetProvisionerJobTimingsByJobID", s.Subtest(func(db database.Store, check *expects) {
w := dbgen.Workspace(s.T(), db, database.Workspace{})
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID})
t := dbgen.ProvisionerJobTimings(s.T(), db, b, 2)
check.Args(j.ID).Asserts(w, policy.ActionRead).Returns(t)
}))
s.Run("GetWorkspaceAgentScriptTimingsByBuildID", s.Subtest(func(db database.Store, check *expects) {
workspace := dbgen.Workspace(s.T(), db, database.Workspace{})
job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: job.ID, WorkspaceID: workspace.ID})
resource := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{
JobID: build.JobID,
})
agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
script := dbgen.WorkspaceAgentScript(s.T(), db, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
timing := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.WorkspaceAgentScriptTiming{
ScriptID: script.ID,
})
rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{
{
StartedAt: timing.StartedAt,
EndedAt: timing.EndedAt,
Stage: timing.Stage,
ScriptID: timing.ScriptID,
ExitCode: timing.ExitCode,
Status: timing.Status,
DisplayName: script.DisplayName,
},
}
check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows)
}))
}
func (s *MethodTestSuite) TestNotifications() {
+64 -3
View File
@@ -189,6 +189,49 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen
return agt
}
func WorkspaceAgentScript(t testing.TB, db database.Store, orig database.WorkspaceAgentScript) database.WorkspaceAgentScript {
scripts, err := db.InsertWorkspaceAgentScripts(genCtx, database.InsertWorkspaceAgentScriptsParams{
WorkspaceAgentID: takeFirst(orig.WorkspaceAgentID, uuid.New()),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
LogSourceID: []uuid.UUID{takeFirst(orig.LogSourceID, uuid.New())},
LogPath: []string{takeFirst(orig.LogPath, "")},
Script: []string{takeFirst(orig.Script, "")},
Cron: []string{takeFirst(orig.Cron, "")},
StartBlocksLogin: []bool{takeFirst(orig.StartBlocksLogin, false)},
RunOnStart: []bool{takeFirst(orig.RunOnStart, false)},
RunOnStop: []bool{takeFirst(orig.RunOnStop, false)},
TimeoutSeconds: []int32{takeFirst(orig.TimeoutSeconds, 0)},
DisplayName: []string{takeFirst(orig.DisplayName, "")},
ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())},
})
require.NoError(t, err, "insert workspace agent script")
require.NotEmpty(t, scripts, "insert workspace agent script returned no scripts")
return scripts[0]
}
func WorkspaceAgentScriptTimings(t testing.TB, db database.Store, script database.WorkspaceAgentScript, count int) []database.WorkspaceAgentScriptTiming {
timings := make([]database.WorkspaceAgentScriptTiming, count)
for i := range count {
timings[i] = WorkspaceAgentScriptTiming(t, db, database.WorkspaceAgentScriptTiming{
ScriptID: script.ID,
})
}
return timings
}
func WorkspaceAgentScriptTiming(t testing.TB, db database.Store, orig database.WorkspaceAgentScriptTiming) database.WorkspaceAgentScriptTiming {
timing, err := db.InsertWorkspaceAgentScriptTimings(genCtx, database.InsertWorkspaceAgentScriptTimingsParams{
StartedAt: takeFirst(orig.StartedAt, dbtime.Now()),
EndedAt: takeFirst(orig.EndedAt, dbtime.Now()),
Stage: takeFirst(orig.Stage, database.WorkspaceAgentScriptTimingStageStart),
ScriptID: takeFirst(orig.ScriptID, uuid.New()),
ExitCode: takeFirst(orig.ExitCode, 0),
Status: takeFirst(orig.Status, database.WorkspaceAgentScriptTimingStatusOk),
})
require.NoError(t, err, "insert workspace agent script")
return timing
}
func Workspace(t testing.TB, db database.Store, orig database.Workspace) database.Workspace {
t.Helper()
@@ -935,12 +978,30 @@ func CryptoKey(t testing.TB, db database.Store, seed database.CryptoKey) databas
return key
}
func ProvisionerJobTimings(t testing.TB, db database.Store, seed database.InsertProvisionerJobTimingsParams) []database.ProvisionerJobTiming {
timings, err := db.InsertProvisionerJobTimings(genCtx, seed)
require.NoError(t, err, "insert provisioner job timings")
func ProvisionerJobTimings(t testing.TB, db database.Store, build database.WorkspaceBuild, count int) []database.ProvisionerJobTiming {
timings := make([]database.ProvisionerJobTiming, count)
for i := range count {
timings[i] = provisionerJobTiming(t, db, database.ProvisionerJobTiming{
JobID: build.JobID,
})
}
return timings
}
func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
JobID: takeFirst(seed.JobID, uuid.New()),
StartedAt: []time.Time{takeFirst(seed.StartedAt, dbtime.Now())},
EndedAt: []time.Time{takeFirst(seed.EndedAt, dbtime.Now())},
Stage: []database.ProvisionerJobTimingStage{takeFirst(seed.Stage, database.ProvisionerJobTimingStageInit)},
Source: []string{takeFirst(seed.Source, "source")},
Action: []string{takeFirst(seed.Action, "action")},
Resource: []string{takeFirst(seed.Resource, "resource")},
})
require.NoError(t, err, "insert provisioner job timing")
return timing[0]
}
func must[V any](v V, err error) V {
if err != nil {
panic(err)
+68 -14
View File
@@ -5793,6 +5793,69 @@ func (q *FakeQuerier) GetWorkspaceAgentPortShare(_ context.Context, arg database
return database.WorkspaceAgentPortShare{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
build, err := q.GetWorkspaceBuildByID(ctx, id)
if err != nil {
return nil, xerrors.Errorf("get build: %w", err)
}
resources, err := q.GetWorkspaceResourcesByJobID(ctx, build.JobID)
if err != nil {
return nil, xerrors.Errorf("get resources: %w", err)
}
resourceIDs := make([]uuid.UUID, 0, len(resources))
for _, res := range resources {
resourceIDs = append(resourceIDs, res.ID)
}
agents, err := q.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs)
if err != nil {
return nil, xerrors.Errorf("get agents: %w", err)
}
agentIDs := make([]uuid.UUID, 0, len(agents))
for _, agent := range agents {
agentIDs = append(agentIDs, agent.ID)
}
scripts, err := q.GetWorkspaceAgentScriptsByAgentIDs(ctx, agentIDs)
if err != nil {
return nil, xerrors.Errorf("get scripts: %w", err)
}
scriptIDs := make([]uuid.UUID, 0, len(scripts))
for _, script := range scripts {
scriptIDs = append(scriptIDs, script.ID)
}
rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{}
for _, t := range q.workspaceAgentScriptTimings {
if !slice.Contains(scriptIDs, t.ScriptID) {
continue
}
var script database.WorkspaceAgentScript
for _, s := range scripts {
if s.ID == t.ScriptID {
script = s
break
}
}
rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{
ScriptID: t.ScriptID,
StartedAt: t.StartedAt,
EndedAt: t.EndedAt,
ExitCode: t.ExitCode,
Stage: t.Stage,
Status: t.Status,
DisplayName: script.DisplayName,
})
}
return rows, nil
}
func (q *FakeQuerier) GetWorkspaceAgentScriptsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -7844,28 +7907,19 @@ func (q *FakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg databa
return nil
}
func (q *FakeQuerier) InsertWorkspaceAgentScriptTimings(_ context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) error {
func (q *FakeQuerier) InsertWorkspaceAgentScriptTimings(_ context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) (database.WorkspaceAgentScriptTiming, error) {
err := validateDatabaseType(arg)
if err != nil {
return err
return database.WorkspaceAgentScriptTiming{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
q.workspaceAgentScriptTimings = append(q.workspaceAgentScriptTimings,
//nolint:gosimple // Stop the linter complaining about changing the type of `arg`.
database.WorkspaceAgentScriptTiming{
ScriptID: arg.ScriptID,
StartedAt: arg.StartedAt,
EndedAt: arg.EndedAt,
ExitCode: arg.ExitCode,
Stage: arg.Stage,
Status: arg.Status,
},
)
timing := database.WorkspaceAgentScriptTiming(arg)
q.workspaceAgentScriptTimings = append(q.workspaceAgentScriptTimings, timing)
return nil
return timing, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentScripts(_ context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
+10 -3
View File
@@ -1404,6 +1404,13 @@ func (m metricsStore) GetWorkspaceAgentPortShare(ctx context.Context, arg databa
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentScriptTimingsByBuildID(ctx, id)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentScriptTimingsByBuildID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
@@ -1936,11 +1943,11 @@ func (m metricsStore) InsertWorkspaceAgentMetadata(ctx context.Context, arg data
return err
}
func (m metricsStore) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) error {
func (m metricsStore) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg database.InsertWorkspaceAgentScriptTimingsParams) (database.WorkspaceAgentScriptTiming, error) {
start := time.Now()
err := m.s.InsertWorkspaceAgentScriptTimings(ctx, arg)
r0, r1 := m.s.InsertWorkspaceAgentScriptTimings(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentScriptTimings").Observe(time.Since(start).Seconds())
return err
return r0, r1
}
func (m metricsStore) InsertWorkspaceAgentScripts(ctx context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) {
+19 -3
View File
@@ -2933,6 +2933,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentPortShare(arg0, arg1 any) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentPortShare), arg0, arg1)
}
// GetWorkspaceAgentScriptTimingsByBuildID mocks base method.
func (m *MockStore) GetWorkspaceAgentScriptTimingsByBuildID(arg0 context.Context, arg1 uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentScriptTimingsByBuildID", arg0, arg1)
ret0, _ := ret[0].([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentScriptTimingsByBuildID indicates an expected call of GetWorkspaceAgentScriptTimingsByBuildID.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptTimingsByBuildID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentScriptTimingsByBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptTimingsByBuildID), arg0, arg1)
}
// GetWorkspaceAgentScriptsByAgentIDs mocks base method.
func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
m.ctrl.T.Helper()
@@ -4080,11 +4095,12 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentMetadata(arg0, arg1 any) *g
}
// InsertWorkspaceAgentScriptTimings mocks base method.
func (m *MockStore) InsertWorkspaceAgentScriptTimings(arg0 context.Context, arg1 database.InsertWorkspaceAgentScriptTimingsParams) error {
func (m *MockStore) InsertWorkspaceAgentScriptTimings(arg0 context.Context, arg1 database.InsertWorkspaceAgentScriptTimingsParams) (database.WorkspaceAgentScriptTiming, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentScriptTimings", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
ret0, _ := ret[0].(database.WorkspaceAgentScriptTiming)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentScriptTimings indicates an expected call of InsertWorkspaceAgentScriptTimings.
+2 -1
View File
@@ -298,6 +298,7 @@ type sqlcQuerier interface {
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error)
GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error)
GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error)
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error)
@@ -395,7 +396,7 @@ type sqlcQuerier interface {
InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error)
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) error
InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) (WorkspaceAgentScriptTiming, error)
InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error)
InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
+65 -4
View File
@@ -11443,6 +11443,57 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks
return items, nil
}
const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, workspace_agent_scripts.display_name
FROM workspace_agent_script_timings
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1
`
type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
StartedAt time.Time `db:"started_at" json:"started_at"`
EndedAt time.Time `db:"ended_at" json:"ended_at"`
ExitCode int32 `db:"exit_code" json:"exit_code"`
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
DisplayName string `db:"display_name" json:"display_name"`
}
func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByBuildID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentScriptTimingsByBuildIDRow
for rows.Next() {
var i GetWorkspaceAgentScriptTimingsByBuildIDRow
if err := rows.Scan(
&i.ScriptID,
&i.StartedAt,
&i.EndedAt,
&i.ExitCode,
&i.Stage,
&i.Status,
&i.DisplayName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order
@@ -11902,7 +11953,7 @@ func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg Inser
return err
}
const insertWorkspaceAgentScriptTimings = `-- name: InsertWorkspaceAgentScriptTimings :exec
const insertWorkspaceAgentScriptTimings = `-- name: InsertWorkspaceAgentScriptTimings :one
INSERT INTO
workspace_agent_script_timings (
script_id,
@@ -11914,6 +11965,7 @@ INSERT INTO
)
VALUES
($1, $2, $3, $4, $5, $6)
RETURNING workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status
`
type InsertWorkspaceAgentScriptTimingsParams struct {
@@ -11925,8 +11977,8 @@ type InsertWorkspaceAgentScriptTimingsParams struct {
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
}
func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAgentScriptTimings,
func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) (WorkspaceAgentScriptTiming, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceAgentScriptTimings,
arg.ScriptID,
arg.StartedAt,
arg.EndedAt,
@@ -11934,7 +11986,16 @@ func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg
arg.Stage,
arg.Status,
)
return err
var i WorkspaceAgentScriptTiming
err := row.Scan(
&i.ScriptID,
&i.StartedAt,
&i.EndedAt,
&i.ExitCode,
&i.Stage,
&i.Status,
)
return i, err
}
const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec
+12 -2
View File
@@ -288,7 +288,7 @@ WHERE
)
;
-- name: InsertWorkspaceAgentScriptTimings :exec
-- name: InsertWorkspaceAgentScriptTimings :one
INSERT INTO
workspace_agent_script_timings (
script_id,
@@ -299,4 +299,14 @@ INSERT INTO
status
)
VALUES
($1, $2, $3, $4, $5, $6);
($1, $2, $3, $4, $5, $6)
RETURNING workspace_agent_script_timings.*;
-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT workspace_agent_script_timings.*, workspace_agent_scripts.display_name
FROM workspace_agent_script_timings
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1;
+67
View File
@@ -647,6 +647,32 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(workspaceBuild.ProvisionerState)
}
// @Summary Get workspace build timings by ID
// @ID get-workspace-build-timings-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceBuildTimings
// @Router /workspacebuilds/{workspacebuild}/timings [get]
func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
build = httpmw.WorkspaceBuildParam(r)
)
timings, err := api.buildTimings(ctx, build)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching timings.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, timings)
}
type workspaceBuildsData struct {
users []database.User
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
@@ -1010,3 +1036,44 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition
// return error status since we should never get here
return codersdk.WorkspaceStatusFailed
}
func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) (codersdk.WorkspaceBuildTimings, error) {
provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching provisioner job timings: %w", err)
}
agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err)
}
res := codersdk.WorkspaceBuildTimings{
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)),
}
for _, t := range provisionerTimings {
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
JobID: t.JobID,
Stage: string(t.Stage),
Source: t.Source,
Action: t.Action,
Resource: t.Resource,
StartedAt: t.StartedAt,
EndedAt: t.EndedAt,
})
}
for _, t := range agentScriptTimings {
res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{
StartedAt: t.StartedAt,
EndedAt: t.EndedAt,
ExitCode: t.ExitCode,
Stage: string(t.Stage),
Status: string(t.Status),
DisplayName: t.DisplayName,
})
}
return res, nil
}
+188 -1
View File
@@ -23,6 +23,8 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"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/externalauth"
"github.com/coder/coder/v2/coderd/rbac"
@@ -40,7 +42,7 @@ func TestWorkspaceBuild(t *testing.T) {
propagation.Baggage{},
),
)
ctx := testutil.Context(t, testutil.WaitShort)
ctx := testutil.Context(t, testutil.WaitLong)
auditor := audit.NewMock()
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
@@ -1180,3 +1182,188 @@ func TestPostWorkspaceBuild(t *testing.T) {
require.Len(t, res.Workspaces, 0)
})
}
func TestWorkspaceBuildTimings(t *testing.T) {
t.Parallel()
// Setup the test environment with a template and version
db, pubsub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
})
owner := coderdtest.CreateFirstUser(t, client)
file := dbgen.File(t, db, database.File{
CreatedBy: owner.UserID,
})
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
OrganizationID: owner.OrganizationID,
InitiatorID: owner.UserID,
FileID: file.ID,
Tags: database.StringMap{
"custom": "true",
},
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: owner.OrganizationID,
JobID: versionJob.ID,
CreatedBy: owner.UserID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: owner.OrganizationID,
ActiveVersionID: version.ID,
CreatedBy: owner.UserID,
})
ws := dbgen.Workspace(t, db, database.Workspace{
OwnerID: owner.UserID,
OrganizationID: owner.OrganizationID,
TemplateID: template.ID,
})
// Create a build to attach timings
var buildNumber int32
makeBuild := func() database.WorkspaceBuild {
buildNumber++
jobID := uuid.New()
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
ID: jobID,
OrganizationID: owner.OrganizationID,
Tags: database.StringMap{jobID.String(): "true"},
})
return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID,
TemplateVersionID: version.ID,
InitiatorID: owner.UserID,
JobID: job.ID,
BuildNumber: buildNumber,
})
}
t.Run("NonExistentBuild", func(t *testing.T) {
t.Parallel()
// When: fetching an inexistent build
buildID := uuid.New()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
_, err := client.WorkspaceBuildTimings(ctx, buildID)
// Then: expect a not found error
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
})
t.Run("EmptyTimings", func(t *testing.T) {
t.Parallel()
// When: fetching timings for a build with no timings
build := makeBuild()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
// Then: return a response with empty timings
require.NoError(t, err)
require.Empty(t, res.ProvisionerTimings)
require.Empty(t, res.AgentScriptTimings)
})
t.Run("ProvisionerTimings", func(t *testing.T) {
t.Parallel()
// When: fetching timings for a build with provisioner timings
build := makeBuild()
provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5)
// Then: return a response with the expected timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
require.NoError(t, err)
require.Len(t, res.ProvisionerTimings, 5)
for i := range res.ProvisionerTimings {
timingRes := res.ProvisionerTimings[i]
genTiming := provisionerTimings[i]
require.Equal(t, genTiming.Resource, timingRes.Resource)
require.Equal(t, genTiming.Action, timingRes.Action)
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
require.Equal(t, genTiming.Source, timingRes.Source)
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
}
})
t.Run("AgentScriptTimings", func(t *testing.T) {
t.Parallel()
// When: fetching timings for a build with agent script timings
build := makeBuild()
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5)
// Then: return a response with the expected timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
require.NoError(t, err)
require.Len(t, res.AgentScriptTimings, 5)
for i := range res.AgentScriptTimings {
timingRes := res.AgentScriptTimings[i]
genTiming := agentScriptTimings[i]
require.Equal(t, genTiming.ExitCode, timingRes.ExitCode)
require.Equal(t, string(genTiming.Status), timingRes.Status)
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
}
})
t.Run("NoAgentScripts", func(t *testing.T) {
t.Parallel()
// When: fetching timings for a build with no agent scripts
build := makeBuild()
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
// Then: return a response with empty agent script timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
require.NoError(t, err)
require.Empty(t, res.AgentScriptTimings)
})
// Some workspaces might not have agents. It is improbable, but possible.
t.Run("NoAgents", func(t *testing.T) {
t.Parallel()
// When: fetching timings for a build with no agents
build := makeBuild()
dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
// Then: return a response with empty agent script timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
require.NoError(t, err)
require.Empty(t, res.AgentScriptTimings)
})
}
+5 -19
View File
@@ -1746,7 +1746,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceTimings
// @Success 200 {object} codersdk.WorkspaceBuildTimings
// @Router /workspaces/{workspace}/timings [get]
func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) {
var (
@@ -1763,30 +1763,16 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) {
return
}
provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
timings, err := api.buildTimings(ctx, build)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace timings.",
Message: "Internal error fetching timings.",
Detail: err.Error(),
})
return
}
res := codersdk.WorkspaceTimings{
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
}
for _, t := range provisionerTimings {
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
JobID: t.JobID,
Stage: string(t.Stage),
Source: t.Source,
Action: t.Action,
Resource: t.Resource,
StartedAt: t.StartedAt,
EndedAt: t.EndedAt,
})
}
httpapi.Write(ctx, rw, http.StatusOK, res)
httpapi.Write(ctx, rw, http.StatusOK, timings)
}
type workspaceData struct {
+91 -147
View File
@@ -3560,165 +3560,109 @@ func TestWorkspaceNotifications(t *testing.T) {
func TestWorkspaceTimings(t *testing.T) {
t.Parallel()
// Setup a base template for the workspaces
db, pubsub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
})
owner := coderdtest.CreateFirstUser(t, client)
file := dbgen.File(t, db, database.File{
CreatedBy: owner.UserID,
})
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
OrganizationID: owner.OrganizationID,
InitiatorID: owner.UserID,
WorkerID: uuid.NullUUID{},
FileID: file.ID,
Tags: database.StringMap{
"custom": "true",
},
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: owner.OrganizationID,
JobID: versionJob.ID,
CreatedBy: owner.UserID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: owner.OrganizationID,
ActiveVersionID: version.ID,
CreatedBy: owner.UserID,
})
coderdtest.CreateFirstUser(t, client)
// Since the tests run in parallel, we need to create a new workspace for
// each test to avoid fetching the wrong latest build.
type workspaceWithBuild struct {
database.Workspace
build database.WorkspaceBuild
}
makeWorkspace := func() workspaceWithBuild {
t.Run("LatestBuild", func(t *testing.T) {
t.Parallel()
// Given: a workspace with many builds, provisioner, and agent script timings
db, pubsub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
})
owner := coderdtest.CreateFirstUser(t, client)
file := dbgen.File(t, db, database.File{
CreatedBy: owner.UserID,
})
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
OrganizationID: owner.OrganizationID,
InitiatorID: owner.UserID,
FileID: file.ID,
Tags: database.StringMap{
"custom": "true",
},
})
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: owner.OrganizationID,
JobID: versionJob.ID,
CreatedBy: owner.UserID,
})
template := dbgen.Template(t, db, database.Template{
OrganizationID: owner.OrganizationID,
ActiveVersionID: version.ID,
CreatedBy: owner.UserID,
})
ws := dbgen.Workspace(t, db, database.Workspace{
OwnerID: owner.UserID,
OrganizationID: owner.OrganizationID,
TemplateID: template.ID,
// Generate unique name for the workspace
Name: "test-workspace-" + uuid.New().String(),
})
jobID := uuid.New()
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
ID: jobID,
OrganizationID: owner.OrganizationID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
Tags: database.StringMap{jobID.String(): "true"},
// Create multiple builds
var buildNumber int32
makeBuild := func() database.WorkspaceBuild {
buildNumber++
jobID := uuid.New()
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
ID: jobID,
OrganizationID: owner.OrganizationID,
Tags: database.StringMap{jobID.String(): "true"},
})
return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID,
TemplateVersionID: version.ID,
InitiatorID: owner.UserID,
JobID: job.ID,
BuildNumber: buildNumber,
})
}
makeBuild()
makeBuild()
latestBuild := makeBuild()
// Add provisioner timings
dbgen.ProvisionerJobTimings(t, db, latestBuild, 5)
// Add agent script timings
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: latestBuild.JobID,
})
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID,
TemplateVersionID: version.ID,
BuildNumber: 1,
Transition: database.WorkspaceTransitionStart,
InitiatorID: owner.UserID,
JobID: job.ID,
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
return workspaceWithBuild{
Workspace: ws,
build: build,
}
}
makeProvisionerTimings := func(jobID uuid.UUID, count int) []database.ProvisionerJobTiming {
// Use the database.ProvisionerJobTiming struct to mock timings data instead
// of directly creating database.InsertProvisionerJobTimingsParams. This
// approach makes the mock data easier to understand, as
// database.InsertProvisionerJobTimingsParams requires slices of each field
// for batch inserts.
timings := make([]database.ProvisionerJobTiming, count)
now := time.Now()
for i := range count {
startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute)
endedAt := startedAt.Add(time.Minute)
timings[i] = database.ProvisionerJobTiming{
StartedAt: startedAt,
EndedAt: endedAt,
Stage: database.ProvisionerJobTimingStageInit,
Action: string(database.AuditActionCreate),
Source: "source",
Resource: fmt.Sprintf("resource[%d]", i),
}
}
insertParams := database.InsertProvisionerJobTimingsParams{
JobID: jobID,
}
for _, timing := range timings {
insertParams.StartedAt = append(insertParams.StartedAt, timing.StartedAt)
insertParams.EndedAt = append(insertParams.EndedAt, timing.EndedAt)
insertParams.Stage = append(insertParams.Stage, timing.Stage)
insertParams.Action = append(insertParams.Action, timing.Action)
insertParams.Source = append(insertParams.Source, timing.Source)
insertParams.Resource = append(insertParams.Resource, timing.Resource)
}
return dbgen.ProvisionerJobTimings(t, db, insertParams)
}
// Given
testCases := []struct {
name string
provisionerTimings int
workspace workspaceWithBuild
error bool
}{
{
name: "workspace with 5 provisioner timings",
provisionerTimings: 5,
workspace: makeWorkspace(),
},
{
name: "workspace with 2 provisioner timings",
provisionerTimings: 2,
workspace: makeWorkspace(),
},
{
name: "workspace with 0 provisioner timings",
provisionerTimings: 0,
workspace: makeWorkspace(),
},
{
name: "workspace not found",
provisionerTimings: 0,
workspace: workspaceWithBuild{},
error: true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Generate timings based on test config
generatedTimings := makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings)
res, err := client.WorkspaceTimings(context.Background(), tc.workspace.ID)
// When error is expected, than an error is returned
if tc.error {
require.Error(t, err)
return
}
// When success is expected, than no error is returned and the length and
// fields are correctly returned
require.NoError(t, err)
require.Len(t, res.ProvisionerTimings, tc.provisionerTimings)
for i := range res.ProvisionerTimings {
timingRes := res.ProvisionerTimings[i]
genTiming := generatedTimings[i]
require.Equal(t, genTiming.Resource, timingRes.Resource)
require.Equal(t, genTiming.Action, timingRes.Action)
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
require.Equal(t, genTiming.Source, timingRes.Source)
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
}
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
}
dbgen.WorkspaceAgentScriptTimings(t, db, script, 3)
// When: fetching the timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceTimings(ctx, ws.ID)
// Then: expect the timings to be returned
require.NoError(t, err)
require.Len(t, res.ProvisionerTimings, 5)
require.Len(t, res.AgentScriptTimings, 3)
})
t.Run("NonExistentWorkspace", func(t *testing.T) {
t.Parallel()
// When: fetching an inexistent workspace
workspaceID := uuid.New()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
_, err := client.WorkspaceTimings(ctx, workspaceID)
// Then: expect a not found error
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
})
}