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")
})
}
+38
View File
@@ -174,3 +174,41 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID)
var params []WorkspaceBuildParameter
return params, json.NewDecoder(res.Body).Decode(&params)
}
type ProvisionerTiming struct {
JobID uuid.UUID `json:"job_id" format:"uuid"`
StartedAt time.Time `json:"started_at" format:"date-time"`
EndedAt time.Time `json:"ended_at" format:"date-time"`
Stage string `json:"stage"`
Source string `json:"source"`
Action string `json:"action"`
Resource string `json:"resource"`
}
type AgentScriptTiming struct {
StartedAt time.Time `json:"started_at" format:"date-time"`
EndedAt time.Time `json:"ended_at" format:"date-time"`
ExitCode int32 `json:"exit_code"`
Stage string `json:"stage"`
Status string `json:"status"`
DisplayName string `json:"display_name"`
}
type WorkspaceBuildTimings struct {
ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"`
AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"`
}
func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (WorkspaceBuildTimings, error) {
path := fmt.Sprintf("/api/v2/workspacebuilds/%s/timings", build.String())
res, err := c.Request(ctx, http.MethodGet, path, nil)
if err != nil {
return WorkspaceBuildTimings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return WorkspaceBuildTimings{}, ReadBodyAsError(res)
}
var timings WorkspaceBuildTimings
return timings, json.NewDecoder(res.Body).Decode(&timings)
}
+4 -19
View File
@@ -626,32 +626,17 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID)
return nil
}
type ProvisionerTiming struct {
JobID uuid.UUID `json:"job_id" format:"uuid"`
StartedAt time.Time `json:"started_at" format:"date-time"`
EndedAt time.Time `json:"ended_at" format:"date-time"`
Stage string `json:"stage"`
Source string `json:"source"`
Action string `json:"action"`
Resource string `json:"resource"`
}
type WorkspaceTimings struct {
ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"`
// TODO: Add AgentScriptTimings when it is done https://github.com/coder/coder/issues/14630
}
func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceTimings, error) {
func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceBuildTimings, error) {
path := fmt.Sprintf("/api/v2/workspaces/%s/timings", id.String())
res, err := c.Request(ctx, http.MethodGet, path, nil)
if err != nil {
return WorkspaceTimings{}, err
return WorkspaceBuildTimings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return WorkspaceTimings{}, ReadBodyAsError(res)
return WorkspaceBuildTimings{}, ReadBodyAsError(res)
}
var timings WorkspaceTimings
var timings WorkspaceBuildTimings
return timings, json.NewDecoder(res.Body).Decode(&timings)
}
+57
View File
@@ -991,6 +991,63 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get workspace build timings by ID
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/timings \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /workspacebuilds/{workspacebuild}/timings`
### Parameters
| Name | In | Type | Required | Description |
| ---------------- | ---- | ------------ | -------- | ------------------ |
| `workspacebuild` | path | string(uuid) | true | Workspace build ID |
### Example responses
> 200 Response
```json
{
"agent_script_timings": [
{
"display_name": "string",
"ended_at": "2019-08-24T14:15:22Z",
"exit_code": 0,
"stage": "string",
"started_at": "2019-08-24T14:15:22Z",
"status": "string"
}
],
"provisioner_timings": [
{
"action": "string",
"ended_at": "2019-08-24T14:15:22Z",
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
"resource": "string",
"source": "string",
"stage": "string",
"started_at": "2019-08-24T14:15:22Z"
}
]
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuildTimings](schemas.md#codersdkworkspacebuildtimings) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get workspace builds by workspace ID
### Code samples
+59 -24
View File
@@ -349,6 +349,30 @@
| --------- | ------ | -------- | ------------ | ----------- |
| `license` | string | true | | |
## codersdk.AgentScriptTiming
```json
{
"display_name": "string",
"ended_at": "2019-08-24T14:15:22Z",
"exit_code": 0,
"stage": "string",
"started_at": "2019-08-24T14:15:22Z",
"status": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| -------------- | ------- | -------- | ------------ | ----------- |
| `display_name` | string | false | | |
| `ended_at` | string | false | | |
| `exit_code` | integer | false | | |
| `stage` | string | false | | |
| `started_at` | string | false | | |
| `status` | string | false | | |
## codersdk.AgentSubsystem
```json
@@ -7348,6 +7372,41 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `name` | string | false | | |
| `value` | string | false | | |
## codersdk.WorkspaceBuildTimings
```json
{
"agent_script_timings": [
{
"display_name": "string",
"ended_at": "2019-08-24T14:15:22Z",
"exit_code": 0,
"stage": "string",
"started_at": "2019-08-24T14:15:22Z",
"status": "string"
}
],
"provisioner_timings": [
{
"action": "string",
"ended_at": "2019-08-24T14:15:22Z",
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
"resource": "string",
"source": "string",
"stage": "string",
"started_at": "2019-08-24T14:15:22Z"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | |
| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | |
## codersdk.WorkspaceConnectionLatencyMS
```json
@@ -7675,30 +7734,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `deleting` |
| `deleted` |
## codersdk.WorkspaceTimings
```json
{
"provisioner_timings": [
{
"action": "string",
"ended_at": "2019-08-24T14:15:22Z",
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
"resource": "string",
"source": "string",
"stage": "string",
"started_at": "2019-08-24T14:15:22Z"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| --------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | |
## codersdk.WorkspaceTransition
```json
+13 -3
View File
@@ -1641,6 +1641,16 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \
```json
{
"agent_script_timings": [
{
"display_name": "string",
"ended_at": "2019-08-24T14:15:22Z",
"exit_code": 0,
"stage": "string",
"started_at": "2019-08-24T14:15:22Z",
"status": "string"
}
],
"provisioner_timings": [
{
"action": "string",
@@ -1657,9 +1667,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceTimings](schemas.md#codersdkworkspacetimings) |
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceBuildTimings](schemas.md#codersdkworkspacebuildtimings) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+17 -6
View File
@@ -32,6 +32,16 @@ export interface AddLicenseRequest {
readonly license: string;
}
// From codersdk/workspacebuilds.go
export interface AgentScriptTiming {
readonly started_at: string;
readonly ended_at: string;
readonly exit_code: number;
readonly stage: string;
readonly status: string;
readonly display_name: string;
}
// From codersdk/templates.go
export interface AgentStatsReportResponse {
readonly num_comms: number;
@@ -1088,7 +1098,7 @@ export interface ProvisionerKeyDaemons {
// From codersdk/provisionerdaemons.go
export type ProvisionerKeyTags = Record<string, string>
// From codersdk/workspaces.go
// From codersdk/workspacebuilds.go
export interface ProvisionerTiming {
readonly job_id: string;
readonly started_at: string;
@@ -1971,6 +1981,12 @@ export interface WorkspaceBuildParameter {
readonly value: string;
}
// From codersdk/workspacebuilds.go
export interface WorkspaceBuildTimings {
readonly provisioner_timings: Readonly<Array<ProvisionerTiming>>;
readonly agent_script_timings: Readonly<Array<AgentScriptTiming>>;
}
// From codersdk/workspaces.go
export interface WorkspaceBuildsRequest extends Pagination {
readonly since?: string;
@@ -2062,11 +2078,6 @@ export interface WorkspaceResourceMetadata {
readonly sensitive: boolean;
}
// From codersdk/workspaces.go
export interface WorkspaceTimings {
readonly provisioner_timings: Readonly<Array<ProvisionerTiming>>;
}
// From codersdk/workspaces.go
export interface WorkspacesRequest extends Pagination {
readonly q?: string;