Add `reason` field for workspace builds (#2438)

* add reason field for workspace build

* add the reason field to FE via API

* update BuildReasonMember to BuildReasonInitiator

* add unit tests

* add more unit tests

* add error for unknown transition

* fix lint

* add documentation

* fix unit tests

* fix generated types

* remove nested transaction

* rename migration file
This commit is contained in:
Abhineet Jain 2022-06-17 13:41:11 -04:00 committed by GitHub
parent 7dcfea10dc
commit 289b98978f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 111 additions and 17 deletions

View File

@ -209,6 +209,17 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
}
provisionerJobID := uuid.New()
now := database.Now()
var buildReason database.BuildReason
switch trans {
case database.WorkspaceTransitionStart:
buildReason = database.BuildReasonAutostart
case database.WorkspaceTransitionStop:
buildReason = database.BuildReasonAutostop
default:
return xerrors.Errorf("Unsupported transition: %q", trans)
}
newProvisionerJob, err := store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: provisionerJobID,
CreatedAt: now,
@ -236,6 +247,7 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
InitiatorID: workspace.OwnerID,
Transition: trans,
JobID: newProvisionerJob.ID,
Reason: buildReason,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)

View File

@ -51,6 +51,9 @@ func TestExecutorAutostartOK(t *testing.T) {
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
}
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
@ -200,6 +203,9 @@ func TestExecutorAutostopOK(t *testing.T) {
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason)
}
func TestExecutorAutostopExtend(t *testing.T) {

View File

@ -1744,6 +1744,7 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser
JobID: arg.JobID,
ProvisionerState: arg.ProvisionerState,
Deadline: arg.Deadline,
Reason: arg.Reason,
}
q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild)
return workspaceBuild, nil

View File

