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