mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): add workspace timings endpoint (#14648)
This commit is contained in:
Generated
+75
@@ -8288,6 +8288,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/timings": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Get workspace timings by ID",
|
||||
"operationId": "get-workspace-timings-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceTimings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -11697,6 +11732,35 @@ const docTemplate = `{
|
||||
"ProvisionerStorageMethodFile"
|
||||
]
|
||||
},
|
||||
"codersdk.ProvisionerTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ProxyHealthReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14520,6 +14584,17 @@ const docTemplate = `{
|
||||
"WorkspaceStatusDeleted"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceTimings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provisioner_timings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ProvisionerTiming"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceTransition": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Generated
+71
@@ -7334,6 +7334,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/timings": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Get workspace timings by ID",
|
||||
"operationId": "get-workspace-timings-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceTimings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -10555,6 +10586,35 @@
|
||||
"enum": ["file"],
|
||||
"x-enum-varnames": ["ProvisionerStorageMethodFile"]
|
||||
},
|
||||
"codersdk.ProvisionerTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ProxyHealthReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13229,6 +13289,17 @@
|
||||
"WorkspaceStatusDeleted"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceTimings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provisioner_timings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ProvisionerTiming"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceTransition": {
|
||||
"type": "string",
|
||||
"enum": ["start", "stop", "delete"],
|
||||
|
||||
@@ -1157,6 +1157,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) {
|
||||
|
||||
@@ -1791,6 +1791,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 {
|
||||
|
||||
@@ -551,6 +551,23 @@ 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) {
|
||||
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) {
|
||||
a := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{})
|
||||
b := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{})
|
||||
|
||||
@@ -893,6 +893,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)
|
||||
|
||||
@@ -195,6 +195,7 @@ type data struct {
|
||||
workspaces []database.Workspace
|
||||
workspaceProxies []database.WorkspaceProxy
|
||||
customRoles []database.CustomRole
|
||||
provisionerJobTimings []database.ProvisionerJobTiming
|
||||
runtimeConfig map[string]string
|
||||
// Locks is a map of lock names. Any keys within the map are currently
|
||||
// locked.
|
||||
@@ -3290,6 +3291,26 @@ 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
|
||||
}
|
||||
sort.Slice(timings, func(i, j int) bool {
|
||||
return timings[i].StartedAt.Before(timings[j].StartedAt)
|
||||
})
|
||||
|
||||
return timings, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -6783,13 +6804,31 @@ 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()
|
||||
|
||||
insertedTimings := make([]database.ProvisionerJobTiming, 0, len(arg.StartedAt))
|
||||
for i := range arg.StartedAt {
|
||||
timing := 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],
|
||||
}
|
||||
q.provisionerJobTimings = append(q.provisionerJobTimings, timing)
|
||||
insertedTimings = append(insertedTimings, timing)
|
||||
}
|
||||
|
||||
return insertedTimings, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) {
|
||||
|
||||
@@ -921,6 +921,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)
|
||||
|
||||
@@ -1868,6 +1868,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()
|
||||
|
||||
@@ -189,6 +189,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)
|
||||
|
||||
@@ -5422,6 +5422,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
|
||||
|
||||
@@ -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;
|
||||
@@ -1740,6 +1740,55 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Get workspace timings by ID
|
||||
// @ID get-workspace-timings-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.WorkspaceTimings
|
||||
// @Router /workspaces/{workspace}/timings [get]
|
||||
func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
)
|
||||
|
||||
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
provisionerTimings, err := api.Database.GetProvisionerJobTimingsByJobID(ctx, build.JobID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace timings.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type workspaceData struct {
|
||||
templates []database.Template
|
||||
builds []codersdk.WorkspaceBuild
|
||||
|
||||
@@ -3556,3 +3556,169 @@ 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,
|
||||
// Generate unique name for the workspace
|
||||
Name: "test-workspace-" + uuid.New().String(),
|
||||
})
|
||||
jobID := uuid.New()
|
||||
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
||||
ID: jobID,
|
||||
OrganizationID: owner.OrganizationID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
Tags: database.StringMap{jobID.String(): "true"},
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: ws.ID,
|
||||
TemplateVersionID: version.ID,
|
||||
BuildNumber: 1,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
InitiatorID: owner.UserID,
|
||||
JobID: job.ID,
|
||||
})
|
||||
return workspaceWithBuild{
|
||||
Workspace: ws,
|
||||
build: build,
|
||||
}
|
||||
}
|
||||
|
||||
makeProvisionerTimings := func(jobID uuid.UUID, count int) []database.ProvisionerJobTiming {
|
||||
// Use the database.ProvisionerJobTiming struct to mock timings data instead
|
||||
// of directly creating database.InsertProvisionerJobTimingsParams. This
|
||||
// approach makes the mock data easier to understand, as
|
||||
// database.InsertProvisionerJobTimingsParams requires slices of each field
|
||||
// for batch inserts.
|
||||
timings := make([]database.ProvisionerJobTiming, count)
|
||||
now := time.Now()
|
||||
for i := range count {
|
||||
startedAt := now.Add(-time.Hour + time.Duration(i)*time.Minute)
|
||||
endedAt := startedAt.Add(time.Minute)
|
||||
timings[i] = database.ProvisionerJobTiming{
|
||||
StartedAt: startedAt,
|
||||
EndedAt: endedAt,
|
||||
Stage: database.ProvisionerJobTimingStageInit,
|
||||
Action: string(database.AuditActionCreate),
|
||||
Source: "source",
|
||||
Resource: fmt.Sprintf("resource[%d]", i),
|
||||
}
|
||||
}
|
||||
insertParams := database.InsertProvisionerJobTimingsParams{
|
||||
JobID: jobID,
|
||||
}
|
||||
for _, timing := range timings {
|
||||
insertParams.StartedAt = append(insertParams.StartedAt, timing.StartedAt)
|
||||
insertParams.EndedAt = append(insertParams.EndedAt, timing.EndedAt)
|
||||
insertParams.Stage = append(insertParams.Stage, timing.Stage)
|
||||
insertParams.Action = append(insertParams.Action, timing.Action)
|
||||
insertParams.Source = append(insertParams.Source, timing.Source)
|
||||
insertParams.Resource = append(insertParams.Resource, timing.Resource)
|
||||
}
|
||||
return dbgen.ProvisionerJobTimings(t, db, insertParams)
|
||||
}
|
||||
|
||||
// Given
|
||||
testCases := []struct {
|
||||
name string
|
||||
provisionerTimings int
|
||||
workspace workspaceWithBuild
|
||||
error bool
|
||||
}{
|
||||
{
|
||||
name: "workspace with 5 provisioner timings",
|
||||
provisionerTimings: 5,
|
||||
workspace: makeWorkspace(),
|
||||
},
|
||||
{
|
||||
name: "workspace with 2 provisioner timings",
|
||||
provisionerTimings: 2,
|
||||
workspace: makeWorkspace(),
|
||||
},
|
||||
{
|
||||
name: "workspace with 0 provisioner timings",
|
||||
provisionerTimings: 0,
|
||||
workspace: makeWorkspace(),
|
||||
},
|
||||
{
|
||||
name: "workspace not found",
|
||||
provisionerTimings: 0,
|
||||
workspace: workspaceWithBuild{},
|
||||
error: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Generate timings based on test config
|
||||
generatedTimings := makeProvisionerTimings(tc.workspace.build.JobID, tc.provisionerTimings)
|
||||
res, err := client.WorkspaceTimings(context.Background(), tc.workspace.ID)
|
||||
|
||||
// When error is expected, than an error is returned
|
||||
if tc.error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
// When success is expected, than no error is returned and the length and
|
||||
// fields are correctly returned
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.ProvisionerTimings, tc.provisionerTimings)
|
||||
for i := range res.ProvisionerTimings {
|
||||
timingRes := res.ProvisionerTimings[i]
|
||||
genTiming := generatedTimings[i]
|
||||
require.Equal(t, genTiming.Resource, timingRes.Resource)
|
||||
require.Equal(t, genTiming.Action, timingRes.Action)
|
||||
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
|
||||
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
|
||||
require.Equal(t, genTiming.Source, timingRes.Source)
|
||||
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
|
||||
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user