From da5e478eafe2c13a735673402766d03af52c7f89 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 11 Sep 2024 18:37:06 +0000 Subject: [PATCH 01/12] feat(coderd): add workspace timings endpoint --- coderd/apidoc/docs.go | 71 ++++++++ coderd/apidoc/swagger.json | 67 ++++++++ coderd/coderd.go | 1 + coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbgen/dbgen.go | 6 + coderd/database/dbmem/dbmem.go | 37 +++- coderd/database/dbmetrics/dbmetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 ++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 37 ++++ coderd/database/queries/provisionerjobs.sql | 5 + coderd/workspaces.go | 49 ++++++ coderd/workspaces_test.go | 178 ++++++++++++++++++++ codersdk/client.go | 3 + codersdk/workspaces.go | 29 ++++ docs/reference/api/schemas.md | 41 +++++ docs/reference/api/workspaces.md | 61 +++++++ site/src/api/typesGenerated.ts | 14 ++ 18 files changed, 624 insertions(+), 2 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index aef30f29c72b0..135c5edd73e56 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8213,6 +8213,44 @@ const docTemplate = `{ } } }, + "/workspaces/{workspace}/timings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspaces" + ], + "summary": "Return workspace timings by ID", + "operationId": "workspace-timings-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceTiming" + } + } + } + } + } + }, "/workspaces/{workspace}/ttl": { "put": { "security": [ @@ -14440,6 +14478,39 @@ const docTemplate = `{ "WorkspaceStatusDeleted" ] }, + "codersdk.WorkspaceTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "label": { + "type": "string" + }, + "metadata": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceTimingMetadata" + } + }, + "started_at": { + "type": "string", + "format": "date-time" + } + } + }, + "codersdk.WorkspaceTimingMetadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceTransition": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a9b4715475264..26d285682b730 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7267,6 +7267,40 @@ } } }, + "/workspaces/{workspace}/timings": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Workspaces"], + "summary": "Return workspace timings by ID", + "operationId": "workspace-timings-by-id", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Workspace ID", + "name": "workspace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceTiming" + } + } + } + } + } + }, "/workspaces/{workspace}/ttl": { "put": { "security": [ @@ -13157,6 +13191,39 @@ "WorkspaceStatusDeleted" ] }, + "codersdk.WorkspaceTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "label": { + "type": "string" + }, + "metadata": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceTimingMetadata" + } + }, + "started_at": { + "type": "string", + "format": "date-time" + } + } + }, + "codersdk.WorkspaceTimingMetadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceTransition": { "type": "string", "enum": ["start", "stop", "delete"], diff --git a/coderd/coderd.go b/coderd/coderd.go index 20ce616eab5ba..8a720c8d2788e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1169,6 +1169,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.go b/coderd/database/dbauthz/dbauthz.go index f6bd03cc50e8b..0c6b28cb32e90 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1784,6 +1784,10 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data return job, nil } +func (q *querier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ProvisionerJobTiming, error) { + return q.db.GetProvisionerJobTimingsByJobID(ctx, jobID) +} + // TODO: we need to add a provisioner job resource func (q *querier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { // if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index ccacb0dc0a995..9c810963cbce2 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -892,6 +892,12 @@ func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) datab return role } +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") + return timings +} + func must[V any](v V, err error) V { if err != nil { panic(err) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3ccd2fa85e624..0b399cf2aa12f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -194,6 +194,7 @@ type data struct { workspaces []database.Workspace workspaceProxies []database.WorkspaceProxy customRoles []database.CustomRole + provisionerJobTimings []database.ProvisionerJobTiming // Locks is a map of lock names. Any keys within the map are currently // locked. locks map[int64]struct{} @@ -3270,6 +3271,23 @@ func (q *FakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) ( return q.getProvisionerJobByIDNoLock(ctx, id) } +func (q *FakeQuerier) GetProvisionerJobTimingsByJobID(_ context.Context, jobID uuid.UUID) ([]database.ProvisionerJobTiming, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + timings := make([]database.ProvisionerJobTiming, 0) + for _, timing := range q.provisionerJobTimings { + if timing.JobID == jobID { + timings = append(timings, timing) + } + } + if len(timings) == 0 { + return nil, sql.ErrNoRows + } + + return timings, nil +} + func (q *FakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -6751,13 +6769,28 @@ func (q *FakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.I return logs, nil } -func (*FakeQuerier) InsertProvisionerJobTimings(_ context.Context, arg database.InsertProvisionerJobTimingsParams) ([]database.ProvisionerJobTiming, error) { +func (q *FakeQuerier) InsertProvisionerJobTimings(_ context.Context, arg database.InsertProvisionerJobTimingsParams) ([]database.ProvisionerJobTiming, error) { err := validateDatabaseType(arg) if err != nil { return nil, err } - return nil, nil + q.mutex.Lock() + defer q.mutex.Unlock() + + for i := range arg.StartedAt { + q.provisionerJobTimings = append(q.provisionerJobTimings, database.ProvisionerJobTiming{ + JobID: arg.JobID, + StartedAt: arg.StartedAt[i], + EndedAt: arg.EndedAt[i], + Stage: arg.Stage[i], + Source: arg.Source[i], + Action: arg.Action[i], + Resource: arg.Resource[i], + }) + } + + return q.provisionerJobTimings, nil } func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 38289c143bfd9..ab11f96256687 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -914,6 +914,13 @@ func (m metricsStore) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) ( return job, err } +func (m metricsStore) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ProvisionerJobTiming, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerJobTimingsByJobID(ctx, jobID) + m.queryLatencies.WithLabelValues("GetProvisionerJobTimingsByJobID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { start := time.Now() jobs, err := m.s.GetProvisionerJobsByIDs(ctx, ids) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 1771807f26b2f..2f19cdc224e8d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1854,6 +1854,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerJobByID(arg0, arg1 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobByID", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobByID), arg0, arg1) } +// GetProvisionerJobTimingsByJobID mocks base method. +func (m *MockStore) GetProvisionerJobTimingsByJobID(arg0 context.Context, arg1 uuid.UUID) ([]database.ProvisionerJobTiming, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerJobTimingsByJobID", arg0, arg1) + ret0, _ := ret[0].([]database.ProvisionerJobTiming) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerJobTimingsByJobID indicates an expected call of GetProvisionerJobTimingsByJobID. +func (mr *MockStoreMockRecorder) GetProvisionerJobTimingsByJobID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobTimingsByJobID", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobTimingsByJobID), arg0, arg1) +} + // GetProvisionerJobsByIDs mocks base method. func (m *MockStore) GetProvisionerJobsByIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.ProvisionerJob, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c614a03834a9b..474e4d18c28bd 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -188,6 +188,7 @@ type sqlcQuerier interface { GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerDaemon, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) + GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fc388e55247d0..bc2060c2a3398 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5317,6 +5317,43 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P return i, err } +const getProvisionerJobTimingsByJobID = `-- name: GetProvisionerJobTimingsByJobID :many +SELECT job_id, started_at, ended_at, stage, source, action, resource FROM provisioner_job_timings +WHERE job_id = $1 +ORDER BY started_at ASC +` + +func (q *sqlQuerier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) { + rows, err := q.db.QueryContext(ctx, getProvisionerJobTimingsByJobID, jobID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ProvisionerJobTiming + for rows.Next() { + var i ProvisionerJobTiming + if err := rows.Scan( + &i.JobID, + &i.StartedAt, + &i.EndedAt, + &i.Stage, + &i.Source, + &i.Action, + &i.Resource, + ); 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 getProvisionerJobsByIDs = `-- name: GetProvisionerJobsByIDs :many SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index 687176d3c255b..95a84fcd3c824 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -156,3 +156,8 @@ SELECT unnest(@action::text[]), unnest(@resource::text[]) RETURNING *; + +-- name: GetProvisionerJobTimingsByJobID :many +SELECT * FROM provisioner_job_timings +WHERE job_id = $1 +ORDER BY started_at ASC; \ No newline at end of file diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 62193b6d673f0..d28109c79023a 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1740,6 +1740,55 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } } +// @Summary Return workspace timings by ID +// @ID workspace-timings-by-id +// @Security CoderSessionToken +// @Produce json +// @Tags Workspaces +// @Param workspace path string true "Workspace ID" format(uuid) +// @Success 200 {object} []codersdk.WorkspaceTiming +// @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.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 + } + + res := make([]codersdk.WorkspaceTiming, 0, len(timings)) + for _, timing := range timings { + res = append(res, codersdk.WorkspaceTiming{ + Label: timing.Resource, + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Metadata: []codersdk.WorkspaceTimingMetadata{ + {Name: "resource", Value: timing.Resource}, + {Name: "action", Value: timing.Action}, + {Name: "source", Value: timing.Source}, + {Name: "stage", Value: string(timing.Stage)}, + }, + }) + } + 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 98f36c3b9a13e..5f23387c6d9f1 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3556,3 +3556,181 @@ 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 workspaceWithBuild struct { + database.Workspace + build database.WorkspaceBuild + } + makeWorkspace := func() workspaceWithBuild { + ws := dbgen.Workspace(t, db, database.Workspace{ + OwnerID: owner.UserID, + OrganizationID: owner.OrganizationID, + TemplateID: template.ID, + Name: "test-workspace", + }) + 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, + }) + return workspaceWithBuild{ + Workspace: ws, + build: build, + } + } + + makeTimings := 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 + tests := []struct { + name string + numberOfTimings int + workspace workspaceWithBuild + error bool + }{ + { + name: "workspace with 5 provisioner timings", + numberOfTimings: 5, + workspace: makeWorkspace(), + }, + { + name: "workspace with 2 provisioner timings", + numberOfTimings: 2, + workspace: makeWorkspace(), + }, + { + name: "workspace with 0 provisioner timings", + numberOfTimings: 0, + workspace: makeWorkspace(), + }, + { + name: "workspace not found", + numberOfTimings: 0, + workspace: workspaceWithBuild{}, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + generatedTimings := make([]database.ProvisionerJobTiming, tt.numberOfTimings) + if tt.numberOfTimings > 0 { + generatedTimings = makeTimings(tt.workspace.build.JobID, tt.numberOfTimings) + } + res, err := client.WorkspaceTimings(context.Background(), tt.workspace.ID) + + // When error is expected + if tt.error { + require.Error(t, err) + return + } + + // When success is expected + require.NoError(t, err) + require.Len(t, res, tt.numberOfTimings) + + // Verify fields + for i := range res { + require.Equal(t, generatedTimings[i].Resource, res[i].Label) + require.Equal(t, generatedTimings[i].StartedAt.UnixMilli(), res[i].StartedAt.UnixMilli(), "diff start times") + require.Equal(t, generatedTimings[i].EndedAt.UnixMilli(), res[i].EndedAt.UnixMilli(), "diff end times") + + // Verify metadata + metaTests := []struct { + name string + value string + }{ + {name: "source", value: generatedTimings[i].Source}, + } + for _, mt := range metaTests { + t.Run(fmt.Sprintf("verify metadata %s", mt.name), func(t *testing.T) { + contains := codersdk.WorkspaceTimingMetadata{ + Name: mt.name, + Value: mt.value, + } + require.Containsf(t, res[i].Metadata, contains, fmt.Sprintf("metadata %s not found", mt.name)) + }) + } + } + }) + } +} diff --git a/codersdk/client.go b/codersdk/client.go index cf013a25c3ce8..17027901a581a 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -192,6 +192,9 @@ func prefixLines(prefix, s []byte) []byte { // Request performs a HTTP request with the body provided. The caller is // responsible for closing the response body. func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) { + if ctx == nil { + return nil, xerrors.Errorf("context should bot be nil") + } ctx, span := tracing.StartSpanWithName(ctx, tracing.FuncNameSkip(1)) defer span.End() diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 4e4b98fe8c243..bd3a2a88375b5 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -626,6 +626,35 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) return nil } +// A timing can originate from either a provisioner job or an agent. Each source +// may have different associated data, some of which is useful for users and +// should be displayed. +type WorkspaceTimingMetadata struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type WorkspaceTiming struct { + Label string `json:"label"` + Metadata []WorkspaceTimingMetadata `json:"metadata"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` +} + +func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) ([]WorkspaceTiming, error) { + path := fmt.Sprintf("/api/v2/workspaces/%s/timings", id.String()) + res, err := c.Request(ctx, http.MethodGet, path, nil) + if err != nil { + return []WorkspaceTiming{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return []WorkspaceTiming{}, ReadBodyAsError(res) + } + var timings []WorkspaceTiming + 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/schemas.md b/docs/reference/api/schemas.md index a729ac4798881..2c108e4cd8392 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7448,6 +7448,47 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `deleting` | | `deleted` | +## codersdk.WorkspaceTiming + +```json +{ + "ended_at": "2019-08-24T14:15:22Z", + "label": "string", + "metadata": [ + { + "name": "string", + "value": "string" + } + ], + "started_at": "2019-08-24T14:15:22Z" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------ | ----------------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `ended_at` | string | false | | | +| `label` | string | false | | | +| `metadata` | array of [codersdk.WorkspaceTimingMetadata](#codersdkworkspacetimingmetadata) | false | | | +| `started_at` | string | false | | | + +## codersdk.WorkspaceTimingMetadata + +```json +{ + "name": "string", + "value": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------- | ------ | -------- | ------------ | ----------- | +| `name` | string | false | | | +| `value` | string | false | | | + ## codersdk.WorkspaceTransition ```json diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 11b2a6283e342..f84aae6c819a1 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1598,6 +1598,67 @@ 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). +## Return 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 +[ + { + "ended_at": "2019-08-24T14:15:22Z", + "label": "string", + "metadata": [ + { + "name": "string", + "value": "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 | array of [codersdk.WorkspaceTiming](schemas.md#codersdkworkspacetiming) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| -------------- | ----------------- | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» ended_at` | string(date-time) | false | | | +| `» label` | string | false | | | +| `» metadata` | array | false | | | +| `»» name` | string | false | | | +| `»» value` | string | false | | | +| `» started_at` | string(date-time) | false | | | + +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 cdad66649e265..bbda1053152c0 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1998,6 +1998,20 @@ export interface WorkspaceResourceMetadata { readonly sensitive: boolean; } +// From codersdk/workspaces.go +export interface WorkspaceTiming { + readonly label: string; + readonly metadata: Readonly>; + readonly started_at: string; + readonly ended_at: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceTimingMetadata { + readonly name: string; + readonly value: string; +} + // From codersdk/workspaces.go export interface WorkspacesRequest extends Pagination { readonly q?: string; From ddb570ca499b4f95dfd691b09f31d08f67dd9ad7 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 12 Sep 2024 09:31:11 -0300 Subject: [PATCH 02/12] Fix verbiage Co-authored-by: Aaron Lehmann --- codersdk/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/client.go b/codersdk/client.go index 17027901a581a..d267355d37096 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -193,7 +193,7 @@ func prefixLines(prefix, s []byte) []byte { // responsible for closing the response body. func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) { if ctx == nil { - return nil, xerrors.Errorf("context should bot be nil") + return nil, xerrors.Errorf("context should not be nil") } ctx, span := tracing.StartSpanWithName(ctx, tracing.FuncNameSkip(1)) defer span.End() From 83ed6c5cf8ce7055545d5d526e6fd7787b92caae Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 15:56:31 +0000 Subject: [PATCH 03/12] Refactor timings response --- coderd/apidoc/docs.go | 59 ++++++++++++----------- coderd/apidoc/swagger.json | 59 ++++++++++++----------- coderd/workspaces.go | 28 +++++------ coderd/workspaces_test.go | 82 ++++++++++++++------------------ codersdk/workspaces.go | 29 ++++++----- docs/reference/api/schemas.md | 69 +++++++++++++++------------ docs/reference/api/workspaces.md | 46 +++++++----------- site/src/api/typesGenerated.ts | 24 +++++----- 8 files changed, 191 insertions(+), 205 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9c41bb0caa8ca..1052ca63133a3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8242,10 +8242,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceTiming" - } + "$ref": "#/definitions/codersdk.WorkspaceTimings" } } } @@ -11660,6 +11657,32 @@ const docTemplate = `{ "ProvisionerStorageMethodFile" ] }, + "codersdk.ProvisionerTiming": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "ended_at": { + "type": "string" + }, + "job_id": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "source": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "started_at": { + "type": "string" + } + } + }, "codersdk.ProxyHealthReport": { "type": "object", "properties": { @@ -14481,36 +14504,14 @@ const docTemplate = `{ "WorkspaceStatusDeleted" ] }, - "codersdk.WorkspaceTiming": { + "codersdk.WorkspaceTimings": { "type": "object", "properties": { - "ended_at": { - "type": "string", - "format": "date-time" - }, - "label": { - "type": "string" - }, - "metadata": { + "provisioner_timings": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceTimingMetadata" + "$ref": "#/definitions/codersdk.ProvisionerTiming" } - }, - "started_at": { - "type": "string", - "format": "date-time" - } - } - }, - "codersdk.WorkspaceTimingMetadata": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bbfde0e065a6e..ea04fbc475321 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7292,10 +7292,7 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.WorkspaceTiming" - } + "$ref": "#/definitions/codersdk.WorkspaceTimings" } } } @@ -10522,6 +10519,32 @@ "enum": ["file"], "x-enum-varnames": ["ProvisionerStorageMethodFile"] }, + "codersdk.ProvisionerTiming": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "ended_at": { + "type": "string" + }, + "job_id": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "source": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "started_at": { + "type": "string" + } + } + }, "codersdk.ProxyHealthReport": { "type": "object", "properties": { @@ -13194,36 +13217,14 @@ "WorkspaceStatusDeleted" ] }, - "codersdk.WorkspaceTiming": { + "codersdk.WorkspaceTimings": { "type": "object", "properties": { - "ended_at": { - "type": "string", - "format": "date-time" - }, - "label": { - "type": "string" - }, - "metadata": { + "provisioner_timings": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceTimingMetadata" + "$ref": "#/definitions/codersdk.ProvisionerTiming" } - }, - "started_at": { - "type": "string", - "format": "date-time" - } - } - }, - "codersdk.WorkspaceTimingMetadata": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" } } }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index d28109c79023a..b566ddfdc91a0 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -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.WorkspaceTiming +// @Success 200 {object} codersdk.WorkspaceTimings // @Router /workspaces/{workspace}/timings [get] func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { var ( @@ -1763,7 +1763,7 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { return } - timings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID) + 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.", @@ -1772,18 +1772,18 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { return } - res := make([]codersdk.WorkspaceTiming, 0, len(timings)) - for _, timing := range timings { - res = append(res, codersdk.WorkspaceTiming{ - Label: timing.Resource, - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Metadata: []codersdk.WorkspaceTimingMetadata{ - {Name: "resource", Value: timing.Resource}, - {Name: "action", Value: timing.Action}, - {Name: "source", Value: timing.Source}, - {Name: "stage", Value: string(timing.Stage)}, - }, + 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) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 5f23387c6d9f1..4ecd7ab7309e4 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3624,7 +3624,7 @@ func TestWorkspaceTimings(t *testing.T) { } } - makeTimings := func(jobID uuid.UUID, count int) []database.ProvisionerJobTiming { + 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 @@ -3660,31 +3660,31 @@ func TestWorkspaceTimings(t *testing.T) { // Given tests := []struct { - name string - numberOfTimings int - workspace workspaceWithBuild - error bool + name string + provisionerTimings int + workspace workspaceWithBuild + error bool }{ { - name: "workspace with 5 provisioner timings", - numberOfTimings: 5, - workspace: makeWorkspace(), + name: "workspace with 5 provisioner timings", + provisionerTimings: 5, + workspace: makeWorkspace(), }, { - name: "workspace with 2 provisioner timings", - numberOfTimings: 2, - workspace: makeWorkspace(), + name: "workspace with 2 provisioner timings", + provisionerTimings: 2, + workspace: makeWorkspace(), }, { - name: "workspace with 0 provisioner timings", - numberOfTimings: 0, - workspace: makeWorkspace(), + name: "workspace with 0 provisioner timings", + provisionerTimings: 0, + workspace: makeWorkspace(), }, { - name: "workspace not found", - numberOfTimings: 0, - workspace: workspaceWithBuild{}, - error: true, + name: "workspace not found", + provisionerTimings: 0, + workspace: workspaceWithBuild{}, + error: true, }, } @@ -3692,44 +3692,32 @@ func TestWorkspaceTimings(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - generatedTimings := make([]database.ProvisionerJobTiming, tt.numberOfTimings) - if tt.numberOfTimings > 0 { - generatedTimings = makeTimings(tt.workspace.build.JobID, tt.numberOfTimings) + // Generate timings based on test config + generatedTimings := make([]database.ProvisionerJobTiming, tt.provisionerTimings) + if tt.provisionerTimings > 0 { + generatedTimings = makeProvisionerTimings(tt.workspace.build.JobID, tt.provisionerTimings) } res, err := client.WorkspaceTimings(context.Background(), tt.workspace.ID) - // When error is expected + // When error is expected, than an error is returned if tt.error { require.Error(t, err) return } - // When success is expected + // 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, tt.numberOfTimings) - - // Verify fields - for i := range res { - require.Equal(t, generatedTimings[i].Resource, res[i].Label) - require.Equal(t, generatedTimings[i].StartedAt.UnixMilli(), res[i].StartedAt.UnixMilli(), "diff start times") - require.Equal(t, generatedTimings[i].EndedAt.UnixMilli(), res[i].EndedAt.UnixMilli(), "diff end times") - - // Verify metadata - metaTests := []struct { - name string - value string - }{ - {name: "source", value: generatedTimings[i].Source}, - } - for _, mt := range metaTests { - t.Run(fmt.Sprintf("verify metadata %s", mt.name), func(t *testing.T) { - contains := codersdk.WorkspaceTimingMetadata{ - Name: mt.name, - Value: mt.value, - } - require.Containsf(t, res[i].Metadata, contains, fmt.Sprintf("metadata %s not found", mt.name)) - }) - } + require.Len(t, res, tt.provisionerTimings) + for i := range res.ProvisionerTimings { + timingRes := res.ProvisionerTimings[i] + require.Equal(t, generatedTimings[i].Resource, timingRes.Resource) + require.Equal(t, generatedTimings[i].Action, timingRes.Action) + require.Equal(t, generatedTimings[i].Stage, timingRes.Stage) + require.Equal(t, generatedTimings[i].JobID, timingRes.JobID) + require.Equal(t, generatedTimings[i].Source, timingRes.Source) + require.Equal(t, generatedTimings[i].StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) + require.Equal(t, generatedTimings[i].EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) } }) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index bd3a2a88375b5..7ccbf0b05345c 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -626,32 +626,31 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) return nil } -// A timing can originate from either a provisioner job or an agent. Each source -// may have different associated data, some of which is useful for users and -// should be displayed. -type WorkspaceTimingMetadata struct { - Name string `json:"name"` - Value string `json:"value"` +type ProvisionerTiming struct { + JobID uuid.UUID `json:"job_id"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + Stage string `json:"stage"` + Source string `json:"source"` + Action string `json:"action"` + Resource string `json:"resource"` } -type WorkspaceTiming struct { - Label string `json:"label"` - Metadata []WorkspaceTimingMetadata `json:"metadata"` - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` +type WorkspaceTimings struct { + ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` } -func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) ([]WorkspaceTiming, error) { +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 []WorkspaceTiming{}, err + return WorkspaceTimings{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return []WorkspaceTiming{}, ReadBodyAsError(res) + return WorkspaceTimings{}, ReadBodyAsError(res) } - var timings []WorkspaceTiming + var timings WorkspaceTimings return timings, json.NewDecoder(res.Body).Decode(&timings) } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 95bc4fc7bf9f3..0d96ea303621a 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4177,6 +4177,32 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | ------ | | `file` | +## codersdk.ProvisionerTiming + +```json +{ + "action": "string", + "ended_at": "string", + "job_id": "string", + "resource": "string", + "source": "string", + "stage": "string", + "started_at": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------ | ------ | -------- | ------------ | ----------- | +| `action` | string | false | | | +| `ended_at` | string | false | | | +| `job_id` | string | false | | | +| `resource` | string | false | | | +| `source` | string | false | | | +| `stage` | string | false | | | +| `started_at` | string | false | | | + ## codersdk.ProxyHealthReport ```json @@ -7454,46 +7480,29 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `deleting` | | `deleted` | -## codersdk.WorkspaceTiming +## codersdk.WorkspaceTimings ```json { - "ended_at": "2019-08-24T14:15:22Z", - "label": "string", - "metadata": [ + "provisioner_timings": [ { - "name": "string", - "value": "string" + "action": "string", + "ended_at": "string", + "job_id": "string", + "resource": "string", + "source": "string", + "stage": "string", + "started_at": "string" } - ], - "started_at": "2019-08-24T14:15:22Z" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------ | ----------------------------------------------------------------------------- | -------- | ------------ | ----------- | -| `ended_at` | string | false | | | -| `label` | string | false | | | -| `metadata` | array of [codersdk.WorkspaceTimingMetadata](#codersdkworkspacetimingmetadata) | false | | | -| `started_at` | string | false | | | - -## codersdk.WorkspaceTimingMetadata - -```json -{ - "name": "string", - "value": "string" + ] } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------- | ------ | -------- | ------------ | ----------- | -| `name` | string | false | | | -| `value` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| --------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | +| `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 c960edb5d3d50..091340f868da2 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1628,40 +1628,26 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ > 200 Response ```json -[ - { - "ended_at": "2019-08-24T14:15:22Z", - "label": "string", - "metadata": [ - { - "name": "string", - "value": "string" - } - ], - "started_at": "2019-08-24T14:15:22Z" - } -] +{ + "provisioner_timings": [ + { + "action": "string", + "ended_at": "string", + "job_id": "string", + "resource": "string", + "source": "string", + "stage": "string", + "started_at": "string" + } + ] +} ``` ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceTiming](schemas.md#codersdkworkspacetiming) | - -