@ -6,6 +6,12 @@ CREATE TYPE audit_action AS ENUM (
'delete'
);
CREATE TYPE build_reason AS ENUM (
'initiator',
'autostart',
'autostop'
);
CREATE TYPE log_level AS ENUM (
'trace',
'debug',
@ -311,7 +317,8 @@ CREATE TABLE workspace_builds (
initiator_id uuid NOT NULL,
provisioner_state bytea,
job_id uuid NOT NULL,
deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
reason build_reason DEFAULT 'initiator'::public.build_reason NOT NULL
);
CREATE TABLE workspace_resources (

View File

@ -0,0 +1,4 @@
ALTER TABLE ONLY workspace_builds
DROP COLUMN IF EXISTS reason;
DROP TYPE build_reason;

View File

@ -0,0 +1,4 @@
CREATE TYPE build_reason AS ENUM ('initiator', 'autostart', 'autostop');
ALTER TABLE ONLY workspace_builds
ADD COLUMN IF NOT EXISTS reason build_reason NOT NULL DEFAULT 'initiator';

View File

@ -34,6 +34,26 @@ func (e *AuditAction) Scan(src interface{}) error {
return nil
}
type BuildReason string
const (
BuildReasonInitiator BuildReason = "initiator"
BuildReasonAutostart BuildReason = "autostart"
BuildReasonAutostop BuildReason = "autostop"
)
func (e *BuildReason) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = BuildReason(s)
case string:
*e = BuildReason(s)
default:
return fmt.Errorf("unsupported scan type for BuildReason: %T", src)
}
return nil
}
type LogLevel string
const (
@ -525,6 +545,7 @@ type WorkspaceBuild struct {
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Deadline time.Time `db:"deadline" json:"deadline"`
Reason BuildReason `db:"reason" json:"reason"`
}
type WorkspaceResource struct {

View File

@ -3327,7 +3327,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3354,12 +3354,13 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}
const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.name, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.name, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
@ -3397,6 +3398,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
); err != nil {
return nil, err
}
@ -3413,7 +3415,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3438,13 +3440,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}
const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3469,13 +3472,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}
const getWorkspaceBuildByWorkspaceID = `-- name: GetWorkspaceBuildByWorkspaceID :many
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3540,6 +3544,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg Get
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
); err != nil {
return nil, err
}
@ -3556,7 +3561,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg Get
const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3585,13 +3590,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}
const getWorkspaceBuildByWorkspaceIDAndName = `-- name: GetWorkspaceBuildByWorkspaceIDAndName :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
FROM
workspace_builds
WHERE
@ -3620,12 +3626,13 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context,
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline FROM workspace_builds WHERE created_at > $1
SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason FROM workspace_builds WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
@ -3650,6 +3657,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
); err != nil {
return nil, err
}
@ -3678,10 +3686,11 @@ INSERT INTO
initiator_id,
job_id,
provisioner_state,
deadline
deadline,
reason
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason
`
type InsertWorkspaceBuildParams struct {
@ -3697,6 +3706,7 @@ type InsertWorkspaceBuildParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
Deadline time.Time `db:"deadline" json:"deadline"`
Reason BuildReason `db:"reason" json:"reason"`
}
func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) {
@ -3713,6 +3723,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa
arg.JobID,
arg.ProvisionerState,
arg.Deadline,
arg.Reason,
)
var i WorkspaceBuild
err := row.Scan(
@ -3728,6 +3739,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa
&i.ProvisionerState,
&i.JobID,
&i.Deadline,
&i.Reason,
)
return i, err
}

View File

@ -114,10 +114,11 @@ INSERT INTO
initiator_id,
job_id,
provisioner_state,
deadline
deadline,
reason
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
-- name: UpdateWorkspaceBuildByID :exec
UPDATE

View File

@ -439,6 +439,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
InitiatorID: apiKey.UserID,
Transition: database.WorkspaceTransition(createBuild.Transition),
JobID: provisionerJob.ID,
Reason: database.BuildReasonInitiator,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
@ -639,6 +640,7 @@ func convertWorkspaceBuild(
InitiatorUsername: initiatorName,
Job: convertProvisionerJob(job),
Deadline: workspaceBuild.Deadline,
Reason: codersdk.BuildReason(workspaceBuild.Reason),
}
}

View File

@ -436,6 +436,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
JobID: provisionerJob.ID,
BuildNumber: 1, // First build!
Deadline: time.Time{}, // provisionerd will set this upon success
Reason: database.BuildReasonInitiator,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
@ -806,6 +807,7 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
ProvisionerState: workspaceBuild.ProvisionerState,
JobID: workspaceBuild.JobID,
Deadline: workspaceBuild.Deadline,
Reason: workspaceBuild.Reason,
}
}
templateByID := map[uuid.UUID]database.Template{}

View File

@ -33,8 +33,10 @@ func TestWorkspace(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_, err := client.Workspace(context.Background(), workspace.ID)
ws, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID)
require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason)
})
t.Run("Deleted", func(t *testing.T) {

View File

@ -19,6 +19,20 @@ const (
WorkspaceTransitionDelete WorkspaceTransition = "delete"
)
type BuildReason string
const (
// "initiator" is used when a workspace build is triggered by a user.
// Combined with the initiator id/username, it indicates which user initiated the build.
BuildReasonInitiator BuildReason = "initiator"
// "autostart" is used when a build to start a workspace is triggered by Autostart.
// The initiator id/username in this case is the workspace owner and can be ignored.
BuildReasonAutostart BuildReason = "autostart"
// "autostop" is used when a build to stop a workspace is triggered by Autostop.
// The initiator id/username in this case is the workspace owner and can be ignored.
BuildReasonAutostop BuildReason = "autostop"
)
// WorkspaceBuild is an at-point representation of a workspace state.
// BuildNumbers start at 1 and increase by 1 for each subsequent build
type WorkspaceBuild struct {
@ -37,6 +51,7 @@ type WorkspaceBuild struct {
InitiatorUsername string `json:"initiator_name"`
Job ProvisionerJob `json:"job"`
Deadline time.Time `json:"deadline"`
Reason BuildReason `db:"reason" json:"reason"`
}
// WorkspaceBuild returns a single workspace build for a workspace.

View File

@ -444,7 +444,7 @@ export interface WorkspaceApp {
readonly icon?: string
}
// From codersdk/workspacebuilds.go:24:6
// From codersdk/workspacebuilds.go:38:6
export interface WorkspaceBuild {
readonly id: string
readonly created_at: string
@ -461,6 +461,7 @@ export interface WorkspaceBuild {
readonly initiator_name: string
readonly job: ProvisionerJob
readonly deadline: string
readonly reason: BuildReason
}
// From codersdk/workspaces.go:84:6
@ -489,6 +490,9 @@ export interface WorkspaceResource {
readonly agents?: WorkspaceAgent[]
}
// From codersdk/workspacebuilds.go:22:6
export type BuildReason = "autostart" | "autostop" | "initiator"
// From codersdk/provisionerdaemons.go:23:6
export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"

View File

@ -144,8 +144,8 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
build_number: 1,
created_at: "2022-05-17T17:39:01.382927298Z",
id: "1",
initiator_id: "",
initiator_name: "",
initiator_id: MockUser.id,
initiator_name: MockUser.username,
job: MockProvisionerJob,
name: "a-workspace-build",
template_version_id: "",
@ -156,6 +156,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
workspace_owner_name: MockUser.username,
workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3",
deadline: "2022-05-17T23:39:00.00Z",
reason: "initiator",
}
export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = {