From 1b62a395bd644b5685fb2eb89304748e2ad031e5 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 1 Oct 2024 19:02:09 +0000 Subject: [PATCH 01/17] feat(coderd): return agent script timings Add the agent script timings into the `/workspaces/:workspaceId/timings` response. --- coderd/agentapi/scripts.go | 2 +- coderd/apidoc/docs.go | 32 +++++ coderd/apidoc/swagger.json | 32 +++++ coderd/database/dbauthz/dbauthz.go | 11 +- coderd/database/dbgen/dbgen.go | 12 ++ coderd/database/dbmem/dbmem.go | 87 +++++++++++--- coderd/database/dbmetrics/dbmetrics.go | 13 +- coderd/database/dbmock/dbmock.go | 22 +++- coderd/database/querier.go | 3 +- coderd/database/queries.sql.go | 69 ++++++++++- coderd/database/queries/workspaceagents.sql | 14 ++- coderd/workspaces.go | 23 ++++ coderd/workspaces_test.go | 126 +++++++++++++++----- codersdk/workspaces.go | 12 +- docs/reference/api/schemas.md | 44 ++++++- docs/reference/api/workspaces.md | 11 ++ site/src/api/typesGenerated.ts | 12 ++ 17 files changed, 464 insertions(+), 61 deletions(-) diff --git a/coderd/agentapi/scripts.go b/coderd/agentapi/scripts.go index 3aa085ade8a03..9f5e098e3c721 100644 --- a/coderd/agentapi/scripts.go +++ b/coderd/agentapi/scripts.go @@ -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, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index de2bb1e6b91a9..1c2bb06e9835f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8884,6 +8884,32 @@ const docTemplate = `{ } } }, + "codersdk.AgentScriptTiming": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "ended_at": { + "type": "string" + }, + "exit_code": { + "type": "integer" + }, + "script_id": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "codersdk.AgentSubsystem": { "type": "string", "enum": [ @@ -14807,6 +14833,12 @@ const docTemplate = `{ "codersdk.WorkspaceTimings": { "type": "object", "properties": { + "agent_script_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentScriptTiming" + } + }, "provisioner_timings": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ed640dd50262f..da3cde7ca6ba0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7874,6 +7874,32 @@ } } }, + "codersdk.AgentScriptTiming": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "ended_at": { + "type": "string" + }, + "exit_code": { + "type": "integer" + }, + "script_id": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "codersdk.AgentSubsystem": { "type": "string", "enum": ["envbox", "envbuilder", "exectrace"], @@ -13496,6 +13522,12 @@ "codersdk.WorkspaceTimings": { "type": "object", "properties": { + "agent_script_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentScriptTiming" + } + }, "provisioner_timings": { "type": "array", "items": { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 6436e7c6e3425..7903121607afb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2421,6 +2421,13 @@ func (q *querier) GetWorkspaceAgentPortShare(ctx context.Context, arg database.G return q.db.GetWorkspaceAgentPortShare(ctx, arg) } +func (q *querier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspaceID) +} + 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) } diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index d18da855be7b8..f5ee5e80e55e0 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -189,6 +189,18 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen return agt } +func WorkspaceAgentScripts(t testing.TB, db database.Store, orig database.InsertWorkspaceAgentScriptsParams) []database.WorkspaceAgentScript { + scripts, err := db.InsertWorkspaceAgentScripts(genCtx, orig) + require.NoError(t, err, "insert workspace agent script") + return scripts +} + +func WorkspaceAgentScriptTiming(t testing.TB, db database.Store, orig database.InsertWorkspaceAgentScriptTimingsParams) database.WorkspaceAgentScriptTiming { + timing, err := db.InsertWorkspaceAgentScriptTimings(genCtx, orig) + 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() diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 09dfa3e7306db..4b4cb93deb179 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5793,6 +5793,67 @@ func (q *FakeQuerier) GetWorkspaceAgentPortShare(_ context.Context, arg database return database.WorkspaceAgentPortShare{}, sql.ErrNoRows } +func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) + if err != nil { + return nil, err + } + + resources, err := q.GetWorkspaceResourcesByJobID(ctx, build.JobID) + if err != nil { + return nil, 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, 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, err + } + scriptIDs := make([]uuid.UUID, 0, len(scripts)) + for _, script := range scripts { + scriptIDs = append(scriptIDs, script.ID) + } + + rows := []database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow{} + for _, t := range q.workspaceAgentScriptTimings { + if slices.Contains(scriptIDs, t.ScriptID) { + var script database.WorkspaceAgentScript + for _, s := range scripts { + if s.ID == t.ScriptID { + script = s + break + } + } + + rows = append(rows, database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow{ + 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 +7905,26 @@ 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{ + ScriptID: arg.ScriptID, + StartedAt: arg.StartedAt, + EndedAt: arg.EndedAt, + ExitCode: arg.ExitCode, + Stage: arg.Stage, + Status: arg.Status, + } + q.workspaceAgentScriptTimings = append(q.workspaceAgentScriptTimings, timing) - return nil + return timing, nil } func (q *FakeQuerier) InsertWorkspaceAgentScripts(_ context.Context, arg database.InsertWorkspaceAgentScriptsParams) ([]database.WorkspaceAgentScript, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index b050a4ce9afc4..16217a8df6665 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1404,6 +1404,13 @@ func (m metricsStore) GetWorkspaceAgentPortShare(ctx context.Context, arg databa return r0, r1 } +func (m metricsStore) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspaceID) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentScriptTimingsByWorkspaceID").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) { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 3c7dbd6d9b958..c0c96582ebcff 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -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) } +// GetWorkspaceAgentScriptTimingsByWorkspaceID mocks base method. +func (m *MockStore) GetWorkspaceAgentScriptTimingsByWorkspaceID(arg0 context.Context, arg1 uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAgentScriptTimingsByWorkspaceID", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAgentScriptTimingsByWorkspaceID indicates an expected call of GetWorkspaceAgentScriptTimingsByWorkspaceID. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptTimingsByWorkspaceID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentScriptTimingsByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptTimingsByWorkspaceID), 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. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d71c54e008350..6e01b574b5cda 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -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) + GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, 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) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f5b2943d1fa04..f972eace8733e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11420,6 +11420,57 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks return items, nil } +const getWorkspaceAgentScriptTimingsByWorkspaceID = `-- name: GetWorkspaceAgentScriptTimingsByWorkspaceID :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.workspace_id = $1 +` + +type GetWorkspaceAgentScriptTimingsByWorkspaceIDRow 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) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByWorkspaceID, workspaceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspaceAgentScriptTimingsByWorkspaceIDRow + for rows.Next() { + var i GetWorkspaceAgentScriptTimingsByWorkspaceIDRow + 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 @@ -11879,7 +11930,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, @@ -11891,6 +11942,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 { @@ -11902,8 +11954,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, @@ -11911,7 +11963,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 diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 1020aba219920..9ca28935522a9 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -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: GetWorkspaceAgentScriptTimingsByWorkspaceID :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.workspace_id = $1; \ No newline at end of file diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 30018a8c6b4d0..a4a231decf349 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1772,9 +1772,20 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { return } + agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspace.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent script timings.", + Detail: err.Error(), + }) + return + } + res := codersdk.WorkspaceTimings{ 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, @@ -1786,6 +1797,18 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { EndedAt: t.EndedAt, }) } + for _, t := range agentScriptTimings { + res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{ + ScriptID: t.ScriptID, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: string(t.Stage), + Status: string(t.Status), + DisplayName: t.DisplayName, + }) + } + httpapi.Write(ctx, rw, http.StatusOK, res) } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 4f5064de48cbe..709a6661437d7 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3592,11 +3592,12 @@ func TestWorkspaceTimings(t *testing.T) { // 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 { + type testWorkspace struct { database.Workspace - build database.WorkspaceBuild + build database.WorkspaceBuild + script database.WorkspaceAgentScript } - makeWorkspace := func() workspaceWithBuild { + makeWorkspace := func() testWorkspace { ws := dbgen.Workspace(t, db, database.Workspace{ OwnerID: owner.UserID, OrganizationID: owner.OrganizationID, @@ -3619,9 +3620,36 @@ func TestWorkspaceTimings(t *testing.T) { InitiatorID: owner.UserID, JobID: job.ID, }) - return workspaceWithBuild{ + // Create a resource, agent, and script to test the timing of agent scripts + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: jobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + scripts := dbgen.WorkspaceAgentScripts(t, db, database.InsertWorkspaceAgentScriptsParams{ + WorkspaceAgentID: agent.ID, + CreatedAt: time.Now(), + LogSourceID: []uuid.UUID{ + uuid.New(), + }, + LogPath: []string{""}, + Script: []string{""}, + Cron: []string{""}, + StartBlocksLogin: []bool{false}, + RunOnStart: []bool{false}, + RunOnStop: []bool{false}, + TimeoutSeconds: []int32{0}, + DisplayName: []string{""}, + ID: []uuid.UUID{ + uuid.New(), + }, + }) + + return testWorkspace{ Workspace: ws, build: build, + script: scripts[0], } } @@ -3659,58 +3687,82 @@ func TestWorkspaceTimings(t *testing.T) { return dbgen.ProvisionerJobTimings(t, db, insertParams) } + makeAgentScriptTimings := func(scriptID uuid.UUID, count int) []database.WorkspaceAgentScriptTiming { + newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) + now := time.Now() + for i := range count { + startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) + endedAt := startedAt.Add(time.Minute) + newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ + StartedAt: startedAt, + EndedAt: endedAt, + Stage: database.WorkspaceAgentScriptTimingStageStart, + ScriptID: scriptID, + ExitCode: 0, + Status: database.WorkspaceAgentScriptTimingStatusOk, + } + } + + timings := make([]database.WorkspaceAgentScriptTiming, 0) + for _, newTiming := range newTimings { + timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) + timings = append(timings, timing) + } + + return timings + } + // Given testCases := []struct { - name string provisionerTimings int - workspace workspaceWithBuild - error bool + agentScriptTimings int + workspace testWorkspace }{ { - name: "workspace with 5 provisioner timings", + workspace: makeWorkspace(), + }, + { provisionerTimings: 5, workspace: makeWorkspace(), }, { - name: "workspace with 2 provisioner timings", provisionerTimings: 2, workspace: makeWorkspace(), }, { - name: "workspace with 0 provisioner timings", - provisionerTimings: 0, + agentScriptTimings: 5, workspace: makeWorkspace(), }, { - name: "workspace not found", - provisionerTimings: 0, - workspace: workspaceWithBuild{}, - error: true, + agentScriptTimings: 2, + workspace: makeWorkspace(), + }, + { + provisionerTimings: 3, + agentScriptTimings: 4, + workspace: makeWorkspace(), }, } for _, tc := range testCases { tc := tc - t.Run(tc.name, func(t *testing.T) { + name := fmt.Sprintf("provisionerTimings=%d, agentScriptTimings=%d", tc.provisionerTimings, tc.agentScriptTimings) + t.Run(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) + genProvisionerTimings := makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings) + genAgentScriptTimings := makeAgentScriptTimings(tc.workspace.script.ID, tc.agentScriptTimings) - // 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 + res, err := client.WorkspaceTimings(context.Background(), tc.workspace.ID) require.NoError(t, err) require.Len(t, res.ProvisionerTimings, tc.provisionerTimings) + require.Len(t, res.AgentScriptTimings, tc.agentScriptTimings) + + // Verify timings data for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] - genTiming := generatedTimings[i] + genTiming := genProvisionerTimings[i] require.Equal(t, genTiming.Resource, timingRes.Resource) require.Equal(t, genTiming.Action, timingRes.Action) require.Equal(t, string(genTiming.Stage), timingRes.Stage) @@ -3719,6 +3771,26 @@ func TestWorkspaceTimings(t *testing.T) { require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) } + for i := range res.AgentScriptTimings { + timingRes := res.AgentScriptTimings[i] + genTiming := genAgentScriptTimings[i] + require.Equal(t, genTiming.ScriptID.String(), timingRes.ScriptID.String()) + 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()) + } }) } } + +func TestWorkspaceTimings_NotFound(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + + _, err := client.WorkspaceTimings(context.Background(), uuid.New()) + require.Contains(t, err.Error(), "not found") +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 658af09cdda61..d28b3e8945149 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -636,9 +636,19 @@ type ProvisionerTiming struct { Resource string `json:"resource"` } +type AgentScriptTiming struct { + ScriptID uuid.UUID `json:"script_id"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + ExitCode int32 `json:"exit_code"` + Stage string `json:"stage"` + Status string `json:"status"` + DisplayName string `json:"display_name"` +} + type WorkspaceTimings struct { ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` - // TODO: Add AgentScriptTimings when it is done https://github.com/coder/coder/issues/14630 + AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` } func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceTimings, error) { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 00004bb83e74b..eee3df30375fc 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -349,6 +349,32 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | +## codersdk.AgentScriptTiming + +```json +{ + "display_name": "string", + "ended_at": "string", + "exit_code": 0, + "script_id": "string", + "stage": "string", + "started_at": "string", + "status": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------- | -------- | ------------ | ----------- | +| `display_name` | string | false | | | +| `ended_at` | string | false | | | +| `exit_code` | integer | false | | | +| `script_id` | string | false | | | +| `stage` | string | false | | | +| `started_at` | string | false | | | +| `status` | string | false | | | + ## codersdk.AgentSubsystem ```json @@ -7609,6 +7635,17 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "agent_script_timings": [ + { + "display_name": "string", + "ended_at": "string", + "exit_code": 0, + "script_id": "string", + "stage": "string", + "started_at": "string", + "status": "string" + } + ], "provisioner_timings": [ { "action": "string", @@ -7625,9 +7662,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| --------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | -| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | +| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | | +| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | ## codersdk.WorkspaceTransition diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 2987cf65159e4..77dfbea443711 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1641,6 +1641,17 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ ```json { + "agent_script_timings": [ + { + "display_name": "string", + "ended_at": "string", + "exit_code": 0, + "script_id": "string", + "stage": "string", + "started_at": "string", + "status": "string" + } + ], "provisioner_timings": [ { "action": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ebc296f57db1b..5e7048eb035d4 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,6 +32,17 @@ export interface AddLicenseRequest { readonly license: string; } +// From codersdk/workspaces.go +export interface AgentScriptTiming { + readonly script_id: string; + 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; @@ -2044,6 +2055,7 @@ export interface WorkspaceResourceMetadata { // From codersdk/workspaces.go export interface WorkspaceTimings { readonly provisioner_timings: Readonly>; + readonly agent_script_timings: Readonly>; } // From codersdk/workspaces.go From 3daab59bc83921b1a4d3fc86a7585bff0e4982aa Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 2 Oct 2024 17:20:19 +0000 Subject: [PATCH 02/17] Apply initial suggestions and improvements --- coderd/apidoc/docs.go | 3 -- coderd/apidoc/swagger.json | 3 -- coderd/database/dbauthz/dbauthz.go | 4 +- coderd/database/dbmem/dbmem.go | 42 +++++++++++---------- coderd/database/dbmetrics/dbmetrics.go | 6 +-- coderd/database/dbmock/dbmock.go | 14 +++---- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 14 +++---- coderd/database/queries/workspaceagents.sql | 4 +- coderd/workspaces.go | 3 +- coderd/workspaces_test.go | 1 - codersdk/workspaces.go | 1 - docs/reference/api/schemas.md | 3 -- docs/reference/api/workspaces.md | 1 - site/src/api/typesGenerated.ts | 1 - 15 files changed, 45 insertions(+), 57 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1c2bb06e9835f..bacd7a37bee14 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8896,9 +8896,6 @@ const docTemplate = `{ "exit_code": { "type": "integer" }, - "script_id": { - "type": "string" - }, "stage": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index da3cde7ca6ba0..f5625e7b3a9f3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7886,9 +7886,6 @@ "exit_code": { "type": "integer" }, - "script_id": { - "type": "string" - }, "stage": { "type": "string" }, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 7903121607afb..25d0c94999948 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2421,11 +2421,11 @@ func (q *querier) GetWorkspaceAgentPortShare(ctx context.Context, arg database.G return q.db.GetWorkspaceAgentPortShare(ctx, arg) } -func (q *querier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { +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.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspaceID) + return q.db.GetWorkspaceAgentScriptTimingsByBuildID(ctx, id) } func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 4b4cb93deb179..50639f44b9591 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5793,11 +5793,11 @@ func (q *FakeQuerier) GetWorkspaceAgentPortShare(_ context.Context, arg database return database.WorkspaceAgentPortShare{}, sql.ErrNoRows } -func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { +func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) + build, err := q.GetWorkspaceBuildByID(ctx, id) if err != nil { return nil, err } @@ -5829,27 +5829,29 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Co scriptIDs = append(scriptIDs, script.ID) } - rows := []database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow{} + rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{} for _, t := range q.workspaceAgentScriptTimings { - if slices.Contains(scriptIDs, t.ScriptID) { - var script database.WorkspaceAgentScript - for _, s := range scripts { - if s.ID == t.ScriptID { - script = s - break - } - } + if !slice.Contains(scriptIDs, t.ScriptID) { + continue + } - rows = append(rows, database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow{ - ScriptID: t.ScriptID, - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: t.Stage, - Status: t.Status, - DisplayName: script.DisplayName, - }) + 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 } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 16217a8df6665..ec607d1bf52bf 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1404,10 +1404,10 @@ func (m metricsStore) GetWorkspaceAgentPortShare(ctx context.Context, arg databa return r0, r1 } -func (m metricsStore) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { +func (m metricsStore) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { start := time.Now() - r0, r1 := m.s.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspaceID) - m.queryLatencies.WithLabelValues("GetWorkspaceAgentScriptTimingsByWorkspaceID").Observe(time.Since(start).Seconds()) + r0, r1 := m.s.GetWorkspaceAgentScriptTimingsByBuildID(ctx, id) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentScriptTimingsByBuildID").Observe(time.Since(start).Seconds()) return r0, r1 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index c0c96582ebcff..747f6acd16eb3 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2933,19 +2933,19 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentPortShare(arg0, arg1 any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentPortShare), arg0, arg1) } -// GetWorkspaceAgentScriptTimingsByWorkspaceID mocks base method. -func (m *MockStore) GetWorkspaceAgentScriptTimingsByWorkspaceID(arg0 context.Context, arg1 uuid.UUID) ([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { +// 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, "GetWorkspaceAgentScriptTimingsByWorkspaceID", arg0, arg1) - ret0, _ := ret[0].([]database.GetWorkspaceAgentScriptTimingsByWorkspaceIDRow) + ret := m.ctrl.Call(m, "GetWorkspaceAgentScriptTimingsByBuildID", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentScriptTimingsByWorkspaceID indicates an expected call of GetWorkspaceAgentScriptTimingsByWorkspaceID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptTimingsByWorkspaceID(arg0, arg1 any) *gomock.Call { +// 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, "GetWorkspaceAgentScriptTimingsByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptTimingsByWorkspaceID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentScriptTimingsByBuildID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptTimingsByBuildID), arg0, arg1) } // GetWorkspaceAgentScriptsByAgentIDs mocks base method. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6e01b574b5cda..2a93c6d2c4ede 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -298,7 +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) - GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, 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) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f972eace8733e..4d3837b9493ec 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11420,17 +11420,17 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks return items, nil } -const getWorkspaceAgentScriptTimingsByWorkspaceID = `-- name: GetWorkspaceAgentScriptTimingsByWorkspaceID :many +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.workspace_id = $1 +WHERE workspace_builds.id = $1 ` -type GetWorkspaceAgentScriptTimingsByWorkspaceIDRow struct { +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"` @@ -11440,15 +11440,15 @@ type GetWorkspaceAgentScriptTimingsByWorkspaceIDRow struct { DisplayName string `db:"display_name" json:"display_name"` } -func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByWorkspaceIDRow, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByWorkspaceID, workspaceID) +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 []GetWorkspaceAgentScriptTimingsByWorkspaceIDRow + var items []GetWorkspaceAgentScriptTimingsByBuildIDRow for rows.Next() { - var i GetWorkspaceAgentScriptTimingsByWorkspaceIDRow + var i GetWorkspaceAgentScriptTimingsByBuildIDRow if err := rows.Scan( &i.ScriptID, &i.StartedAt, diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 9ca28935522a9..2c26740db1d88 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -302,11 +302,11 @@ VALUES ($1, $2, $3, $4, $5, $6) RETURNING workspace_agent_script_timings.*; --- name: GetWorkspaceAgentScriptTimingsByWorkspaceID :many +-- 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.workspace_id = $1; \ No newline at end of file +WHERE workspace_builds.id = $1; \ No newline at end of file diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a4a231decf349..cd7a2840bad1d 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1772,7 +1772,7 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { return } - agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByWorkspaceID(ctx, workspace.ID) + agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace agent script timings.", @@ -1799,7 +1799,6 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { } for _, t := range agentScriptTimings { res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{ - ScriptID: t.ScriptID, StartedAt: t.StartedAt, EndedAt: t.EndedAt, ExitCode: t.ExitCode, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 709a6661437d7..1ca57d89a2e55 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3774,7 +3774,6 @@ func TestWorkspaceTimings(t *testing.T) { for i := range res.AgentScriptTimings { timingRes := res.AgentScriptTimings[i] genTiming := genAgentScriptTimings[i] - require.Equal(t, genTiming.ScriptID.String(), timingRes.ScriptID.String()) require.Equal(t, genTiming.ExitCode, timingRes.ExitCode) require.Equal(t, string(genTiming.Status), timingRes.Status) require.Equal(t, string(genTiming.Stage), timingRes.Stage) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index d28b3e8945149..b09fd72ea02e3 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -637,7 +637,6 @@ type ProvisionerTiming struct { } type AgentScriptTiming struct { - ScriptID uuid.UUID `json:"script_id"` StartedAt time.Time `json:"started_at"` EndedAt time.Time `json:"ended_at"` ExitCode int32 `json:"exit_code"` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index eee3df30375fc..71d28cb46e8c4 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -356,7 +356,6 @@ "display_name": "string", "ended_at": "string", "exit_code": 0, - "script_id": "string", "stage": "string", "started_at": "string", "status": "string" @@ -370,7 +369,6 @@ | `display_name` | string | false | | | | `ended_at` | string | false | | | | `exit_code` | integer | false | | | -| `script_id` | string | false | | | | `stage` | string | false | | | | `started_at` | string | false | | | | `status` | string | false | | | @@ -7640,7 +7638,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| "display_name": "string", "ended_at": "string", "exit_code": 0, - "script_id": "string", "stage": "string", "started_at": "string", "status": "string" diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 77dfbea443711..ab4372002d8a7 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1646,7 +1646,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "display_name": "string", "ended_at": "string", "exit_code": 0, - "script_id": "string", "stage": "string", "started_at": "string", "status": "string" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5e7048eb035d4..23e5397403edc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -34,7 +34,6 @@ export interface AddLicenseRequest { // From codersdk/workspaces.go export interface AgentScriptTiming { - readonly script_id: string; readonly started_at: string; readonly ended_at: string; readonly exit_code: number; From 15ca63842c023ba0f36ecc941c1caadc7ef4d584 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 3 Oct 2024 17:08:11 +0000 Subject: [PATCH 03/17] Move timings to build and improve tests --- coderd/coderd.go | 2 +- coderd/workspacebuilds.go | 62 +++++++++ coderd/workspacebuilds_test.go | 194 +++++++++++++++++++++++++++ coderd/workspaces.go | 71 ---------- coderd/workspaces_test.go | 237 --------------------------------- codersdk/workspacebuilds.go | 38 ++++++ codersdk/workspaces.go | 38 ------ 7 files changed, 295 insertions(+), 347 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index cbe008a726636..d73d76c86c7db 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1150,7 +1150,6 @@ func New(options *Options) *API { r.Post("/", api.postWorkspaceAgentPortShare) r.Delete("/", api.deleteWorkspaceAgentPortShare) }) - r.Get("/timings", api.workspaceTimings) }) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { @@ -1165,6 +1164,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) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index e04e585d4aa53..9ea6a77554098 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -647,6 +647,68 @@ 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) + ) + + provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace timings.", + Detail: err.Error(), + }) + return + } + + agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent script timings.", + Detail: err.Error(), + }) + return + } + + 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, + }) + } + + httpapi.Write(ctx, rw, http.StatusOK, res) +} + type workspaceBuildsData struct { users []database.User jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 757dac7fb6326..a7e9e2622bb75 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -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" @@ -1180,3 +1182,195 @@ 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, + 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, + }) + + makeProvisionerTimings := func(build database.WorkspaceBuild, 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: build.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) + } + + makeAgentScriptTimings := func(build database.WorkspaceBuild, count int) []database.WorkspaceAgentScriptTiming { + // Create a resource, agent, and script to test the timing of agent scripts + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + scripts := dbgen.WorkspaceAgentScripts(t, db, database.InsertWorkspaceAgentScriptsParams{ + WorkspaceAgentID: agent.ID, + CreatedAt: time.Now(), + LogSourceID: []uuid.UUID{ + uuid.New(), + }, + LogPath: []string{""}, + Script: []string{""}, + Cron: []string{""}, + StartBlocksLogin: []bool{false}, + RunOnStart: []bool{false}, + RunOnStop: []bool{false}, + TimeoutSeconds: []int32{0}, + DisplayName: []string{""}, + ID: []uuid.UUID{ + uuid.New(), + }, + }) + + newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) + now := time.Now() + for i := range count { + startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) + endedAt := startedAt.Add(time.Minute) + newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ + StartedAt: startedAt, + EndedAt: endedAt, + Stage: database.WorkspaceAgentScriptTimingStageStart, + ScriptID: scripts[0].ID, + ExitCode: 0, + Status: database.WorkspaceAgentScriptTimingStatusOk, + } + } + + timings := make([]database.WorkspaceAgentScriptTiming, 0) + for _, newTiming := range newTimings { + timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) + timings = append(timings, timing) + } + + return timings + } + + // Given + testCases := []struct { + name string + provisionerTimings int + actionScriptTimings int + }{ + {name: "with empty provisioner timings", provisionerTimings: 0}, + {name: "with provisioner timings", provisionerTimings: 5}, + {name: "with empty agent script timings", actionScriptTimings: 0}, + {name: "with agent script timings", actionScriptTimings: 5}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Create a build to attach provisioner timings + 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"}, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: ws.ID, + TemplateVersionID: version.ID, + BuildNumber: 1, + Transition: database.WorkspaceTransitionStart, + InitiatorID: owner.UserID, + JobID: job.ID, + }) + + // Generate timings based on test config + genProvisionerTimings := makeProvisionerTimings(build, tc.provisionerTimings) + genAgentScriptTimings := makeAgentScriptTimings(build, tc.provisionerTimings) + + res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + require.NoError(t, err) + require.Len(t, res.ProvisionerTimings, tc.provisionerTimings) + + for i := range res.ProvisionerTimings { + timingRes := res.ProvisionerTimings[i] + genTiming := genProvisionerTimings[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()) + } + + for i := range res.AgentScriptTimings { + timingRes := res.AgentScriptTimings[i] + genTiming := genAgentScriptTimings[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()) + } + }) + } +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index cd7a2840bad1d..2c4f0b52c0116 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1740,77 +1740,6 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } } -// @Summary Get workspace timings by ID -// @ID get-workspace-timings-by-id -// @Security CoderSessionToken -// @Produce json -// @Tags Workspaces -// @Param workspace path string true "Workspace ID" format(uuid) -// @Success 200 {object} codersdk.WorkspaceTimings -// @Router /workspaces/{workspace}/timings [get] -func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - workspace = httpmw.WorkspaceParam(r) - ) - - build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build.", - Detail: err.Error(), - }) - return - } - - provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace timings.", - Detail: err.Error(), - }) - return - } - - agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agent script timings.", - Detail: err.Error(), - }) - return - } - - res := codersdk.WorkspaceTimings{ - 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, - }) - } - - httpapi.Write(ctx, rw, http.StatusOK, res) -} - type workspaceData struct { templates []database.Template builds []codersdk.WorkspaceBuild diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 1ca57d89a2e55..98f36c3b9a13e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3556,240 +3556,3 @@ 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, - }) - - // Since the tests run in parallel, we need to create a new workspace for - // each test to avoid fetching the wrong latest build. - type testWorkspace struct { - database.Workspace - build database.WorkspaceBuild - script database.WorkspaceAgentScript - } - makeWorkspace := func() testWorkspace { - 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"}, - }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: ws.ID, - TemplateVersionID: version.ID, - BuildNumber: 1, - Transition: database.WorkspaceTransitionStart, - InitiatorID: owner.UserID, - JobID: job.ID, - }) - // Create a resource, agent, and script to test the timing of agent scripts - resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ - JobID: jobID, - }) - agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ - ResourceID: resource.ID, - }) - scripts := dbgen.WorkspaceAgentScripts(t, db, database.InsertWorkspaceAgentScriptsParams{ - WorkspaceAgentID: agent.ID, - CreatedAt: time.Now(), - LogSourceID: []uuid.UUID{ - uuid.New(), - }, - LogPath: []string{""}, - Script: []string{""}, - Cron: []string{""}, - StartBlocksLogin: []bool{false}, - RunOnStart: []bool{false}, - RunOnStop: []bool{false}, - TimeoutSeconds: []int32{0}, - DisplayName: []string{""}, - ID: []uuid.UUID{ - uuid.New(), - }, - }) - - return testWorkspace{ - Workspace: ws, - build: build, - script: scripts[0], - } - } - - 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) - } - - makeAgentScriptTimings := func(scriptID uuid.UUID, count int) []database.WorkspaceAgentScriptTiming { - newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) - now := time.Now() - for i := range count { - startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) - endedAt := startedAt.Add(time.Minute) - newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ - StartedAt: startedAt, - EndedAt: endedAt, - Stage: database.WorkspaceAgentScriptTimingStageStart, - ScriptID: scriptID, - ExitCode: 0, - Status: database.WorkspaceAgentScriptTimingStatusOk, - } - } - - timings := make([]database.WorkspaceAgentScriptTiming, 0) - for _, newTiming := range newTimings { - timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) - timings = append(timings, timing) - } - - return timings - } - - // Given - testCases := []struct { - provisionerTimings int - agentScriptTimings int - workspace testWorkspace - }{ - { - workspace: makeWorkspace(), - }, - { - provisionerTimings: 5, - workspace: makeWorkspace(), - }, - { - provisionerTimings: 2, - workspace: makeWorkspace(), - }, - { - agentScriptTimings: 5, - workspace: makeWorkspace(), - }, - { - agentScriptTimings: 2, - workspace: makeWorkspace(), - }, - { - provisionerTimings: 3, - agentScriptTimings: 4, - workspace: makeWorkspace(), - }, - } - - for _, tc := range testCases { - tc := tc - name := fmt.Sprintf("provisionerTimings=%d, agentScriptTimings=%d", tc.provisionerTimings, tc.agentScriptTimings) - t.Run(name, func(t *testing.T) { - t.Parallel() - - // Generate timings based on test config - genProvisionerTimings := makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings) - genAgentScriptTimings := makeAgentScriptTimings(tc.workspace.script.ID, tc.agentScriptTimings) - - res, err := client.WorkspaceTimings(context.Background(), tc.workspace.ID) - require.NoError(t, err) - require.Len(t, res.ProvisionerTimings, tc.provisionerTimings) - require.Len(t, res.AgentScriptTimings, tc.agentScriptTimings) - - // Verify timings data - for i := range res.ProvisionerTimings { - timingRes := res.ProvisionerTimings[i] - genTiming := genProvisionerTimings[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()) - } - for i := range res.AgentScriptTimings { - timingRes := res.AgentScriptTimings[i] - genTiming := genAgentScriptTimings[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()) - } - }) - } -} - -func TestWorkspaceTimings_NotFound(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - coderdtest.CreateFirstUser(t, client) - - _, err := client.WorkspaceTimings(context.Background(), uuid.New()) - require.Contains(t, err.Error(), "not found") -} diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 682cb424af1b1..2c26978c8a2b8 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -174,3 +174,41 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) var params []WorkspaceBuildParameter return params, json.NewDecoder(res.Body).Decode(¶ms) } + +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"` + EndedAt time.Time `json:"ended_at"` + 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) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index b09fd72ea02e3..4e4b98fe8c243 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -626,44 +626,6 @@ 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 AgentScriptTiming struct { - StartedAt time.Time `json:"started_at"` - EndedAt time.Time `json:"ended_at"` - ExitCode int32 `json:"exit_code"` - Stage string `json:"stage"` - Status string `json:"status"` - DisplayName string `json:"display_name"` -} - -type WorkspaceTimings struct { - ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` - AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` -} - -func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceTimings, 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 - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return WorkspaceTimings{}, ReadBodyAsError(res) - } - var timings WorkspaceTimings - return timings, json.NewDecoder(res.Body).Decode(&timings) -} - // WorkspaceNotifyChannel is the PostgreSQL NOTIFY // channel to listen for updates on. The payload is empty, // because the size of a workspace payload can be very large. From ddde7a30cee4bbd4fc17d7a852dfa510aabddc56 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 3 Oct 2024 18:33:32 +0000 Subject: [PATCH 04/17] Fix lint and gen --- coderd/apidoc/docs.go | 69 ++++++++----------------------- coderd/apidoc/swagger.json | 65 ++++++++--------------------- coderd/database/dbmem/dbmem.go | 9 +--- docs/reference/api/schemas.md | 70 ++++++++++++++++---------------- docs/reference/api/workspaces.md | 57 -------------------------- site/src/api/typesGenerated.ts | 16 ++++---- 6 files changed, 78 insertions(+), 208 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3159e6cf27fbc..49ab32984a247 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8421,41 +8421,6 @@ const docTemplate = `{ } } }, - "/workspaces/{workspace}/timings": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Workspaces" - ], - "summary": "Get workspace timings by ID", - "operationId": "get-workspace-timings-by-id", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Workspace ID", - "name": "workspace", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.WorkspaceTimings" - } - } - } - } - }, "/workspaces/{workspace}/ttl": { "put": { "security": [ @@ -14619,6 +14584,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": { @@ -14862,23 +14844,6 @@ const docTemplate = `{ "WorkspaceStatusDeleted" ] }, - "codersdk.WorkspaceTimings": { - "type": "object", - "properties": { - "agent_script_timings": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AgentScriptTiming" - } - }, - "provisioner_timings": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ProvisionerTiming" - } - } - } - }, "codersdk.WorkspaceTransition": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c911e81af97b6..c8e669b3bac43 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7451,37 +7451,6 @@ } } }, - "/workspaces/{workspace}/timings": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Workspaces"], - "summary": "Get workspace timings by ID", - "operationId": "get-workspace-timings-by-id", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Workspace ID", - "name": "workspace", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.WorkspaceTimings" - } - } - } - } - }, "/workspaces/{workspace}/ttl": { "put": { "security": [ @@ -13308,6 +13277,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": { @@ -13547,23 +13533,6 @@ "WorkspaceStatusDeleted" ] }, - "codersdk.WorkspaceTimings": { - "type": "object", - "properties": { - "agent_script_timings": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AgentScriptTiming" - } - }, - "provisioner_timings": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.ProvisionerTiming" - } - } - } - }, "codersdk.WorkspaceTransition": { "type": "string", "enum": ["start", "stop", "delete"], diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 50639f44b9591..ec09aa5d9c2a4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7916,14 +7916,7 @@ func (q *FakeQuerier) InsertWorkspaceAgentScriptTimings(_ context.Context, arg d q.mutex.Lock() defer q.mutex.Unlock() - timing := 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 timing, nil diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 18d471ac56f6a..5e312a39cae6a 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7340,6 +7340,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": "string", + "exit_code": 0, + "stage": "string", + "started_at": "string", + "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 @@ -7667,41 +7702,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `deleting` | | `deleted` | -## codersdk.WorkspaceTimings - -```json -{ - "agent_script_timings": [ - { - "display_name": "string", - "ended_at": "string", - "exit_code": 0, - "stage": "string", - "started_at": "string", - "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.WorkspaceTransition ```json diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index ab4372002d8a7..fbf3171b86dc4 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1616,63 +1616,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/resolve-autos To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get workspace timings by ID - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /workspaces/{workspace}/timings` - -### Parameters - -| Name | In | Type | Required | Description | -| ----------- | ---- | ------------ | -------- | ------------ | -| `workspace` | path | string(uuid) | true | Workspace ID | - -### Example responses - -> 200 Response - -```json -{ - "agent_script_timings": [ - { - "display_name": "string", - "ended_at": "string", - "exit_code": 0, - "stage": "string", - "started_at": "string", - "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.WorkspaceTimings](schemas.md#codersdkworkspacetimings) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Update workspace TTL by ID ### Code samples diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d11b727c1d103..3927160cee3e1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,7 +32,7 @@ export interface AddLicenseRequest { readonly license: string; } -// From codersdk/workspaces.go +// From codersdk/workspacebuilds.go export interface AgentScriptTiming { readonly started_at: string; readonly ended_at: string; @@ -1091,7 +1091,7 @@ export interface ProvisionerKeyDaemons { // From codersdk/provisionerdaemons.go export type ProvisionerKeyTags = Record -// From codersdk/workspaces.go +// From codersdk/workspacebuilds.go export interface ProvisionerTiming { readonly job_id: string; readonly started_at: string; @@ -1969,6 +1969,12 @@ export interface WorkspaceBuildParameter { readonly value: string; } +// From codersdk/workspacebuilds.go +export interface WorkspaceBuildTimings { + readonly provisioner_timings: Readonly>; + readonly agent_script_timings: Readonly>; +} + // From codersdk/workspaces.go export interface WorkspaceBuildsRequest extends Pagination { readonly since?: string; @@ -2060,12 +2066,6 @@ export interface WorkspaceResourceMetadata { readonly sensitive: boolean; } -// From codersdk/workspaces.go -export interface WorkspaceTimings { - readonly provisioner_timings: Readonly>; - readonly agent_script_timings: Readonly>; -} - // From codersdk/workspaces.go export interface WorkspacesRequest extends Pagination { readonly q?: string; From 5abc511cff308b2ec97c54d3014b6d2cc8997f42 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 3 Oct 2024 18:43:44 +0000 Subject: [PATCH 05/17] Fix @Router notation --- coderd/apidoc/docs.go | 35 ++++++++++++++++++++++ coderd/apidoc/swagger.json | 31 ++++++++++++++++++++ coderd/workspacebuilds.go | 2 +- docs/reference/api/builds.md | 57 ++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 49ab32984a247..afe4b90e60358 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7417,6 +7417,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": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c8e669b3bac43..96a8a9d83d918 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6561,6 +6561,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": [ diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 9ea6a77554098..dab7c0752a51f 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -654,7 +654,7 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { // @Tags Builds // @Param workspacebuild path string true "Workspace build ID" format(uuid) // @Success 200 {object} codersdk.WorkspaceBuildTimings -// @Router/workspacebuilds/{workspacebuild}/timings [get] +// @Router /workspacebuilds/{workspacebuild}/timings [get] func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index c0f1658e8ec8a..6c2c6075cc3bd 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -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": "string", + "exit_code": 0, + "stage": "string", + "started_at": "string", + "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 From bcd8b04a06bdf4418bbccbec7b2ef0fe27d04897 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 3 Oct 2024 18:59:16 +0000 Subject: [PATCH 06/17] Improve tests intention --- coderd/workspacebuilds_test.go | 240 ++++++++++++++++++++------------- 1 file changed, 149 insertions(+), 91 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index a7e9e2622bb75..8f04a722ca336 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1216,6 +1216,32 @@ func TestWorkspaceBuildTimings(t *testing.T) { CreatedBy: owner.UserID, }) + // Create a build to attach timings + makeBuild := func() database.WorkspaceBuild { + 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"}, + }) + return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: ws.ID, + TemplateVersionID: version.ID, + BuildNumber: 1, + Transition: database.WorkspaceTransitionStart, + InitiatorID: owner.UserID, + JobID: job.ID, + }) + } + makeProvisionerTimings := func(build database.WorkspaceBuild, count int) []database.ProvisionerJobTiming { // Use the database.ProvisionerJobTiming struct to mock timings data instead // of directly creating database.InsertProvisionerJobTimingsParams. This @@ -1250,8 +1276,86 @@ func TestWorkspaceBuildTimings(t *testing.T) { return dbgen.ProvisionerJobTimings(t, db, insertParams) } - makeAgentScriptTimings := func(build database.WorkspaceBuild, count int) []database.WorkspaceAgentScriptTiming { - // Create a resource, agent, and script to test the timing of agent scripts + makeAgentScriptTimings := func(script database.WorkspaceAgentScript, count int) []database.WorkspaceAgentScriptTiming { + newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) + now := time.Now() + for i := range count { + startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) + endedAt := startedAt.Add(time.Minute) + newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ + StartedAt: startedAt, + EndedAt: endedAt, + Stage: database.WorkspaceAgentScriptTimingStageStart, + ScriptID: script.ID, + ExitCode: 0, + Status: database.WorkspaceAgentScriptTimingStatusOk, + } + } + + timings := make([]database.WorkspaceAgentScriptTiming, 0) + for _, newTiming := range newTimings { + timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) + timings = append(timings, timing) + } + + return timings + } + + t.Run("NonExistentBuild", func(t *testing.T) { + t.Parallel() + + // When: fetching an inexistent build + buildID := uuid.New() + _, err := client.WorkspaceBuildTimings(context.Background(), 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() + res, err := client.WorkspaceBuildTimings(context.Background(), 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 := makeProvisionerTimings(build, 5) + + // Then: return a response with the expected timings + res, err := client.WorkspaceBuildTimings(context.Background(), 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, }) @@ -1276,101 +1380,55 @@ func TestWorkspaceBuildTimings(t *testing.T) { uuid.New(), }, }) + agentScriptTimings := makeAgentScriptTimings(scripts[0], 5) - newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) - now := time.Now() - for i := range count { - startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) - endedAt := startedAt.Add(time.Minute) - newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ - StartedAt: startedAt, - EndedAt: endedAt, - Stage: database.WorkspaceAgentScriptTimingStageStart, - ScriptID: scripts[0].ID, - ExitCode: 0, - Status: database.WorkspaceAgentScriptTimingStatusOk, - } + // Then: return a response with the expected timings + res, err := client.WorkspaceBuildTimings(context.Background(), 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()) } + }) - timings := make([]database.WorkspaceAgentScriptTiming, 0) - for _, newTiming := range newTimings { - timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) - timings = append(timings, timing) - } + t.Run("NoAgentScripts", func(t *testing.T) { + t.Parallel() - return timings - } + // 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, + }) - // Given - testCases := []struct { - name string - provisionerTimings int - actionScriptTimings int - }{ - {name: "with empty provisioner timings", provisionerTimings: 0}, - {name: "with provisioner timings", provisionerTimings: 5}, - {name: "with empty agent script timings", actionScriptTimings: 0}, - {name: "with agent script timings", actionScriptTimings: 5}, - } + // Then: return a response with empty agent script timings + res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + require.NoError(t, err) + require.Empty(t, res.AgentScriptTimings) + }) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - // Create a build to attach provisioner timings - 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"}, - }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: ws.ID, - TemplateVersionID: version.ID, - BuildNumber: 1, - Transition: database.WorkspaceTransitionStart, - InitiatorID: owner.UserID, - JobID: job.ID, - }) - - // Generate timings based on test config - genProvisionerTimings := makeProvisionerTimings(build, tc.provisionerTimings) - genAgentScriptTimings := makeAgentScriptTimings(build, tc.provisionerTimings) - - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) - require.NoError(t, err) - require.Len(t, res.ProvisionerTimings, tc.provisionerTimings) - - for i := range res.ProvisionerTimings { - timingRes := res.ProvisionerTimings[i] - genTiming := genProvisionerTimings[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()) - } + // Some workspaces might not have agents. It is improbable, but possible. + t.Run("NoAgents", func(t *testing.T) { + t.Parallel() - for i := range res.AgentScriptTimings { - timingRes := res.AgentScriptTimings[i] - genTiming := genAgentScriptTimings[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()) - } + // 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 + res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + require.NoError(t, err) + require.Empty(t, res.AgentScriptTimings) + }) } From f4c3331cd1e5c687e62a34776a6e6b2246f6a34c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 12:08:38 +0000 Subject: [PATCH 07/17] Fix date-time format --- coderd/apidoc/docs.go | 6 ++++-- coderd/apidoc/swagger.json | 6 ++++-- codersdk/workspacebuilds.go | 4 ++-- docs/reference/api/builds.md | 4 ++-- docs/reference/api/schemas.md | 8 ++++---- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index afe4b90e60358..f150a5e7644fa 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8891,7 +8891,8 @@ const docTemplate = `{ "type": "string" }, "ended_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "exit_code": { "type": "integer" @@ -8900,7 +8901,8 @@ const docTemplate = `{ "type": "string" }, "started_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "status": { "type": "string" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 96a8a9d83d918..4ba2514fc4c3f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7881,7 +7881,8 @@ "type": "string" }, "ended_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "exit_code": { "type": "integer" @@ -7890,7 +7891,8 @@ "type": "string" }, "started_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "status": { "type": "string" diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 2c26978c8a2b8..3cb00c313f4bf 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -186,8 +186,8 @@ type ProvisionerTiming struct { } type AgentScriptTiming struct { - StartedAt time.Time `json:"started_at"` - EndedAt time.Time `json:"ended_at"` + 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"` diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 6c2c6075cc3bd..d49ab50fbb1ef 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1019,10 +1019,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim "agent_script_timings": [ { "display_name": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, "stage": "string", - "started_at": "string", + "started_at": "2019-08-24T14:15:22Z", "status": "string" } ], diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 5e312a39cae6a..7fdbb6562c000 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -354,10 +354,10 @@ ```json { "display_name": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, "stage": "string", - "started_at": "string", + "started_at": "2019-08-24T14:15:22Z", "status": "string" } ``` @@ -7347,10 +7347,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "agent_script_timings": [ { "display_name": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, "stage": "string", - "started_at": "string", + "started_at": "2019-08-24T14:15:22Z", "status": "string" } ], From 28521643e418927932a3a68d877463fd57418eb7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 12:23:21 +0000 Subject: [PATCH 08/17] Add dbauthz test --- coderd/database/dbauthz/dbauthz_test.go | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f3aec6c9326b0..a50adb1a1dbe6 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -586,6 +586,46 @@ func (s *MethodTestSuite) TestProvisionerJob() { JobID: j.ID, }).Asserts(w, policy.ActionRead).Returns([]database.ProvisionerJobLog{}) })) + s.Run("GetWorkspaceAgentScriptTimingsByBuildID", 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}) + r := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{ + JobID: b.JobID, + }) + a := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ + ResourceID: r.ID, + }) + scripts := dbgen.WorkspaceAgentScripts(s.T(), db, database.InsertWorkspaceAgentScriptsParams{ + WorkspaceAgentID: a.ID, + CreatedAt: time.Now(), + LogSourceID: []uuid.UUID{ + uuid.New(), + }, + LogPath: []string{""}, + Script: []string{""}, + Cron: []string{""}, + StartBlocksLogin: []bool{false}, + RunOnStart: []bool{false}, + RunOnStop: []bool{false}, + TimeoutSeconds: []int32{0}, + DisplayName: []string{""}, + ID: []uuid.UUID{ + uuid.New(), + }, + }) + t := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.InsertWorkspaceAgentScriptTimingsParams{ + StartedAt: dbtime.Now(), + EndedAt: dbtime.Now(), + Stage: database.WorkspaceAgentScriptTimingStageStart, + ScriptID: scripts[0].ID, + ExitCode: 0, + Status: database.WorkspaceAgentScriptTimingStatusOk, + }) + check.Args(b.ID).Asserts(w, policy.ActionRead).Returns(t) + })) } func (s *MethodTestSuite) TestLicense() { From b70628a88ec7d305041d0c6c20db9b6ac9d24b99 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 12:37:55 +0000 Subject: [PATCH 09/17] Fix dbauthz test --- coderd/database/dbauthz/dbauthz_test.go | 129 +++++++++++++----------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index a50adb1a1dbe6..529baaaa53af7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -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{}) @@ -586,46 +566,6 @@ func (s *MethodTestSuite) TestProvisionerJob() { JobID: j.ID, }).Asserts(w, policy.ActionRead).Returns([]database.ProvisionerJobLog{}) })) - s.Run("GetWorkspaceAgentScriptTimingsByBuildID", 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}) - r := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{ - JobID: b.JobID, - }) - a := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ - ResourceID: r.ID, - }) - scripts := dbgen.WorkspaceAgentScripts(s.T(), db, database.InsertWorkspaceAgentScriptsParams{ - WorkspaceAgentID: a.ID, - CreatedAt: time.Now(), - LogSourceID: []uuid.UUID{ - uuid.New(), - }, - LogPath: []string{""}, - Script: []string{""}, - Cron: []string{""}, - StartBlocksLogin: []bool{false}, - RunOnStart: []bool{false}, - RunOnStop: []bool{false}, - TimeoutSeconds: []int32{0}, - DisplayName: []string{""}, - ID: []uuid.UUID{ - uuid.New(), - }, - }) - t := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.InsertWorkspaceAgentScriptTimingsParams{ - StartedAt: dbtime.Now(), - EndedAt: dbtime.Now(), - Stage: database.WorkspaceAgentScriptTimingStageStart, - ScriptID: scripts[0].ID, - ExitCode: 0, - Status: database.WorkspaceAgentScriptTimingStatusOk, - }) - check.Args(b.ID).Asserts(w, policy.ActionRead).Returns(t) - })) } func (s *MethodTestSuite) TestLicense() { @@ -2901,6 +2841,75 @@ 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, + }) + _ = 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(j, 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, + }) + scripts := dbgen.WorkspaceAgentScripts(s.T(), db, database.InsertWorkspaceAgentScriptsParams{ + WorkspaceAgentID: agent.ID, + CreatedAt: time.Now(), + LogSourceID: []uuid.UUID{ + uuid.New(), + }, + LogPath: []string{""}, + Script: []string{""}, + Cron: []string{""}, + StartBlocksLogin: []bool{false}, + RunOnStart: []bool{false}, + RunOnStop: []bool{false}, + TimeoutSeconds: []int32{0}, + DisplayName: []string{""}, + ID: []uuid.UUID{ + uuid.New(), + }, + }) + timing := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.InsertWorkspaceAgentScriptTimingsParams{ + StartedAt: dbtime.Now(), + EndedAt: dbtime.Now(), + Stage: database.WorkspaceAgentScriptTimingStageStart, + ScriptID: scripts[0].ID, + ExitCode: 0, + Status: database.WorkspaceAgentScriptTimingStatusOk, + }) + row := database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Stage: timing.Stage, + ScriptID: timing.ScriptID, + ExitCode: timing.ExitCode, + Status: timing.Status, + DisplayName: scripts[0].DisplayName, + } + check.Args(build.ID).Asserts(workspace, policy.ActionRead).Returns(row) + })) } func (s *MethodTestSuite) TestNotifications() { From 9e09c6485b4eae34e518fa55004a5cf4c40f13c4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 12:44:28 +0000 Subject: [PATCH 10/17] Fix policy --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 529baaaa53af7..a1f76956ed3eb 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2859,7 +2859,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { Action: []string{"action1", "action2"}, Resource: []string{"resource1", "resource2"}, }) - check.Args(j.ID).Asserts(j, policy.ActionRead).Returns(t) + 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{}) From 77861664e43c0ff33af039dfbca51e437daf89fe Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 12:52:38 +0000 Subject: [PATCH 11/17] Fix test again --- coderd/database/dbauthz/dbauthz_test.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index a1f76956ed3eb..eaba7d79619f1 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2899,16 +2899,18 @@ func (s *MethodTestSuite) TestSystemFunctions() { ExitCode: 0, Status: database.WorkspaceAgentScriptTimingStatusOk, }) - row := database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Stage: timing.Stage, - ScriptID: timing.ScriptID, - ExitCode: timing.ExitCode, - Status: timing.Status, - DisplayName: scripts[0].DisplayName, + rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ + { + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Stage: timing.Stage, + ScriptID: timing.ScriptID, + ExitCode: timing.ExitCode, + Status: timing.Status, + DisplayName: scripts[0].DisplayName, + }, } - check.Args(build.ID).Asserts(workspace, policy.ActionRead).Returns(row) + check.Args(build.ID).Asserts(workspace, policy.ActionRead).Returns(rows) })) } From ed4cbd0fe745aa39db7bcc6e4890c4516354e3c3 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 13:00:49 +0000 Subject: [PATCH 12/17] Fix asserts --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index eaba7d79619f1..999f9b36265f7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2910,7 +2910,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { DisplayName: scripts[0].DisplayName, }, } - check.Args(build.ID).Asserts(workspace, policy.ActionRead).Returns(rows) + check.Args(build.ID).Asserts(policy.ActionRead, rbac.ResourceSystem).Returns(rows) })) } From 920e41d99f88b633442b19e19fe1dbf3ec4bc6a8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 13:09:27 +0000 Subject: [PATCH 13/17] Fix order --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 999f9b36265f7..ef6f03b30d62a 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2910,7 +2910,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { DisplayName: scripts[0].DisplayName, }, } - check.Args(build.ID).Asserts(policy.ActionRead, rbac.ResourceSystem).Returns(rows) + check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) })) } From 65aeeea7a6c440696be17fbdaa4176b501c74804 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 15:18:14 +0000 Subject: [PATCH 14/17] Wrap errors and set timeout for tests --- coderd/database/dbmem/dbmem.go | 8 ++++---- coderd/workspacebuilds_test.go | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ec09aa5d9c2a4..006a02e39dc6f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5799,12 +5799,12 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex build, err := q.GetWorkspaceBuildByID(ctx, id) if err != nil { - return nil, err + return nil, xerrors.Errorf("get build: %w", err) } resources, err := q.GetWorkspaceResourcesByJobID(ctx, build.JobID) if err != nil { - return nil, err + return nil, xerrors.Errorf("get resources: %w", err) } resourceIDs := make([]uuid.UUID, 0, len(resources)) for _, res := range resources { @@ -5813,7 +5813,7 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex agents, err := q.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) if err != nil { - return nil, err + return nil, xerrors.Errorf("get agents: %w", err) } agentIDs := make([]uuid.UUID, 0, len(agents)) for _, agent := range agents { @@ -5822,7 +5822,7 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex scripts, err := q.GetWorkspaceAgentScriptsByAgentIDs(ctx, agentIDs) if err != nil { - return nil, err + return nil, xerrors.Errorf("get scripts: %w", err) } scriptIDs := make([]uuid.UUID, 0, len(scripts)) for _, script := range scripts { diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 8f04a722ca336..0e5ffe20c8eb6 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1306,7 +1306,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { // When: fetching an inexistent build buildID := uuid.New() - _, err := client.WorkspaceBuildTimings(context.Background(), buildID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + _, err := client.WorkspaceBuildTimings(ctx, buildID) // Then: expect a not found error require.Error(t, err) @@ -1318,7 +1320,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { // When: fetching timings for a build with no timings build := makeBuild() - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) // Then: return a response with empty timings require.NoError(t, err) @@ -1334,7 +1338,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { provisionerTimings := makeProvisionerTimings(build, 5) // Then: return a response with the expected timings - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) require.Len(t, res.ProvisionerTimings, 5) @@ -1383,7 +1389,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { agentScriptTimings := makeAgentScriptTimings(scripts[0], 5) // Then: return a response with the expected timings - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) require.Len(t, res.AgentScriptTimings, 5) @@ -1411,7 +1419,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // Then: return a response with empty agent script timings - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) require.Empty(t, res.AgentScriptTimings) }) @@ -1427,7 +1437,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // Then: return a response with empty agent script timings - res, err := client.WorkspaceBuildTimings(context.Background(), build.ID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) require.Empty(t, res.AgentScriptTimings) }) From c7ae412a1d0e61cacf98aac3d9f53680afdff4d6 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 17:11:46 +0000 Subject: [PATCH 15/17] Add back the workspace timings endpoint --- coderd/apidoc/docs.go | 35 ++++++++ coderd/apidoc/swagger.json | 31 +++++++ coderd/coderd.go | 1 + coderd/database/dbauthz/dbauthz_test.go | 43 ++------- coderd/database/dbgen/dbgen.go | 65 ++++++++++++-- coderd/workspacebuilds.go | 85 +++++++++--------- coderd/workspacebuilds_test.go | 96 ++------------------- coderd/workspaces.go | 35 ++++++++ coderd/workspaces_test.go | 110 ++++++++++++++++++++++++ codersdk/workspaces.go | 14 +++ docs/reference/api/workspaces.md | 57 ++++++++++++ 11 files changed, 399 insertions(+), 173 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f150a5e7644fa..3578d644b22a0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8456,6 +8456,41 @@ const docTemplate = `{ } } }, + "/workspaces/{workspace}/timings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspaces" + ], + "summary": "Get workspace timings by ID", + "operationId": "get-workspace-timings-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceBuildTimings" + } + } + } + } + }, "/workspaces/{workspace}/ttl": { "put": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4ba2514fc4c3f..2555abb536587 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7482,6 +7482,37 @@ } } }, + "/workspaces/{workspace}/timings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Workspaces"], + "summary": "Get workspace timings by ID", + "operationId": "get-workspace-timings-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceBuildTimings" + } + } + } + } + }, "/workspaces/{workspace}/ttl": { "put": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index d73d76c86c7db..80b607bda9ba4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1150,6 +1150,7 @@ func New(options *Options) *API { r.Post("/", api.postWorkspaceAgentPortShare) r.Delete("/", api.deleteWorkspaceAgentPortShare) }) + r.Get("/timings", api.workspaceTimings) }) }) r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ef6f03b30d62a..c431f4d675525 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2846,19 +2846,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { 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"}, - }) + 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) { @@ -2873,31 +2862,11 @@ func (s *MethodTestSuite) TestSystemFunctions() { agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ ResourceID: resource.ID, }) - scripts := dbgen.WorkspaceAgentScripts(s.T(), db, database.InsertWorkspaceAgentScriptsParams{ + script := dbgen.WorkspaceAgentScript(s.T(), db, database.WorkspaceAgentScript{ WorkspaceAgentID: agent.ID, - CreatedAt: time.Now(), - LogSourceID: []uuid.UUID{ - uuid.New(), - }, - LogPath: []string{""}, - Script: []string{""}, - Cron: []string{""}, - StartBlocksLogin: []bool{false}, - RunOnStart: []bool{false}, - RunOnStop: []bool{false}, - TimeoutSeconds: []int32{0}, - DisplayName: []string{""}, - ID: []uuid.UUID{ - uuid.New(), - }, }) - timing := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.InsertWorkspaceAgentScriptTimingsParams{ - StartedAt: dbtime.Now(), - EndedAt: dbtime.Now(), - Stage: database.WorkspaceAgentScriptTimingStageStart, - ScriptID: scripts[0].ID, - ExitCode: 0, - Status: database.WorkspaceAgentScriptTimingStatusOk, + timing := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.WorkspaceAgentScriptTiming{ + ScriptID: script.ID, }) rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ { @@ -2907,7 +2876,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { ScriptID: timing.ScriptID, ExitCode: timing.ExitCode, Status: timing.Status, - DisplayName: scripts[0].DisplayName, + DisplayName: script.DisplayName, }, } check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index b481682c12796..1031b51f72313 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -189,14 +189,45 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen return agt } -func WorkspaceAgentScripts(t testing.TB, db database.Store, orig database.InsertWorkspaceAgentScriptsParams) []database.WorkspaceAgentScript { - scripts, err := db.InsertWorkspaceAgentScripts(genCtx, orig) +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") - return scripts + require.NotEmpty(t, scripts, "insert workspace agent script returned no scripts") + return scripts[0] } -func WorkspaceAgentScriptTiming(t testing.TB, db database.Store, orig database.InsertWorkspaceAgentScriptTimingsParams) database.WorkspaceAgentScriptTiming { - timing, err := db.InsertWorkspaceAgentScriptTimings(genCtx, orig) +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 } @@ -947,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) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index dab7c0752a51f..92e21b78e0756 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -661,52 +661,16 @@ func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) { build = httpmw.WorkspaceBuildParam(r) ) - provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace timings.", - Detail: err.Error(), - }) - return - } - - agentScriptTimings, err := api.Database.GetWorkspaceAgentScriptTimingsByBuildID(ctx, build.ID) - 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 agent script timings.", + Message: "Internal error fetching timings.", Detail: err.Error(), }) return } - 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, - }) - } - - httpapi.Write(ctx, rw, http.StatusOK, res) + httpapi.Write(ctx, rw, http.StatusOK, timings) } type workspaceBuildsData struct { @@ -1072,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 +} diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 0e5ffe20c8eb6..2e69127d9684e 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1199,7 +1199,6 @@ func TestWorkspaceBuildTimings(t *testing.T) { 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", @@ -1215,92 +1214,28 @@ func TestWorkspaceBuildTimings(t *testing.T) { 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 makeBuild := func() database.WorkspaceBuild { - 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"}, }) return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: ws.ID, TemplateVersionID: version.ID, - BuildNumber: 1, - Transition: database.WorkspaceTransitionStart, InitiatorID: owner.UserID, JobID: job.ID, }) } - makeProvisionerTimings := func(build database.WorkspaceBuild, 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: build.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) - } - - makeAgentScriptTimings := func(script database.WorkspaceAgentScript, count int) []database.WorkspaceAgentScriptTiming { - newTimings := make([]database.InsertWorkspaceAgentScriptTimingsParams, count) - now := time.Now() - for i := range count { - startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute) - endedAt := startedAt.Add(time.Minute) - newTimings[i] = database.InsertWorkspaceAgentScriptTimingsParams{ - StartedAt: startedAt, - EndedAt: endedAt, - Stage: database.WorkspaceAgentScriptTimingStageStart, - ScriptID: script.ID, - ExitCode: 0, - Status: database.WorkspaceAgentScriptTimingStatusOk, - } - } - - timings := make([]database.WorkspaceAgentScriptTiming, 0) - for _, newTiming := range newTimings { - timing := dbgen.WorkspaceAgentScriptTiming(t, db, newTiming) - timings = append(timings, timing) - } - - return timings - } - t.Run("NonExistentBuild", func(t *testing.T) { t.Parallel() @@ -1335,7 +1270,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { // When: fetching timings for a build with provisioner timings build := makeBuild() - provisionerTimings := makeProvisionerTimings(build, 5) + provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) // Then: return a response with the expected timings ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) @@ -1368,25 +1303,10 @@ func TestWorkspaceBuildTimings(t *testing.T) { agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ ResourceID: resource.ID, }) - scripts := dbgen.WorkspaceAgentScripts(t, db, database.InsertWorkspaceAgentScriptsParams{ + script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{ WorkspaceAgentID: agent.ID, - CreatedAt: time.Now(), - LogSourceID: []uuid.UUID{ - uuid.New(), - }, - LogPath: []string{""}, - Script: []string{""}, - Cron: []string{""}, - StartBlocksLogin: []bool{false}, - RunOnStart: []bool{false}, - RunOnStop: []bool{false}, - TimeoutSeconds: []int32{0}, - DisplayName: []string{""}, - ID: []uuid.UUID{ - uuid.New(), - }, }) - agentScriptTimings := makeAgentScriptTimings(scripts[0], 5) + agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5) // Then: return a response with the expected timings ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 2c4f0b52c0116..2407130ea38e4 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1740,6 +1740,41 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } } +// @Summary Get workspace timings by ID +// @ID get-workspace-timings-by-id +// @Security CoderSessionToken +// @Produce json +// @Tags Workspaces +// @Param workspace path string true "Workspace ID" format(uuid) +// @Success 200 {object} codersdk.WorkspaceBuildTimings +// @Router /workspaces/{workspace}/timings [get] +func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + workspace = httpmw.WorkspaceParam(r) + ) + + build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace build.", + Detail: err.Error(), + }) + return + } + + 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 workspaceData struct { templates []database.Template builds []codersdk.WorkspaceBuild diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 98f36c3b9a13e..f37c01b6b53f2 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3556,3 +3556,113 @@ func TestWorkspaceNotifications(t *testing.T) { }) }) } + +func TestWorkspaceTimings(t *testing.T) { + t.Parallel() + + db, pubsub := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: pubsub, + }) + coderdtest.CreateFirstUser(t, client) + + 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, + }) + + // 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, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + 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.WaitShort) + 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.WaitShort) + 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") + }) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 4e4b98fe8c243..5ce1769150e02 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -626,6 +626,20 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) return nil } +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 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) +} + // WorkspaceNotifyChannel is the PostgreSQL NOTIFY // channel to listen for updates on. The payload is empty, // because the size of a workspace payload can be very large. diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index fbf3171b86dc4..283dab5db91b5 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1616,6 +1616,63 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/resolve-autos To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get workspace timings by ID + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaces/{workspace}/timings` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------- | ---- | ------------ | -------- | ------------ | +| `workspace` | path | string(uuid) | true | Workspace 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). + ## Update workspace TTL by ID ### Code samples From aaedd1c8263be6f1b50944e33024f0f2e2c0d583 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 17:26:34 +0000 Subject: [PATCH 16/17] Fix unique build number --- coderd/workspacebuilds_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 2e69127d9684e..e7b2ccb9055f5 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1221,7 +1221,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // 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, @@ -1233,6 +1235,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { TemplateVersionID: version.ID, InitiatorID: owner.UserID, JobID: job.ID, + BuildNumber: buildNumber, }) } From a846eb7514b4a299d74cfc0e289612c979935b2a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 4 Oct 2024 17:59:58 +0000 Subject: [PATCH 17/17] Wait long --- coderd/workspacebuilds_test.go | 14 +++++++------- coderd/workspaces_test.go | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index e7b2ccb9055f5..580b01fdec1a9 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -42,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, @@ -1244,7 +1244,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { // When: fetching an inexistent build buildID := uuid.New() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) _, err := client.WorkspaceBuildTimings(ctx, buildID) @@ -1258,7 +1258,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { // When: fetching timings for a build with no timings build := makeBuild() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) @@ -1276,7 +1276,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) // Then: return a response with the expected timings - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) @@ -1312,7 +1312,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5) // Then: return a response with the expected timings - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) @@ -1342,7 +1342,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // Then: return a response with empty agent script timings - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) @@ -1360,7 +1360,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // Then: return a response with empty agent script timings - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index f37c01b6b53f2..dc83289340059 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3642,7 +3642,7 @@ func TestWorkspaceTimings(t *testing.T) { dbgen.WorkspaceAgentScriptTimings(t, db, script, 3) // When: fetching the timings - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceTimings(ctx, ws.ID) @@ -3657,7 +3657,7 @@ func TestWorkspaceTimings(t *testing.T) { // When: fetching an inexistent workspace workspaceID := uuid.New() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) _, err := client.WorkspaceTimings(ctx, workspaceID)