Response Schema

- -Status Code **200** - -| Name | Type | Required | Restrictions | Description | -| -------------- | ----------------- | -------- | ------------ | ----------- | -| `[array item]` | array | false | | | -| `» ended_at` | string(date-time) | false | | | -| `» label` | string | false | | | -| `» metadata` | array | false | | | -| `»» name` | string | false | | | -| `»» value` | string | false | | | -| `» started_at` | string(date-time) | false | | | +| 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). diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fa8ac78dfdb5d..9054876e55741 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1050,6 +1050,17 @@ export interface ProvisionerKey { readonly tags: Record; } +// From codersdk/workspaces.go +export interface ProvisionerTiming { + readonly job_id: string; + readonly started_at: string; + readonly ended_at: string; + readonly stage: string; + readonly source: string; + readonly action: string; + readonly resource: string; +} + // From codersdk/workspaceproxy.go export interface ProxyHealthReport { readonly errors: Readonly>; @@ -2000,17 +2011,8 @@ export interface WorkspaceResourceMetadata { } // From codersdk/workspaces.go -export interface WorkspaceTiming { - readonly label: string; - readonly metadata: Readonly>; - readonly started_at: string; - readonly ended_at: string; -} - -// From codersdk/workspaces.go -export interface WorkspaceTimingMetadata { - readonly name: string; - readonly value: string; +export interface WorkspaceTimings { + readonly provisioner_timings: Readonly>; } // From codersdk/workspaces.go From 4015216374bfb8986ba491dd1349d27a1aa495ca Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 17:13:31 +0000 Subject: [PATCH 04/12] Tweaks --- coderd/apidoc/docs.go | 4 ++-- coderd/apidoc/swagger.json | 4 ++-- coderd/workspaces.go | 4 ++-- coderd/workspaces_test.go | 19 ++++++++++--------- codersdk/workspaces.go | 1 + docs/reference/api/workspaces.md | 2 +- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1052ca63133a3..e2ff0a19ad83b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8226,8 +8226,8 @@ const docTemplate = `{ "tags": [ "Workspaces" ], - "summary": "Return workspace timings by ID", - "operationId": "workspace-timings-by-id", + "summary": "Get workspace timings by ID", + "operationId": "get-workspace-timings-by-id", "parameters": [ { "type": "string", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ea04fbc475321..72e545989f255 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7276,8 +7276,8 @@ ], "produces": ["application/json"], "tags": ["Workspaces"], - "summary": "Return workspace timings by ID", - "operationId": "workspace-timings-by-id", + "summary": "Get workspace timings by ID", + "operationId": "get-workspace-timings-by-id", "parameters": [ { "type": "string", diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b566ddfdc91a0..188ec92818c72 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1740,8 +1740,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } } -// @Summary Return workspace timings by ID -// @ID workspace-timings-by-id +// @Summary Get workspace timings by ID +// @ID get-workspace-timings-by-id // @Security CoderSessionToken // @Produce json // @Tags Workspaces diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 4ecd7ab7309e4..487fea2699520 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3659,7 +3659,7 @@ func TestWorkspaceTimings(t *testing.T) { } // Given - tests := []struct { + testCases := []struct { name string provisionerTimings int workspace workspaceWithBuild @@ -3688,19 +3688,20 @@ func TestWorkspaceTimings(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { t.Parallel() // Generate timings based on test config - generatedTimings := make([]database.ProvisionerJobTiming, tt.provisionerTimings) - if tt.provisionerTimings > 0 { - generatedTimings = makeProvisionerTimings(tt.workspace.build.JobID, tt.provisionerTimings) + generatedTimings := make([]database.ProvisionerJobTiming, tc.provisionerTimings) + if tc.provisionerTimings > 0 { + generatedTimings = makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings) } - res, err := client.WorkspaceTimings(context.Background(), tt.workspace.ID) + res, err := client.WorkspaceTimings(context.Background(), tc.workspace.ID) // When error is expected, than an error is returned - if tt.error { + if tc.error { require.Error(t, err) return } @@ -3708,7 +3709,7 @@ func TestWorkspaceTimings(t *testing.T) { // 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, tt.provisionerTimings) + require.Len(t, res, tc.provisionerTimings) for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] require.Equal(t, generatedTimings[i].Resource, timingRes.Resource) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 7ccbf0b05345c..fb9023812ed15 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -638,6 +638,7 @@ type ProvisionerTiming struct { type WorkspaceTimings struct { ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` + // TODO: Add AgentScriptTimings when it is done https://github.com/coder/coder/issues/14630 } func (c *Client) WorkspaceTimings(ctx context.Context, id uuid.UUID) (WorkspaceTimings, error) { diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 091340f868da2..592c5cbed78f6 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1604,7 +1604,7 @@ 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). -## Return workspace timings by ID +## Get workspace timings by ID ### Code samples From 01b416883949765d8f57ecfe103ff7755296cf7a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 17:21:20 +0000 Subject: [PATCH 05/12] Fix date-time format for swagger --- coderd/apidoc/docs.go | 6 ++++-- coderd/apidoc/swagger.json | 6 ++++-- codersdk/workspaces.go | 4 ++-- docs/reference/api/schemas.md | 8 ++++---- docs/reference/api/workspaces.md | 4 ++-- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e2ff0a19ad83b..f489b90f5cd50 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11664,7 +11664,8 @@ const docTemplate = `{ "type": "string" }, "ended_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "job_id": { "type": "string" @@ -11679,7 +11680,8 @@ const docTemplate = `{ "type": "string" }, "started_at": { - "type": "string" + "type": "string", + "format": "date-time" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 72e545989f255..06bc9d885dfac 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10526,7 +10526,8 @@ "type": "string" }, "ended_at": { - "type": "string" + "type": "string", + "format": "date-time" }, "job_id": { "type": "string" @@ -10541,7 +10542,8 @@ "type": "string" }, "started_at": { - "type": "string" + "type": "string", + "format": "date-time" } } }, diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index fb9023812ed15..5acb8cc4b6c8e 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -628,8 +628,8 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) type ProvisionerTiming struct { JobID uuid.UUID `json:"job_id"` - 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"` Stage string `json:"stage"` Source string `json:"source"` Action string `json:"action"` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 0d96ea303621a..d221852b97d41 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4182,12 +4182,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ```json { "action": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "job_id": "string", "resource": "string", "source": "string", "stage": "string", - "started_at": "string" + "started_at": "2019-08-24T14:15:22Z" } ``` @@ -7487,12 +7487,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "provisioner_timings": [ { "action": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "job_id": "string", "resource": "string", "source": "string", "stage": "string", - "started_at": "string" + "started_at": "2019-08-24T14:15:22Z" } ] } diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 592c5cbed78f6..b53f33823c49a 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1632,12 +1632,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "provisioner_timings": [ { "action": "string", - "ended_at": "string", + "ended_at": "2019-08-24T14:15:22Z", "job_id": "string", "resource": "string", "source": "string", "stage": "string", - "started_at": "string" + "started_at": "2019-08-24T14:15:22Z" } ] } From aec73e8276f1117ad20bbb4bb3703912b408a324 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 17:30:24 +0000 Subject: [PATCH 06/12] Fix uuid format --- codersdk/workspaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 5acb8cc4b6c8e..658af09cdda61 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -627,7 +627,7 @@ func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) } type ProvisionerTiming struct { - JobID uuid.UUID `json:"job_id"` + 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"` From c9cbacfb87c63c62a762bfdf6f08b9e630a5acf4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 17:38:24 +0000 Subject: [PATCH 07/12] Fix make gen --- coderd/apidoc/docs.go | 3 ++- coderd/apidoc/swagger.json | 3 ++- docs/reference/api/schemas.md | 4 ++-- docs/reference/api/workspaces.md | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f489b90f5cd50..6ef218f3beb69 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11668,7 +11668,8 @@ const docTemplate = `{ "format": "date-time" }, "job_id": { - "type": "string" + "type": "string", + "format": "uuid" }, "resource": { "type": "string" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 06bc9d885dfac..df82814aa139d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10530,7 +10530,8 @@ "format": "date-time" }, "job_id": { - "type": "string" + "type": "string", + "format": "uuid" }, "resource": { "type": "string" diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index d221852b97d41..ca57ad4f60a35 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4183,7 +4183,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o { "action": "string", "ended_at": "2019-08-24T14:15:22Z", - "job_id": "string", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", "stage": "string", @@ -7488,7 +7488,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "action": "string", "ended_at": "2019-08-24T14:15:22Z", - "job_id": "string", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", "stage": "string", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index b53f33823c49a..92ce677e6ece9 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1633,7 +1633,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ { "action": "string", "ended_at": "2019-08-24T14:15:22Z", - "job_id": "string", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", "stage": "string", From 0e55b09e4998c057ee9e72bdef8cbb0d2ea93046 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 18:31:48 +0000 Subject: [PATCH 08/12] Fix tests --- coderd/database/dbmem/dbmem.go | 12 +++++++++--- coderd/workspaces_test.go | 22 ++++++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8499a1904dee3..f661581e8dd54 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3298,6 +3298,9 @@ func (q *FakeQuerier) GetProvisionerJobTimingsByJobID(_ context.Context, jobID u if len(timings) == 0 { return nil, sql.ErrNoRows } + sort.Slice(timings, func(i, j int) bool { + return timings[i].StartedAt.Before(timings[j].StartedAt) + }) return timings, nil } @@ -6804,8 +6807,9 @@ func (q *FakeQuerier) InsertProvisionerJobTimings(_ context.Context, arg databas q.mutex.Lock() defer q.mutex.Unlock() + insertedTimings := make([]database.ProvisionerJobTiming, 0, len(arg.StartedAt)) for i := range arg.StartedAt { - q.provisionerJobTimings = append(q.provisionerJobTimings, database.ProvisionerJobTiming{ + timing := database.ProvisionerJobTiming{ JobID: arg.JobID, StartedAt: arg.StartedAt[i], EndedAt: arg.EndedAt[i], @@ -6813,10 +6817,12 @@ func (q *FakeQuerier) InsertProvisionerJobTimings(_ context.Context, arg databas Source: arg.Source[i], Action: arg.Action[i], Resource: arg.Resource[i], - }) + } + q.provisionerJobTimings = append(q.provisionerJobTimings, timing) + insertedTimings = append(insertedTimings, timing) } - return q.provisionerJobTimings, nil + return insertedTimings, nil } func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 487fea2699520..12d8f23aa88c7 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3694,10 +3694,7 @@ func TestWorkspaceTimings(t *testing.T) { t.Parallel() // Generate timings based on test config - generatedTimings := make([]database.ProvisionerJobTiming, tc.provisionerTimings) - if tc.provisionerTimings > 0 { - generatedTimings = makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings) - } + 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 @@ -3709,16 +3706,17 @@ func TestWorkspaceTimings(t *testing.T) { // 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, tc.provisionerTimings) + require.Len(t, res.ProvisionerTimings, tc.provisionerTimings) for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] - require.Equal(t, generatedTimings[i].Resource, timingRes.Resource) - require.Equal(t, generatedTimings[i].Action, timingRes.Action) - require.Equal(t, generatedTimings[i].Stage, timingRes.Stage) - require.Equal(t, generatedTimings[i].JobID, timingRes.JobID) - require.Equal(t, generatedTimings[i].Source, timingRes.Source) - require.Equal(t, generatedTimings[i].StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) - require.Equal(t, generatedTimings[i].EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) + 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()) } }) } From 23286ebf1d7dc940246017376b85ca6c25ff9a5a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 18:45:16 +0000 Subject: [PATCH 09/12] Add authz test --- coderd/database/dbauthz/dbauthz_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 4b4874f34247c..1c21ea98af5ed 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -551,6 +551,11 @@ 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) { + j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) + t := dbgen.ProvisionerJobTimings(s.T(), db, database.InsertProvisionerJobTimingsParams{JobID: j.ID}) + check.Args(j.ID).Asserts().Returns(slice.New(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{}) From b7ad71451529dcbb424ad3bf4d85c71fcab5799c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 18:52:47 +0000 Subject: [PATCH 10/12] Fix authz test --- 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 1c21ea98af5ed..37b2c593367c0 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -554,7 +554,7 @@ func (s *MethodTestSuite) TestProvisionerJob() { s.Run("GetProvisionerJobTimingsByJobID", s.Subtest(func(db database.Store, check *expects) { j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) t := dbgen.ProvisionerJobTimings(s.T(), db, database.InsertProvisionerJobTimingsParams{JobID: j.ID}) - check.Args(j.ID).Asserts().Returns(slice.New(t)) + check.Args(j.ID).Asserts().Returns(t) })) s.Run("GetProvisionerJobsByIDs", s.Subtest(func(db database.Store, check *expects) { a := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) From 10846d27b7a7e2f1070f92fb722f0242cb417b58 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 19:03:34 +0000 Subject: [PATCH 11/12] Try to fix authz test with jobID outside --- coderd/database/dbauthz/dbauthz_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 37b2c593367c0..ebe4674be7de4 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -552,8 +552,20 @@ func (s *MethodTestSuite) TestProvisionerJob() { Asserts(v.RBACObject(tpl), []policy.Action{policy.ActionRead, policy.ActionUpdate}).Returns() })) s.Run("GetProvisionerJobTimingsByJobID", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - t := dbgen.ProvisionerJobTimings(s.T(), db, database.InsertProvisionerJobTimingsParams{JobID: j.ID}) + jobID := uuid.New() + j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: jobID}) + t := dbgen.ProvisionerJobTimings(s.T(), db, database.InsertProvisionerJobTimingsParams{ + JobID: jobID, + 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().Returns(t) })) s.Run("GetProvisionerJobsByIDs", s.Subtest(func(db database.Store, check *expects) { From 9fd7fbeaa6d145cedb5d24095619d6d3346e89b0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 16 Sep 2024 19:18:29 +0000 Subject: [PATCH 12/12] Fix ws unique name --- coderd/workspaces_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 12d8f23aa88c7..4f5064de48cbe 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3601,7 +3601,8 @@ func TestWorkspaceTimings(t *testing.T) { OwnerID: owner.UserID, OrganizationID: owner.OrganizationID, TemplateID: template.ID, - Name: "test-workspace", + // Generate unique name for the workspace + Name: "test-workspace-" + uuid.New().String(), }) jobID := uuid.New() job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{