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{ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy