From 1979cde773db922685c095e8b8019f8454aaa581 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 09:58:25 -0600 Subject: [PATCH 01/36] feat: Implement view for workspace builds to include rbac info Removes the need to fetch the workspace to run an rbac check. --- coderd/database/dump.sql | 47 ++++++++++----- .../000101_workspace_build_view.down.sql | 1 + .../000101_workspace_build_view.up.sql | 14 +++++ coderd/database/models.go | 18 ++++++ coderd/database/querier.go | 8 +-- coderd/database/queries.sql.go | 60 ++++++++++++------- coderd/database/queries/workspacebuilds.sql | 22 +++---- coderd/database/sqlc.yaml | 2 + 8 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 coderd/database/migrations/000101_workspace_build_view.down.sql create mode 100644 coderd/database/migrations/000101_workspace_build_view.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 29316b88633c6..69186a82e012a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -561,6 +561,39 @@ CREATE TABLE workspace_builds ( daily_cost integer DEFAULT 0 NOT NULL ); +CREATE TABLE workspaces ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + owner_id uuid NOT NULL, + organization_id uuid NOT NULL, + template_id uuid NOT NULL, + deleted boolean DEFAULT false NOT NULL, + name character varying(64) NOT NULL, + autostart_schedule text, + ttl bigint, + last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL +); + +CREATE VIEW workspace_builds_rbac AS + SELECT workspace_builds.id, + workspace_builds.created_at, + workspace_builds.updated_at, + workspace_builds.workspace_id, + workspace_builds.template_version_id, + workspace_builds.build_number, + workspace_builds.transition, + workspace_builds.initiator_id, + workspace_builds.provisioner_state, + workspace_builds.job_id, + workspace_builds.deadline, + workspace_builds.reason, + workspace_builds.daily_cost, + workspaces.organization_id, + workspaces.owner_id AS workspace_owner_id + FROM (public.workspace_builds + JOIN workspaces ON ((workspace_builds.workspace_id = workspaces.id))); + CREATE TABLE workspace_resource_metadata ( workspace_resource_id uuid NOT NULL, key character varying(1024) NOT NULL, @@ -591,20 +624,6 @@ CREATE TABLE workspace_resources ( daily_cost integer DEFAULT 0 NOT NULL ); -CREATE TABLE workspaces ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - owner_id uuid NOT NULL, - organization_id uuid NOT NULL, - template_id uuid NOT NULL, - deleted boolean DEFAULT false NOT NULL, - name character varying(64) NOT NULL, - autostart_schedule text, - ttl bigint, - last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL -); - ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass); diff --git a/coderd/database/migrations/000101_workspace_build_view.down.sql b/coderd/database/migrations/000101_workspace_build_view.down.sql new file mode 100644 index 0000000000000..a8ce5434bc705 --- /dev/null +++ b/coderd/database/migrations/000101_workspace_build_view.down.sql @@ -0,0 +1 @@ +DROP VIEW workspace_builds_rbac; diff --git a/coderd/database/migrations/000101_workspace_build_view.up.sql b/coderd/database/migrations/000101_workspace_build_view.up.sql new file mode 100644 index 0000000000000..5c7a580730d52 --- /dev/null +++ b/coderd/database/migrations/000101_workspace_build_view.up.sql @@ -0,0 +1,14 @@ +BEGIN; +-- workspace_builds_rbac includes the linked workspace information +-- required to perform RBAC checks on workspace builds without needing +-- to fetch the workspace. +CREATE VIEW workspace_builds_rbac AS +SELECT + workspace_builds.*, + workspaces.organization_id AS organization_id, + workspaces.owner_id AS workspace_owner_id +FROM + workspace_builds +INNER JOIN + workspaces ON workspace_builds.workspace_id = workspaces.id; +COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index 21e22566fc40b..e9f6c9619d598 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1590,6 +1590,8 @@ type WorkspaceBuild struct { Deadline time.Time `db:"deadline" json:"deadline"` Reason BuildReason `db:"reason" json:"reason"` DailyCost int32 `db:"daily_cost" json:"daily_cost"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` } type WorkspaceBuildParameter struct { @@ -1600,6 +1602,22 @@ type WorkspaceBuildParameter struct { Value string `db:"value" json:"value"` } +type WorkspaceBuildThin struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` + 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"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` +} + type WorkspaceResource struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a2849211b60be..cdbfa5921a1fe 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -55,7 +55,7 @@ type sqlcQuerier interface { GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) GetLastUpdateCheck(ctx context.Context) (string, error) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildThin, error) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) GetLicenseByID(ctx context.Context, id int32) (License, error) GetLicenses(ctx context.Context) ([]License, error) @@ -169,7 +169,7 @@ type sqlcQuerier interface { InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) - InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuildThin, error) InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) @@ -205,8 +205,8 @@ type sqlcQuerier interface { UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error - UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) - UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) + UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuildThin, error) + UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuildThin, error) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c2022fa98f502..b972c3d69bb06 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5765,9 +5765,9 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds + workspace_builds_rbac WHERE workspace_id = $1 ORDER BY @@ -5793,6 +5793,8 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ) return i, err } @@ -5812,15 +5814,15 @@ JOIN ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number ` -func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { +func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildThin, error) { rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuilds) if err != nil { return nil, err } defer rows.Close() - var items []WorkspaceBuild + var items []WorkspaceBuildThin for rows.Next() { - var i WorkspaceBuild + var i WorkspaceBuildThin if err := rows.Scan( &i.ID, &i.CreatedAt, @@ -5850,19 +5852,19 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB } const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.organization_id, wb.workspace_owner_id FROM ( SELECT workspace_id, MAX(build_number) as max_build_number FROM - workspace_builds + workspace_builds WHERE workspace_id = ANY($1 :: uuid [ ]) GROUP BY workspace_id ) m JOIN - workspace_builds wb + workspace_builds_rbac wb ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number ` @@ -5889,6 +5891,8 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ); err != nil { return nil, err } @@ -5905,9 +5909,9 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds + workspace_builds_rbac WHERE id = $1 LIMIT @@ -5931,15 +5935,17 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ) return i, err } const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds + workspace_builds_rbac WHERE job_id = $1 LIMIT @@ -5963,15 +5969,17 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ) return i, err } const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds + workspace_builds_rbac WHERE workspace_id = $1 AND build_number = $2 @@ -5999,15 +6007,17 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ) return i, err } const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds + workspace_builds_rbac WHERE workspace_builds.workspace_id = $1 AND workspace_builds.created_at > $2 @@ -6074,6 +6084,8 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ); err != nil { return nil, err } @@ -6089,7 +6101,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE created_at > $1 +SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM workspace_builds_rbac WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { @@ -6115,6 +6127,8 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ); err != nil { return nil, err } @@ -6164,7 +6178,7 @@ type InsertWorkspaceBuildParams struct { Reason BuildReason `db:"reason" json:"reason"` } -func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) { +func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuildThin, error) { row := q.db.QueryRowContext(ctx, insertWorkspaceBuild, arg.ID, arg.CreatedAt, @@ -6179,7 +6193,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa arg.Deadline, arg.Reason, ) - var i WorkspaceBuild + var i WorkspaceBuildThin err := row.Scan( &i.ID, &i.CreatedAt, @@ -6216,14 +6230,14 @@ type UpdateWorkspaceBuildByIDParams struct { Deadline time.Time `db:"deadline" json:"deadline"` } -func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) { +func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuildThin, error) { row := q.db.QueryRowContext(ctx, updateWorkspaceBuildByID, arg.ID, arg.UpdatedAt, arg.ProvisionerState, arg.Deadline, ) - var i WorkspaceBuild + var i WorkspaceBuildThin err := row.Scan( &i.ID, &i.CreatedAt, @@ -6256,9 +6270,9 @@ type UpdateWorkspaceBuildCostByIDParams struct { DailyCost int32 `db:"daily_cost" json:"daily_cost"` } -func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) { +func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuildThin, error) { row := q.db.QueryRowContext(ctx, updateWorkspaceBuildCostByID, arg.ID, arg.DailyCost) - var i WorkspaceBuild + var i WorkspaceBuildThin err := row.Scan( &i.ID, &i.CreatedAt, diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 30658634da4e0..6466093534dbe 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -2,7 +2,7 @@ SELECT * FROM - workspace_builds + workspace_builds_rbac WHERE id = $1 LIMIT @@ -12,20 +12,20 @@ LIMIT SELECT * FROM - workspace_builds + workspace_builds_rbac WHERE job_id = $1 LIMIT 1; -- name: GetWorkspaceBuildsCreatedAfter :many -SELECT * FROM workspace_builds WHERE created_at > $1; +SELECT * FROM workspace_builds_rbac WHERE created_at > $1; -- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT * FROM - workspace_builds + workspace_builds_rbac WHERE workspace_id = $1 AND build_number = $2; @@ -34,11 +34,11 @@ WHERE SELECT * FROM - workspace_builds + workspace_builds_rbac WHERE workspace_builds.workspace_id = $1 AND workspace_builds.created_at > @since - AND CASE + AND CASE -- This allows using the last element on a page as effectively a cursor. -- This is an important option for scripts that need to paginate without -- duplicating or missing data. @@ -65,15 +65,15 @@ LIMIT -- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - * + * FROM - workspace_builds + workspace_builds_rbac WHERE - workspace_id = $1 + workspace_id = $1 ORDER BY build_number desc LIMIT - 1; + 1; -- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many SELECT wb.* @@ -88,7 +88,7 @@ FROM ( workspace_id ) m JOIN - workspace_builds wb + workspace_builds_rbac wb ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; -- name: GetLatestWorkspaceBuilds :many diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index e708a6c4ca065..98c4308da584b 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -20,6 +20,8 @@ overrides: go_type: type: "TemplateACL" rename: + workspace_builds_rbac: WorkspaceBuild + workspace_build: WorkspaceBuildThin api_key: APIKey api_key_scope: APIKeyScope api_key_scope_all: APIKeyScopeAll From 4c0cd840ca0b9b7998c998a8da79801bfa2fd34e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:03:45 -0600 Subject: [PATCH 02/36] chore: Use workspace build as RBAC object --- coderd/database/dbauthz/querier.go | 44 ++++---------- coderd/database/dbauthz/querier_test.go | 42 ++++++------- coderd/database/dbauthz/system.go | 2 +- coderd/database/dbfake/databasefake.go | 67 +++++++++++++-------- coderd/database/dbgen/generator.go | 37 ++++++++---- coderd/database/modelmethods.go | 50 +++++++++++++++ coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 30 ++++----- coderd/database/queries/workspacebuilds.sql | 6 +- coderd/workspacebuilds.go | 5 +- coderd/workspaces.go | 3 +- 11 files changed, 174 insertions(+), 114 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 691f680e42feb..91ca67fc54372 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1179,10 +1179,7 @@ func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesP } func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { - if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil { - return database.WorkspaceBuild{}, err - } - return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) + return fetch(q.log, q.auth, q.db.GetLatestWorkspaceBuildByWorkspaceID)(ctx, workspaceID) } func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { @@ -1320,34 +1317,15 @@ func (q *querier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUI } func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuild, error) { - build, err := q.db.GetWorkspaceBuildByID(ctx, buildID) - if err != nil { - return database.WorkspaceBuild{}, err - } - if _, err := q.GetWorkspaceByID(ctx, build.WorkspaceID); err != nil { - return database.WorkspaceBuild{}, err - } - return build, nil + return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByID)(ctx, buildID) } func (q *querier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { - build, err := q.db.GetWorkspaceBuildByJobID(ctx, jobID) - if err != nil { - return database.WorkspaceBuild{}, err - } - // Authorized fetch - _, err = q.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - return database.WorkspaceBuild{}, err - } - return build, nil + return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByJobID)(ctx, jobID) } func (q *querier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { - if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { - return database.WorkspaceBuild{}, err - } - return q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, arg) + return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber)(ctx, arg) } func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { @@ -1475,10 +1453,10 @@ func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorksp return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg) } -func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { +func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuildThin, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } var action rbac.Action = rbac.ActionUpdate @@ -1487,7 +1465,7 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW } if err = q.authorizeContext(ctx, action, w); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } return q.db.InsertWorkspaceBuild(ctx, arg) @@ -1563,19 +1541,19 @@ func (q *querier) UpdateWorkspaceAutostart(ctx context.Context, arg database.Upd return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceAutostart)(ctx, arg) } -func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { +func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuildThin, error) { build, err := q.db.GetWorkspaceBuildByID(ctx, arg.ID) if err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) if err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace.RBACObject()) if err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } return q.db.UpdateWorkspaceBuildByID(ctx, arg) diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index 0f7e7c4ffa45d..9649a8427e3f7 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -946,16 +946,16 @@ func (s *MethodTestSuite) TestWorkspace() { s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) - check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b) + check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b.WithWorkspace(ws)) })) s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args([]uuid.UUID{ws.ID}).Asserts(ws, rbac.ActionRead).Returns(slice.New(b)) })) s.Run("GetWorkspaceAgentByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(agt) @@ -969,7 +969,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAgentsByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args([]uuid.UUID{res.ID}).Asserts(ws, rbac.ActionRead). @@ -977,7 +977,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentLifecycleStateByIDParams{ @@ -987,7 +987,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentStartupByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentStartupByIDParams{ @@ -996,7 +996,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppByAgentIDAndSlug", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1008,7 +1008,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1018,13 +1018,13 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { aWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: aWs.ID, JobID: uuid.New()}) + aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: aWs.ID, JobID: uuid.New()}).WithWorkspace(aWs) aRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: aBuild.JobID}) aAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: aRes.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: aAgt.ID}) bWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}) + bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) bRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: bBuild.JobID}) bAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: bRes.ID}) b := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: bAgt.ID}) @@ -1035,17 +1035,17 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.ID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.JobID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ WorkspaceID: ws.ID, BuildNumber: build.BuildNumber, @@ -1059,14 +1059,14 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildsByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 1}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 2}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 3}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 1}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildsByWorkspaceIDParams{WorkspaceID: ws.ID}).Asserts(ws, rbac.ActionRead) // ordering })) s.Run("GetWorkspaceByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(ws) @@ -1081,14 +1081,14 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceResourceByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) check.Args(res.ID).Asserts(ws, rbac.ActionRead).Returns(res) })) s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) a := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) b := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) @@ -1097,7 +1097,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("Build/GetWorkspaceResourcesByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) job := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args(job.ID).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceResource{}) })) @@ -1113,7 +1113,7 @@ func (s *MethodTestSuite) TestWorkspace() { tJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: v.JobID, Type: database.ProvisionerJobTypeTemplateVersionImport}) ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) wJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args([]uuid.UUID{tJob.ID, wJob.ID}).Asserts(v.RBACObject(tpl), rbac.ActionRead, ws, rbac.ActionRead).Returns([]database.WorkspaceResource{}) })) diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index 5baf6ad7604eb..f0f01d4bb282e 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -112,7 +112,7 @@ func (q *querier) GetDeploymentDAUs(ctx context.Context) ([]database.GetDeployme } // UpdateWorkspaceBuildCostByID is used by the provisioning system to update the cost of a workspace build. -func (q *querier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { +func (q *querier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuildThin, error) { return q.db.UpdateWorkspaceBuildCostByID(ctx, arg) } diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 7c1ea4b3f4abd..a84a57b71041a 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -60,7 +60,7 @@ func New() database.Store { provisionerJobs: make([]database.ProvisionerJob, 0), templateVersions: make([]database.TemplateVersion, 0), templates: make([]database.Template, 0), - workspaceBuilds: make([]database.WorkspaceBuild, 0), + workspaceBuilds: make([]database.WorkspaceBuildThin, 0), workspaceApps: make([]database.WorkspaceApp, 0), workspaces: make([]database.Workspace, 0), licenses: make([]database.License, 0), @@ -118,7 +118,7 @@ type data struct { templates []database.Template workspaceAgents []database.WorkspaceAgent workspaceApps []database.WorkspaceApp - workspaceBuilds []database.WorkspaceBuild + workspaceBuilds []database.WorkspaceBuildThin workspaceBuildParameters []database.WorkspaceBuildParameter workspaceResourceMetadata []database.WorkspaceResourceMetadatum workspaceResources []database.WorkspaceResource @@ -1208,7 +1208,7 @@ func (q *fakeQuerier) GetWorkspaceByAgentID(_ context.Context, agentID uuid.UUID return database.Workspace{}, sql.ErrNoRows } - var build database.WorkspaceBuild + var build database.WorkspaceBuildThin for _, _build := range q.workspaceBuilds { if _build.JobID == resource.JobID { build = _build @@ -1322,13 +1322,28 @@ func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.U return apps, nil } +// expandWorkspaceThins must be called from a locked context. +func (q *fakeQuerier) expandWorkspaceThins(thins []database.WorkspaceBuildThin) []database.WorkspaceBuild { + cpy := make([]database.WorkspaceBuild, 0, len(thins)) + for _, thin := range thins { + cpy = append(cpy, q.expandWorkspaceThin(thin)) + } + return cpy +} + +// expandWorkspaceThin must be called from a locked context. +func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuildThin) database.WorkspaceBuild { + w, _ := q.GetWorkspaceByID(context.Background(), thin.WorkspaceID) + return thin.Expand(w.OrganizationID, w.OwnerID) +} + func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() for _, history := range q.workspaceBuilds { if history.ID == id { - return history, nil + return q.expandWorkspaceThin(history), nil } } return database.WorkspaceBuild{}, sql.ErrNoRows @@ -1340,7 +1355,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI for _, build := range q.workspaceBuilds { if build.JobID == jobID { - return build, nil + return q.expandWorkspaceThin(build), nil } } return database.WorkspaceBuild{}, sql.ErrNoRows @@ -1350,7 +1365,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo q.mutex.RLock() defer q.mutex.RUnlock() - var row database.WorkspaceBuild + var row database.WorkspaceBuildThin var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { @@ -1361,14 +1376,14 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo if buildNum == -1 { return database.WorkspaceBuild{}, sql.ErrNoRows } - return row, nil + return q.expandWorkspaceThin(row), nil } func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuild) + builds := make(map[uuid.UUID]database.WorkspaceBuildThin) buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { id := workspaceBuild.WorkspaceID @@ -1381,7 +1396,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.Wo for i, n := range buildNumbers { if n > 0 { b := builds[i] - returnBuilds = append(returnBuilds, b) + returnBuilds = append(returnBuilds, q.expandWorkspaceThin(b)) } } if len(returnBuilds) == 0 { @@ -1394,7 +1409,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuild) + builds := make(map[uuid.UUID]database.WorkspaceBuildThin) buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { for _, id := range ids { @@ -1408,7 +1423,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, for i, n := range buildNumbers { if n > 0 { b := builds[i] - returnBuilds = append(returnBuilds, b) + returnBuilds = append(returnBuilds, q.expandWorkspaceThin(b)) } } if len(returnBuilds) == 0 { @@ -1427,7 +1442,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, q.mutex.RLock() defer q.mutex.RUnlock() - history := make([]database.WorkspaceBuild, 0) + history := make([]database.WorkspaceBuildThin, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.Before(params.Since) { continue @@ -1438,7 +1453,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, } // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { + slices.SortFunc(history, func(a, b database.WorkspaceBuildThin) bool { // use greater than since we want descending order return a.BuildNumber > b.BuildNumber }) @@ -1477,7 +1492,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, if len(history) == 0 { return nil, sql.ErrNoRows } - return history, nil + return q.expandWorkspaceThins(history), nil } func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { @@ -1495,7 +1510,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con if workspaceBuild.BuildNumber != arg.BuildNumber { continue } - return workspaceBuild, nil + return q.expandWorkspaceThin(workspaceBuild), nil } return database.WorkspaceBuild{}, sql.ErrNoRows } @@ -1521,7 +1536,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after ti workspaceBuilds := make([]database.WorkspaceBuild, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.After(after) { - workspaceBuilds = append(workspaceBuilds, workspaceBuild) + workspaceBuilds = append(workspaceBuilds, q.expandWorkspaceThin(workspaceBuild)) } } return workspaceBuilds, nil @@ -3058,15 +3073,15 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork return workspace, nil } -func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() defer q.mutex.Unlock() - workspaceBuild := database.WorkspaceBuild{ + workspaceBuild := database.WorkspaceBuildThin{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -3490,9 +3505,9 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() @@ -3508,12 +3523,12 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildThin{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() @@ -3527,7 +3542,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg databa q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildThin{}, sql.ErrNoRows } func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { @@ -4365,7 +4380,7 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI continue } - var lastBuild database.WorkspaceBuild + var lastBuild database.WorkspaceBuildThin for _, build := range q.workspaceBuilds { if build.WorkspaceID != workspace.ID { continue diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index cb8f52c06529a..6abdc46e50df8 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -152,20 +152,31 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas return workspace } -func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuild) database.WorkspaceBuild { +type workspaceBuild interface { + database.WorkspaceBuild | database.WorkspaceBuildThin +} + +func WorkspaceBuild[Build workspaceBuild](t testing.TB, db database.Store, orig Build) database.WorkspaceBuildThin { + var input database.WorkspaceBuildThin + switch any(orig).(type) { + case database.WorkspaceBuild: + input = any(orig).(database.WorkspaceBuild).ToThin() + case database.WorkspaceBuildThin: + input = any(orig).(database.WorkspaceBuildThin) + } build, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ - ID: takeFirst(orig.ID, uuid.New()), - CreatedAt: takeFirst(orig.CreatedAt, database.Now()), - UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()), - WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), - TemplateVersionID: takeFirst(orig.TemplateVersionID, uuid.New()), - BuildNumber: takeFirst(orig.BuildNumber, 1), - Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart), - InitiatorID: takeFirst(orig.InitiatorID, uuid.New()), - JobID: takeFirst(orig.JobID, uuid.New()), - ProvisionerState: takeFirstSlice(orig.ProvisionerState, []byte{}), - Deadline: takeFirst(orig.Deadline, database.Now().Add(time.Hour)), - Reason: takeFirst(orig.Reason, database.BuildReasonInitiator), + ID: takeFirst(input.ID, uuid.New()), + CreatedAt: takeFirst(input.CreatedAt, database.Now()), + UpdatedAt: takeFirst(input.UpdatedAt, database.Now()), + WorkspaceID: takeFirst(input.WorkspaceID, uuid.New()), + TemplateVersionID: takeFirst(input.TemplateVersionID, uuid.New()), + BuildNumber: takeFirst(input.BuildNumber, 1), + Transition: takeFirst(input.Transition, database.WorkspaceTransitionStart), + InitiatorID: takeFirst(input.InitiatorID, uuid.New()), + JobID: takeFirst(input.JobID, uuid.New()), + ProvisionerState: takeFirstSlice(input.ProvisionerState, []byte{}), + Deadline: takeFirst(input.Deadline, database.Now().Add(time.Hour)), + Reason: takeFirst(input.Reason, database.BuildReasonInitiator), }) require.NoError(t, err, "insert workspace build") return build diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 44c598697ef8b..6aa9610998178 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -4,6 +4,8 @@ import ( "sort" "strconv" + "github.com/google/uuid" + "github.com/coder/coder/coderd/rbac" ) @@ -74,6 +76,12 @@ func (g Group) RBACObject() rbac.Object { InOrg(g.OrganizationID) } +func (b WorkspaceBuild) RBACObject() rbac.Object { + return rbac.ResourceWorkspace.WithID(b.WorkspaceID). + InOrg(b.OrganizationID). + WithOwner(b.WorkspaceOwnerID.String()) +} + func (w Workspace) RBACObject() rbac.Object { return rbac.ResourceWorkspace.WithID(w.ID). InOrg(w.OrganizationID). @@ -156,6 +164,48 @@ func (l License) RBACObject() rbac.Object { return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10)) } +func (b WorkspaceBuild) ToThin() WorkspaceBuildThin { + return WorkspaceBuildThin{ + ID: b.ID, + CreatedAt: b.CreatedAt, + UpdatedAt: b.UpdatedAt, + WorkspaceID: b.WorkspaceID, + TemplateVersionID: b.TemplateVersionID, + BuildNumber: b.BuildNumber, + Transition: b.Transition, + InitiatorID: b.InitiatorID, + ProvisionerState: b.ProvisionerState, + JobID: b.JobID, + Deadline: b.Deadline, + Reason: b.Reason, + DailyCost: b.DailyCost, + } +} + +func (b WorkspaceBuildThin) WithWorkspace(workspace Workspace) WorkspaceBuild { + return b.Expand(workspace.OrganizationID, workspace.OwnerID) +} + +func (b WorkspaceBuildThin) Expand(orgID, ownerID uuid.UUID) WorkspaceBuild { + return WorkspaceBuild{ + ID: b.ID, + CreatedAt: b.CreatedAt, + UpdatedAt: b.UpdatedAt, + WorkspaceID: b.WorkspaceID, + TemplateVersionID: b.TemplateVersionID, + BuildNumber: b.BuildNumber, + Transition: b.Transition, + InitiatorID: b.InitiatorID, + ProvisionerState: b.ProvisionerState, + JobID: b.JobID, + Deadline: b.Deadline, + Reason: b.Reason, + DailyCost: b.DailyCost, + OrganizationID: orgID, + WorkspaceOwnerID: ownerID, + } +} + func ConvertUserRows(rows []GetUsersRow) []User { users := make([]User, len(rows)) for i, r := range rows { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index cdbfa5921a1fe..db6a0fa219ddf 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -55,7 +55,7 @@ type sqlcQuerier interface { GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) GetLastUpdateCheck(ctx context.Context) (string, error) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildThin, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) GetLicenseByID(ctx context.Context, id int32) (License, error) GetLicenses(ctx context.Context) ([]License, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b972c3d69bb06..c8aef05dbbb4c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5765,15 +5765,15 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM - workspace_builds_rbac + workspace_builds_rbac WHERE - workspace_id = $1 + workspace_id = $1 ORDER BY build_number desc LIMIT - 1 + 1 ` func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) { @@ -5800,7 +5800,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.organization_id, wb.workspace_owner_id FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -5810,19 +5810,19 @@ FROM ( workspace_id ) m JOIN - workspace_builds wb + workspace_builds_rbac wb ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number ` -func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildThin, error) { +func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuilds) if err != nil { return nil, err } defer rows.Close() - var items []WorkspaceBuildThin + var items []WorkspaceBuild for rows.Next() { - var i WorkspaceBuildThin + var i WorkspaceBuild if err := rows.Scan( &i.ID, &i.CreatedAt, @@ -5837,6 +5837,8 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB &i.Deadline, &i.Reason, &i.DailyCost, + &i.OrganizationID, + &i.WorkspaceOwnerID, ); err != nil { return nil, err } @@ -5857,14 +5859,14 @@ FROM ( SELECT workspace_id, MAX(build_number) as max_build_number FROM - workspace_builds + workspace_builds WHERE workspace_id = ANY($1 :: uuid [ ]) GROUP BY workspace_id ) m JOIN - workspace_builds_rbac wb + workspace_builds_rbac wb ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number ` @@ -6019,9 +6021,9 @@ SELECT FROM workspace_builds_rbac WHERE - workspace_builds.workspace_id = $1 - AND workspace_builds.created_at > $2 - AND CASE + workspace_builds_rbac.workspace_id = $1 + AND workspace_builds_rbac.created_at > $2 + AND CASE -- This allows using the last element on a page as effectively a cursor. -- This is an important option for scripts that need to paginate without -- duplicating or missing data. diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 6466093534dbe..da1a9d5356a22 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -36,8 +36,8 @@ SELECT FROM workspace_builds_rbac WHERE - workspace_builds.workspace_id = $1 - AND workspace_builds.created_at > @since + workspace_builds_rbac.workspace_id = $1 + AND workspace_builds_rbac.created_at > @since AND CASE -- This allows using the last element on a page as effectively a cursor. -- This is an important option for scripts that need to paginate without @@ -102,7 +102,7 @@ FROM ( workspace_id ) m JOIN - workspace_builds wb + workspace_builds_rbac wb ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; -- name: InsertWorkspaceBuild :one diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 26176f5a0c93e..32195f60c238b 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -584,7 +584,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("insert provisioner job: %w", err) } - workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + thinBuild, err := db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: workspaceBuildID, CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -601,6 +601,9 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("insert workspace build: %w", err) } + // Assign owning fields. + workspaceBuild = thinBuild.WithWorkspace(workspace) + names := make([]string, 0, len(parameters)) values := make([]string, 0, len(parameters)) for _, param := range parameters { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 5e7398da29f23..f138066eb6d74 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -528,7 +528,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req if err != nil { return xerrors.Errorf("insert provisioner job: %w", err) } - workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + workspaceBuildThin, err := db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: workspaceBuildID, CreatedAt: now, UpdatedAt: now, @@ -544,6 +544,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req if err != nil { return xerrors.Errorf("insert workspace build: %w", err) } + workspaceBuild = workspaceBuildThin.WithWorkspace(workspace) names := make([]string, 0, len(createWorkspace.RichParameterValues)) values := make([]string, 0, len(createWorkspace.RichParameterValues)) From 0b98766227664bf2f57e36f46057599142f3161b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:07:30 -0600 Subject: [PATCH 03/36] Remove uneeded extra code --- coderd/database/dbgen/generator.go | 37 +++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index 6abdc46e50df8..0ec2bcb025eba 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -152,31 +152,20 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas return workspace } -type workspaceBuild interface { - database.WorkspaceBuild | database.WorkspaceBuildThin -} - -func WorkspaceBuild[Build workspaceBuild](t testing.TB, db database.Store, orig Build) database.WorkspaceBuildThin { - var input database.WorkspaceBuildThin - switch any(orig).(type) { - case database.WorkspaceBuild: - input = any(orig).(database.WorkspaceBuild).ToThin() - case database.WorkspaceBuildThin: - input = any(orig).(database.WorkspaceBuildThin) - } +func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuild) database.WorkspaceBuildThin { build, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ - ID: takeFirst(input.ID, uuid.New()), - CreatedAt: takeFirst(input.CreatedAt, database.Now()), - UpdatedAt: takeFirst(input.UpdatedAt, database.Now()), - WorkspaceID: takeFirst(input.WorkspaceID, uuid.New()), - TemplateVersionID: takeFirst(input.TemplateVersionID, uuid.New()), - BuildNumber: takeFirst(input.BuildNumber, 1), - Transition: takeFirst(input.Transition, database.WorkspaceTransitionStart), - InitiatorID: takeFirst(input.InitiatorID, uuid.New()), - JobID: takeFirst(input.JobID, uuid.New()), - ProvisionerState: takeFirstSlice(input.ProvisionerState, []byte{}), - Deadline: takeFirst(input.Deadline, database.Now().Add(time.Hour)), - Reason: takeFirst(input.Reason, database.BuildReasonInitiator), + ID: takeFirst(orig.ID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, database.Now()), + UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()), + WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), + TemplateVersionID: takeFirst(orig.TemplateVersionID, uuid.New()), + BuildNumber: takeFirst(orig.BuildNumber, 1), + Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart), + InitiatorID: takeFirst(orig.InitiatorID, uuid.New()), + JobID: takeFirst(orig.JobID, uuid.New()), + ProvisionerState: takeFirstSlice(orig.ProvisionerState, []byte{}), + Deadline: takeFirst(orig.Deadline, database.Now().Add(time.Hour)), + Reason: takeFirst(orig.Reason, database.BuildReasonInitiator), }) require.NoError(t, err, "insert workspace build") return build From 8a269470f7acc5cc261443192a914d341de43600 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:17:53 -0600 Subject: [PATCH 04/36] UsePostFilter --- coderd/database/dbauthz/querier.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index d1ba3604f7ee5..771a8583f3b62 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1143,17 +1143,7 @@ func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, work } func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { - // This is not ideal as not all builds will be returned if the workspace cannot be read. - // This should probably be handled differently? Maybe join workspace builds with workspace - // ownership properties and filter on that. - for _, id := range ids { - _, err := q.GetWorkspaceByID(ctx, id) - if err != nil { - return nil, err - } - } - - return q.db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, ids) + return fetchWithPostFilter(q.auth, q.db.GetLatestWorkspaceBuildsByWorkspaceIDs)(ctx, ids) } func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { @@ -1255,10 +1245,19 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil } func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { - if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { + builds, err := q.db.GetWorkspaceBuildsByWorkspaceID(ctx, arg) + if err != nil { + return nil, err + } + if len(builds) == 0 { + return nil, sql.ErrNoRows + } + // All builds come from the same workspace, so we only need to check the first one. + err = q.authorizeContext(ctx, rbac.ActionRead, builds[0]) + if err != nil { return nil, err } - return q.db.GetWorkspaceBuildsByWorkspaceID(ctx, arg) + return builds, nil } func (q *querier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) { From 6bee9af396d02144aea9b0cf21ccfbd370815a46 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:25:21 -0600 Subject: [PATCH 05/36] Remove some db round trips --- coderd/database/dbauthz/querier.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 771a8583f3b62..66653de19be11 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1317,11 +1317,7 @@ func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.U if err != nil { return nil, err } - workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - return nil, err - } - obj = workspace + obj = build default: return nil, xerrors.Errorf("unknown job type: %s", job.Type) } @@ -1362,12 +1358,7 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa return err } - workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - return err - } - - err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace) + err = q.authorizeContext(ctx, rbac.ActionUpdate, build) if err != nil { return err } @@ -1431,11 +1422,7 @@ func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.Upd return database.WorkspaceBuildThin{}, err } - workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) - if err != nil { - return database.WorkspaceBuildThin{}, err - } - err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace.RBACObject()) + err = q.authorizeContext(ctx, rbac.ActionUpdate, build) if err != nil { return database.WorkspaceBuildThin{}, err } From 81165d535e8ba434dba0790fb0abebd4f5daecf6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:27:49 -0600 Subject: [PATCH 06/36] Fix dbgen unit test --- coderd/database/dbgen/generator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbgen/generator_test.go b/coderd/database/dbgen/generator_test.go index c09cc6df8a466..6082d1d33addb 100644 --- a/coderd/database/dbgen/generator_test.go +++ b/coderd/database/dbgen/generator_test.go @@ -167,7 +167,7 @@ func TestGenerator(t *testing.T) { t.Parallel() db := dbfake.New() exp := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{}) - require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID))) + require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).ToThin()) }) t.Run("User", func(t *testing.T) { From e252c0420a5ff7adac7f4234534b739430efffae Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 11:41:55 -0600 Subject: [PATCH 07/36] Fix compile issue --- coderd/provisionerdserver/provisionerdserver_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index bcc0d6b0ad93a..34ee3403fbdf7 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -635,13 +635,14 @@ func TestFailJob(t *testing.T) { ID: uuid.New(), }) require.NoError(t, err) - build, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + buildThin, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: uuid.New(), WorkspaceID: workspace.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, }) require.NoError(t, err) + build := buildThin.WithWorkspace(workspace) input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: build.ID, }) From b698672e83458a57433506d9e44e63036367f484 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 15:28:35 -0600 Subject: [PATCH 08/36] fix merge --- coderd/database/dbfake/databasefake.go | 58 ++++++++++++++++---------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 8823d8d9a8174..27dca53d5b9dc 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -40,7 +40,6 @@ func New() database.Store { mutex: &sync.RWMutex{}, data: &data{ apiKeys: make([]database.APIKey, 0), - agentStats: make([]database.AgentStat, 0), organizationMembers: make([]database.OrganizationMember, 0), organizations: make([]database.Organization, 0), users: make([]database.User, 0), @@ -1214,7 +1213,7 @@ func (q *fakeQuerier) GetWorkspaceByAgentID(_ context.Context, agentID uuid.UUID return database.Workspace{}, sql.ErrNoRows } - var build database.WorkspaceBuild + var build database.WorkspaceBuildThin for _, _build := range q.workspaceBuilds { if _build.JobID == resource.JobID { build = _build @@ -1334,7 +1333,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da for _, history := range q.workspaceBuilds { if history.ID == id { - return history, nil + return q.expandWorkspaceThin(history), nil } } return database.WorkspaceBuild{}, sql.ErrNoRows @@ -1346,7 +1345,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI for _, build := range q.workspaceBuilds { if build.JobID == jobID { - return build, nil + return q.expandWorkspaceThin(build), nil } } return database.WorkspaceBuild{}, sql.ErrNoRows @@ -1360,7 +1359,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { - row = workspaceBuild + row = q.expandWorkspaceThin(workspaceBuild) buildNum = workspaceBuild.BuildNumber } } @@ -1379,7 +1378,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.Wo for _, workspaceBuild := range q.workspaceBuilds { id := workspaceBuild.WorkspaceID if workspaceBuild.BuildNumber > buildNumbers[id] { - builds[id] = workspaceBuild + builds[id] = q.expandWorkspaceThin(workspaceBuild) buildNumbers[id] = workspaceBuild.BuildNumber } } @@ -1405,7 +1404,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, for _, workspaceBuild := range q.workspaceBuilds { for _, id := range ids { if id == workspaceBuild.WorkspaceID && workspaceBuild.BuildNumber > buildNumbers[id] { - builds[id] = workspaceBuild + builds[id] = q.expandWorkspaceThin(workspaceBuild) buildNumbers[id] = workspaceBuild.BuildNumber } } @@ -1433,7 +1432,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, q.mutex.RLock() defer q.mutex.RUnlock() - history := make([]database.WorkspaceBuild, 0) + history := make([]database.WorkspaceBuildThin, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.Before(params.Since) { continue @@ -1444,7 +1443,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, } // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { + slices.SortFunc(history, func(a, b database.WorkspaceBuildThin) bool { // use greater than since we want descending order return a.BuildNumber > b.BuildNumber }) @@ -1483,7 +1482,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, if len(history) == 0 { return nil, sql.ErrNoRows } - return history, nil + return q.expandWorkspaceThins(history), nil } func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { @@ -1501,7 +1500,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con if workspaceBuild.BuildNumber != arg.BuildNumber { continue } - return workspaceBuild, nil + return q.expandWorkspaceThin(workspaceBuild), nil } return database.WorkspaceBuild{}, sql.ErrNoRows } @@ -1527,7 +1526,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after ti workspaceBuilds := make([]database.WorkspaceBuild, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.After(after) { - workspaceBuilds = append(workspaceBuilds, workspaceBuild) + workspaceBuilds = append(workspaceBuilds, q.expandWorkspaceThin(workspaceBuild)) } } return workspaceBuilds, nil @@ -3064,15 +3063,15 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork return workspace, nil } -func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() defer q.mutex.Unlock() - workspaceBuild := database.WorkspaceBuild{ + workspaceBuild := database.WorkspaceBuildThin{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -3496,9 +3495,9 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() @@ -3514,12 +3513,12 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildThin{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuildThin, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildThin{}, err } q.mutex.Lock() @@ -3533,7 +3532,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg databa q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildThin{}, sql.ErrNoRows } func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { @@ -4377,7 +4376,7 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI continue } if build.CreatedAt.After(lastBuild.CreatedAt) { - lastBuild = build + lastBuild = q.expandWorkspaceThin(build) } } sum += int64(lastBuild.DailyCost) @@ -4401,3 +4400,18 @@ func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, } return sql.ErrNoRows } + +// expandWorkspaceThins must be called from a locked context. +func (q *fakeQuerier) expandWorkspaceThins(thins []database.WorkspaceBuildThin) []database.WorkspaceBuild { + cpy := make([]database.WorkspaceBuild, 0, len(thins)) + for _, thin := range thins { + cpy = append(cpy, q.expandWorkspaceThin(thin)) + } + return cpy +} + +// expandWorkspaceThin must be called from a locked context. +func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuildThin) database.WorkspaceBuild { + w, _ := q.GetWorkspaceByID(context.Background(), thin.WorkspaceID) + return thin.Expand(w.OrganizationID, w.OwnerID) +} From 3ce72c513695f91d3e0e12469d8164fbf5d4213e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 Feb 2023 16:27:58 -0600 Subject: [PATCH 09/36] rename migration file +1 --- ...e_build_view.down.sql => 000102_workspace_build_view.down.sql} | 0 ...space_build_view.up.sql => 000102_workspace_build_view.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000101_workspace_build_view.down.sql => 000102_workspace_build_view.down.sql} (100%) rename coderd/database/migrations/{000101_workspace_build_view.up.sql => 000102_workspace_build_view.up.sql} (100%) diff --git a/coderd/database/migrations/000101_workspace_build_view.down.sql b/coderd/database/migrations/000102_workspace_build_view.down.sql similarity index 100% rename from coderd/database/migrations/000101_workspace_build_view.down.sql rename to coderd/database/migrations/000102_workspace_build_view.down.sql diff --git a/coderd/database/migrations/000101_workspace_build_view.up.sql b/coderd/database/migrations/000102_workspace_build_view.up.sql similarity index 100% rename from coderd/database/migrations/000101_workspace_build_view.up.sql rename to coderd/database/migrations/000102_workspace_build_view.up.sql From c3218f71cab8d571c2a95bca08efe2816a41fa86 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 1 Mar 2023 16:59:53 -0600 Subject: [PATCH 10/36] fix fake --- coderd/database/dbfake/databasefake.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 27dca53d5b9dc..93ed5d0f73746 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -4370,13 +4370,13 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI continue } - var lastBuild database.WorkspaceBuild + var lastBuild database.WorkspaceBuildThin for _, build := range q.workspaceBuilds { if build.WorkspaceID != workspace.ID { continue } if build.CreatedAt.After(lastBuild.CreatedAt) { - lastBuild = q.expandWorkspaceThin(build) + lastBuild = build } } sum += int64(lastBuild.DailyCost) From 67a5b4ab28f8a605d13ea951066a00f391846e02 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Mar 2023 16:48:13 -0600 Subject: [PATCH 11/36] chore: Use golang templates instead of sqlc files --- coderd/database/bindvars.go | 91 +++++ .../000102_workspace_build_view.down.sql | 1 - .../000102_workspace_build_view.up.sql | 14 - coderd/database/modelmethods.go | 32 +- coderd/database/modelqueries.go | 60 +++ coderd/database/models.go | 24 +- coderd/database/querier.go | 8 - coderd/database/queries.sql.go | 382 ------------------ coderd/database/queries/workspacebuilds.sql | 107 ----- coderd/database/sqlc.yaml | 1 - coderd/database/sqlx.go | 54 +++ coderd/database/sqlxqueries/README.md | 36 ++ .../sqlxqueries/imgs/goland-gosql.png | Bin 0 -> 32805 bytes .../sqlxqueries/imgs/goland-user-params.png | Bin 0 -> 25338 bytes coderd/database/sqlxqueries/sqlxqueries.go | 51 +++ coderd/database/sqlxqueries/workspace.gosql | 134 ++++++ 16 files changed, 455 insertions(+), 540 deletions(-) create mode 100644 coderd/database/bindvars.go delete mode 100644 coderd/database/migrations/000102_workspace_build_view.down.sql delete mode 100644 coderd/database/migrations/000102_workspace_build_view.up.sql create mode 100644 coderd/database/sqlx.go create mode 100644 coderd/database/sqlxqueries/README.md create mode 100644 coderd/database/sqlxqueries/imgs/goland-gosql.png create mode 100644 coderd/database/sqlxqueries/imgs/goland-user-params.png create mode 100644 coderd/database/sqlxqueries/sqlxqueries.go create mode 100644 coderd/database/sqlxqueries/workspace.gosql diff --git a/coderd/database/bindvars.go b/coderd/database/bindvars.go new file mode 100644 index 0000000000000..42d10330fc27f --- /dev/null +++ b/coderd/database/bindvars.go @@ -0,0 +1,91 @@ +package database + +import ( + "database/sql/driver" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx/reflectx" + "github.com/lib/pq" + + "github.com/coder/coder/coderd/util/slice" +) + +var nameRegex = regexp.MustCompile(`@([a-zA-Z0-9_]+)`) + +// dbmapper grabs struct 'db' tags. +var dbmapper = reflectx.NewMapper("db") +var sqlValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem() + +// bindNamed is an implementation that improves on the SQLx implementation. This +// adjusts the query to use "$#" syntax for arguments instead of "@argument". The +// returned args are the values of the struct fields that match the names in the +// correct order and indexing. +// +// 1. SQLx does not reuse arguments, so "@arg, @arg" will result in two arguments +// "$1, $2" instead of "$1, $1". +// 2. SQLx does not handle uuid arrays. +// 3. SQLx only supports ":name" style arguments and breaks "::" type casting. +func bindNamed(query string, arg interface{}) (newQuery string, args []interface{}, err error) { + // We do not need to implement a sql parser to extract and replace the variable names. + // All names follow a simple regex. + names := nameRegex.FindAllString(query, -1) + // Get all unique names + names = slice.Unique(names) + + // Replace all names with the correct index + for i, name := range names { + rpl := fmt.Sprintf("$%d", i+1) + query = strings.ReplaceAll(query, name, rpl) + // Remove the "@" prefix to match to the "db" struct tag. + names[i] = strings.TrimPrefix(name, "@") + } + + arglist := make([]interface{}, 0, len(names)) + + // This comes straight from SQLx's implementation to get the values + // of the struct fields. + v := reflect.ValueOf(arg) + for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; { + v = v.Elem() + } + + // If there is only 1 argument, and the argument is not a struct, then + // the only argument is the value passed in. This is a nice shortcut + // for simple queries with 1 param like "id". + if v.Type().Kind() != reflect.Struct && len(names) == 1 { + arglist = append(arglist, pqValue(v)) + return query, arglist, nil + } + + err = dbmapper.TraversalsByNameFunc(v.Type(), names, func(i int, t []int) error { + if len(t) == 0 { + return fmt.Errorf("could not find name %s in %#v", names[i], arg) + } + + val := reflectx.FieldByIndexesReadOnly(v, t) + arglist = append(arglist, pqValue(val)) + + return nil + }) + if err != nil { + return "", nil, err + } + + return query, arglist, nil +} + +func pqValue(val reflect.Value) interface{} { + valI := val.Interface() + // Handle some custom types to make arguments easier to use. + switch valI.(type) { + // Feel free to add more types here as needed. + case []uuid.UUID: + return pq.Array(valI) + default: + return valI + } +} diff --git a/coderd/database/migrations/000102_workspace_build_view.down.sql b/coderd/database/migrations/000102_workspace_build_view.down.sql deleted file mode 100644 index a8ce5434bc705..0000000000000 --- a/coderd/database/migrations/000102_workspace_build_view.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP VIEW workspace_builds_rbac; diff --git a/coderd/database/migrations/000102_workspace_build_view.up.sql b/coderd/database/migrations/000102_workspace_build_view.up.sql deleted file mode 100644 index 5c7a580730d52..0000000000000 --- a/coderd/database/migrations/000102_workspace_build_view.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -BEGIN; --- workspace_builds_rbac includes the linked workspace information --- required to perform RBAC checks on workspace builds without needing --- to fetch the workspace. -CREATE VIEW workspace_builds_rbac AS -SELECT - workspace_builds.*, - workspaces.organization_id AS organization_id, - workspaces.owner_id AS workspace_owner_id -FROM - workspace_builds -INNER JOIN - workspaces ON workspace_builds.workspace_id = workspaces.id; -COMMIT; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 6aa9610998178..349e77c47739f 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -188,21 +188,23 @@ func (b WorkspaceBuildThin) WithWorkspace(workspace Workspace) WorkspaceBuild { func (b WorkspaceBuildThin) Expand(orgID, ownerID uuid.UUID) WorkspaceBuild { return WorkspaceBuild{ - ID: b.ID, - CreatedAt: b.CreatedAt, - UpdatedAt: b.UpdatedAt, - WorkspaceID: b.WorkspaceID, - TemplateVersionID: b.TemplateVersionID, - BuildNumber: b.BuildNumber, - Transition: b.Transition, - InitiatorID: b.InitiatorID, - ProvisionerState: b.ProvisionerState, - JobID: b.JobID, - Deadline: b.Deadline, - Reason: b.Reason, - DailyCost: b.DailyCost, - OrganizationID: orgID, - WorkspaceOwnerID: ownerID, + WorkspaceBuildThin: WorkspaceBuildThin{ + ID: b.ID, + CreatedAt: b.CreatedAt, + UpdatedAt: b.UpdatedAt, + WorkspaceID: b.WorkspaceID, + TemplateVersionID: b.TemplateVersionID, + BuildNumber: b.BuildNumber, + Transition: b.Transition, + InitiatorID: b.InitiatorID, + ProvisionerState: b.ProvisionerState, + JobID: b.JobID, + Deadline: b.Deadline, + Reason: b.Reason, + DailyCost: b.DailyCost, + }, + OrganizationID: orgID, + WorkspaceOwnerID: ownerID, } } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 348555285ad03..ef07d993e5658 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/google/uuid" "github.com/lib/pq" @@ -177,6 +178,65 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([ type workspaceQuerier interface { GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) + GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) + GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) +} + +type WorkspaceBuild struct { + WorkspaceBuildThin + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + WorkspaceOwnerID uuid.UUID `db:"owner_id" json:"owner_id"` +} + +func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { + return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByID", id) +} + +func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) { + return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByJobID", jobID) +} + +func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuild, error) { + return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuildsCreatedAfter", after) +} + +type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { + BuildNumber int32 `db:"build_number"` + WorkspaceID uuid.UUID `db:"workspace_id""` +} + +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { + return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", arg) +} + +type GetWorkspaceBuildsByWorkspaceIDParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + Since time.Time `db:"since" json:"since"` + AfterID uuid.UUID `db:"after_id" json:"after_id"` + OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` + LimitOpt int32 `db:"limit_opt" json:"limit_opt"` +} + +func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) { + return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuildsByWorkspaceID", arg) +} + +func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) { + return sqlxGet[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuildByWorkspaceID", workspacedID) +} + +func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) { + return sqlxSelect[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) +} + +func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { + return sqlxSelect[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuilds", nil) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. diff --git a/coderd/database/models.go b/coderd/database/models.go index 4ab0096ee1718..fa1b390f8b1c9 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1581,7 +1581,15 @@ type WorkspaceApp struct { External bool `db:"external" json:"external"` } -type WorkspaceBuild struct { +type WorkspaceBuildParameter struct { + WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"` + // Parameter name + Name string `db:"name" json:"name"` + // Parameter value + Value string `db:"value" json:"value"` +} + +type WorkspaceBuildThin struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -1595,19 +1603,9 @@ type WorkspaceBuild struct { Deadline time.Time `db:"deadline" json:"deadline"` Reason BuildReason `db:"reason" json:"reason"` DailyCost int32 `db:"daily_cost" json:"daily_cost"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` } -type WorkspaceBuildParameter struct { - WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"` - // Parameter name - Name string `db:"name" json:"name"` - // Parameter value - Value string `db:"value" json:"value"` -} - -type WorkspaceBuildThin struct { +type WorkspaceBuildsRbac struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -1621,6 +1619,8 @@ type WorkspaceBuildThin struct { Deadline time.Time `db:"deadline" json:"deadline"` Reason BuildReason `db:"reason" json:"reason"` DailyCost int32 `db:"daily_cost" json:"daily_cost"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` } type WorkspaceResource struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a78bef2976b3b..0dbf783a955a9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -54,9 +54,6 @@ type sqlcQuerier interface { GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) GetLastUpdateCheck(ctx context.Context) (string, error) - GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) GetLicenseByID(ctx context.Context, id int32) (License, error) GetLicenses(ctx context.Context) ([]License, error) GetLogoURL(ctx context.Context) (string, error) @@ -115,12 +112,7 @@ type sqlcQuerier interface { GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) - GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) - GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) - GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c4e109b830678..24e8706284d27 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5783,388 +5783,6 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins return err } -const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one -SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id -FROM - workspace_builds_rbac -WHERE - workspace_id = $1 -ORDER BY - build_number desc -LIMIT - 1 -` - -func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) { - row := q.db.QueryRowContext(ctx, getLatestWorkspaceBuildByWorkspaceID, workspaceID) - var i WorkspaceBuild - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ) - return i, err -} - -const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.organization_id, wb.workspace_owner_id -FROM ( - SELECT - workspace_id, MAX(build_number) as max_build_number - FROM - workspace_builds - GROUP BY - workspace_id -) m -JOIN - workspace_builds_rbac wb -ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number -` - -func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { - rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuilds) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceBuild - for rows.Next() { - var i WorkspaceBuild - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ); 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 getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.organization_id, wb.workspace_owner_id -FROM ( - SELECT - workspace_id, MAX(build_number) as max_build_number - FROM - workspace_builds - WHERE - workspace_id = ANY($1 :: uuid [ ]) - GROUP BY - workspace_id -) m -JOIN - workspace_builds_rbac wb -ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number -` - -func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) { - rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuildsByWorkspaceIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceBuild - for rows.Next() { - var i WorkspaceBuild - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ); 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 getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one -SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id -FROM - workspace_builds_rbac -WHERE - id = $1 -LIMIT - 1 -` - -func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceBuildByID, id) - var i WorkspaceBuild - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ) - return i, err -} - -const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one -SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id -FROM - workspace_builds_rbac -WHERE - job_id = $1 -LIMIT - 1 -` - -func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceBuildByJobID, jobID) - var i WorkspaceBuild - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ) - return i, err -} - -const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one -SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id -FROM - workspace_builds_rbac -WHERE - workspace_id = $1 - AND build_number = $2 -` - -type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` -} - -func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndBuildNumber, arg.WorkspaceID, arg.BuildNumber) - var i WorkspaceBuild - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ) - return i, err -} - -const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many -SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id -FROM - workspace_builds_rbac -WHERE - workspace_builds_rbac.workspace_id = $1 - AND workspace_builds_rbac.created_at > $2 - AND CASE - -- This allows using the last element on a page as effectively a cursor. - -- This is an important option for scripts that need to paginate without - -- duplicating or missing data. - WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ( - -- The pagination cursor is the last ID of the previous page. - -- The query is ordered by the build_number field, so select all - -- rows after the cursor. - build_number > ( - SELECT - build_number - FROM - workspace_builds - WHERE - id = $3 - ) - ) - ELSE true -END -ORDER BY - build_number desc OFFSET $4 -LIMIT - -- A null limit means "no limit", so 0 means return all - NULLIF($5 :: int, 0) -` - -type GetWorkspaceBuildsByWorkspaceIDParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - Since time.Time `db:"since" json:"since"` - AfterID uuid.UUID `db:"after_id" json:"after_id"` - OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` - LimitOpt int32 `db:"limit_opt" json:"limit_opt"` -} - -func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsByWorkspaceID, - arg.WorkspaceID, - arg.Since, - arg.AfterID, - arg.OffsetOpt, - arg.LimitOpt, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceBuild - for rows.Next() { - var i WorkspaceBuild - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ); 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 getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, organization_id, workspace_owner_id FROM workspace_builds_rbac WHERE created_at > $1 -` - -func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsCreatedAfter, createdAt) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceBuild - for rows.Next() { - var i WorkspaceBuild - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.BuildNumber, - &i.Transition, - &i.InitiatorID, - &i.ProvisionerState, - &i.JobID, - &i.Deadline, - &i.Reason, - &i.DailyCost, - &i.OrganizationID, - &i.WorkspaceOwnerID, - ); 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 insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :one INSERT INTO workspace_builds ( diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index da1a9d5356a22..db886b37199ad 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -1,110 +1,3 @@ --- name: GetWorkspaceBuildByID :one -SELECT - * -FROM - workspace_builds_rbac -WHERE - id = $1 -LIMIT - 1; - --- name: GetWorkspaceBuildByJobID :one -SELECT - * -FROM - workspace_builds_rbac -WHERE - job_id = $1 -LIMIT - 1; - --- name: GetWorkspaceBuildsCreatedAfter :many -SELECT * FROM workspace_builds_rbac WHERE created_at > $1; - --- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one -SELECT - * -FROM - workspace_builds_rbac -WHERE - workspace_id = $1 - AND build_number = $2; - --- name: GetWorkspaceBuildsByWorkspaceID :many -SELECT - * -FROM - workspace_builds_rbac -WHERE - workspace_builds_rbac.workspace_id = $1 - AND workspace_builds_rbac.created_at > @since - AND CASE - -- This allows using the last element on a page as effectively a cursor. - -- This is an important option for scripts that need to paginate without - -- duplicating or missing data. - WHEN @after_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ( - -- The pagination cursor is the last ID of the previous page. - -- The query is ordered by the build_number field, so select all - -- rows after the cursor. - build_number > ( - SELECT - build_number - FROM - workspace_builds - WHERE - id = @after_id - ) - ) - ELSE true -END -ORDER BY - build_number desc OFFSET @offset_opt -LIMIT - -- A null limit means "no limit", so 0 means return all - NULLIF(@limit_opt :: int, 0); - --- name: GetLatestWorkspaceBuildByWorkspaceID :one -SELECT - * -FROM - workspace_builds_rbac -WHERE - workspace_id = $1 -ORDER BY - build_number desc -LIMIT - 1; - --- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.* -FROM ( - SELECT - workspace_id, MAX(build_number) as max_build_number - FROM - workspace_builds - WHERE - workspace_id = ANY(@ids :: uuid [ ]) - GROUP BY - workspace_id -) m -JOIN - workspace_builds_rbac wb -ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; - --- name: GetLatestWorkspaceBuilds :many -SELECT wb.* -FROM ( - SELECT - workspace_id, MAX(build_number) as max_build_number - FROM - workspace_builds - GROUP BY - workspace_id -) m -JOIN - workspace_builds_rbac wb -ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; - -- name: InsertWorkspaceBuild :one INSERT INTO workspace_builds ( diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 98c4308da584b..9e42bcaa26ed9 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -20,7 +20,6 @@ overrides: go_type: type: "TemplateACL" rename: - workspace_builds_rbac: WorkspaceBuild workspace_build: WorkspaceBuildThin api_key: APIKey api_key_scope: APIKeyScope diff --git a/coderd/database/sqlx.go b/coderd/database/sqlx.go new file mode 100644 index 0000000000000..29fb44d31ecdc --- /dev/null +++ b/coderd/database/sqlx.go @@ -0,0 +1,54 @@ +package database + +import ( + "context" + + "github.com/coder/coder/coderd/database/sqlxqueries" + "golang.org/x/xerrors" +) + +func sqlxSelect[RT any](ctx context.Context, q *sqlQuerier, queryName string, argument interface{}) ([]RT, error) { + var empty []RT + + query, err := sqlxqueries.Query(queryName, nil) + if err != nil { + return empty, xerrors.Errorf("get query: %w", err) + } + + query, args, err := bindNamed(query, argument) + if err != nil { + return empty, xerrors.Errorf("bind named: %w", err) + } + + // GetContext maps the results of the query to the items slice by struct + // db tags. + err = q.sdb.SelectContext(ctx, &empty, query, args...) + if err != nil { + return empty, xerrors.Errorf("%s: %w", queryName, err) + } + + return empty, nil +} + +func sqlxGet[RT any](ctx context.Context, q *sqlQuerier, queryName string, argument interface{}) (RT, error) { + var empty RT + + query, err := sqlxqueries.Query(queryName, nil) + if err != nil { + return empty, xerrors.Errorf("get query: %w", err) + } + + query, args, err := bindNamed(query, argument) + if err != nil { + return empty, xerrors.Errorf("bind named: %w", err) + } + + // GetContext maps the results of the query to the items slice by struct + // db tags. + err = q.sdb.GetContext(ctx, &empty, query, args...) + if err != nil { + return empty, xerrors.Errorf("%s: %w", queryName, err) + } + + return empty, nil +} diff --git a/coderd/database/sqlxqueries/README.md b/coderd/database/sqlxqueries/README.md new file mode 100644 index 0000000000000..c7d5ac49b5fb0 --- /dev/null +++ b/coderd/database/sqlxqueries/README.md @@ -0,0 +1,36 @@ +# Editor/IDE config + +To edit template files, it is best to configure your IDE to work with go template files. + +## VSCode + +Required extension (Default Golang Extension): https://marketplace.visualstudio.com/items?itemName=golang.Go + +The default extension [supports syntax highlighting](https://github.com/golang/vscode-go/wiki/features#go-template-syntax-highlighting), but requires a configuration change. You must add this section to your golang extension settings: + +```json + "gopls": { + "ui.semanticTokens": true + }, +``` + +Unfortunately, the VSCode extension does not support both go template and postgres highlighting. You can switch between the two with: + +1. `ctl + shift + p` +1. "Change language Mode" +1. "Postgres" or "Go Template File" + - Feel free to create a permanent file association with `*.gosql` files. + + +## Goland + +Goland supports [template highlighting](https://www.jetbrains.com/help/go/integration-with-go-templates.html) out of the box. To associate sql files, add a new file type in **Editor** settings. Select "Go template files". Add a new filename of `*.gosql` and select "postgres" as the "Template Data Language". + + +![Goland language configuration](./imgs/goland-gosql.png) + +It also helps to support the sqlc type variables. You can do this by adding ["User Parameters"](https://www.jetbrains.com/help/datagrip/settings-tools-database-user-parameters.html) in database queries. + +![Goland language configuration](./imgs/goland-user-params.png) + +You can also add `dump.sql` as a DDL data source for proper table column recognition. diff --git a/coderd/database/sqlxqueries/imgs/goland-gosql.png b/coderd/database/sqlxqueries/imgs/goland-gosql.png new file mode 100644 index 0000000000000000000000000000000000000000..ce66ce025376ad7699677616f825ac6768e257ef GIT binary patch literal 32805 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 A82|tP literal 0 HcmV?d00001 diff --git a/coderd/database/sqlxqueries/imgs/goland-user-params.png b/coderd/database/sqlxqueries/imgs/goland-user-params.png new file mode 100644 index 0000000000000000000000000000000000000000..594d208c4a8b04f95d12a7cd9a178b5e6057169c GIT binary patch literal 25338 zcmZ^~Wn7#+&^L;8p|D6x3yT#f#frPT%i^w!ySuk66e}*p9g4fVySqc7NTIm4Ah?^LaFf%f3A=afykEc?HDte-<<}G#DEj^NY%Ug$C&x82I@3 z`uct0;^z7C<%^?}Q+|GaRaLcs2v|W$Swu>jUq~bR%q^{;NuBCChS@oJ<(1VDkx`yL z{y8OW_D&uaAw8wlt#R=QhGw>DMfJ>PLs{jma>`oHk=^RrMn4jhGmF}!>Pg3UTFv2 zrHtyV2xo%{=+=9GY&AMf;8!~M!AManvAKU64GG~Iy1#Qkkku?xj{Ebs@)=uJU}N+x zQ=1Q0=#@;2f=Y;BqhFD~s;Ur7f;-duJGCc|Q9rUk)4T~nH-+T1NBRQW0I2?YlrS%( zHHE*kTr&}MrM+LCo|gYBRI1B7o+2wBGD}6c!faoP;LXjM-IKl@1A-f^EZBtX9rdvM zxsS%>5OzB6o_h51zVMDp|K`@)Ayk1evi>lwLc+?yO#cc=UOb0~s+fbG4=>&=Azgdk z0nSkI#7M)lZx#Q?(L;HC{cg~PC84bT-J!|`#;2ZSphl2Jji+-!e~!GkSgHqukC}o7 zNB@?rkrC$rWu%k7Z?_voS>Acpi5F6i6hb=K+Jrv?RZ>at?&t%qA}Tl}@P`?^#noUz zQzeW(@T1=Q^Ug)FN?*j7PoQ3wJGznI>t^H@zm_wvR({b(3j1-Bw-Tyy;2__!iG~>p z>ZsTu48rRb*9EE@?@UkKh|@acj}Ci1_$7NwuO><@6UqL-m7KJf!V9tjrebL#pzdLt6gWa8xDFvWn4v+GiB{m@BtKD;a9aSsfZ?k;h2W5Xk-*3>5I9kg;R%uK zu{wk}Q8xZHb^UvJ2UyTSCeU{Ms@trE|BFO23Q9*;rm|sa@fAuUgmL8WUlq{2K_VFk zJ&_0ekCXtI9e*0@%z-{WLM47}ED`dy*Lx88;sk~9?M2z2f9;EnrTo8WM0E9JWf<(GVP+6cb;3$ z6;l#&ZdR?UoKqXQJC_a>zpTHN7F_mNGK?!(D&6gP$G>IARxSU-F-7`R8$4D%gRi%S zAOCYsZ8-Q!OMl3$>nTazwv1Kp_A!=*x}m)6pAcNYFNm?s<$|@G zV}?TjbeHbY^0Caf4C2pdN*qP52 z?1KD5RggYZF(CFgyQ*F)v(h5}N6KgZAK#a2r!=a=rj8tVG!*Fx39f_89-dOH-4q!L1hQw9wuY*Mq z&L<#6^y+2ZR~SR{hBR!0PFy3QgJ$y>1zb(3V}uEKOqgtDKH|VHRqI6Gpnba8_O7@B zdix4STqgVjCeI@Wd)2de^E+OR#_!QhkyB==7L^u5+3`bll~+4W-baOH*M4iwjKy6j zXyG)G)*Z|DoEr+VdvvX3eX}TLam`7TiiZ)VQKJ};s(<@DLh<+{o?e}CuR4RMwW>wec6Yce1K{dIlVL?<90-3+v zLsGrCJeZKv-DU)9gS3*;3Iyl+8)W{hUOHa}WZYVk$rc|O5T*O&hYO$H>9mxx*~M?-{?$Vj5%D4p!>;TvD0A;WYpZ>{dAI> zzLv_CG&6GmeME#x__f=R>xj@UWgZV4U|xBvO_#bI?VSsaM}gcz3&B8au95zJ_~z($ zx|YE!u7_WebB3u+4a%k#uq%L;TR9VWw@v)?U*3jUx>57h)+B_wPnf!#@S2#M4qsUD z$$faP9QLHJd_7ueE~0`=*K3hCwxgt{JYI#GNTd=9DQscO<1$|RwzRWn!`JZJ8-}htI?1<9N-qcm(!*8hBNX3pK2k ze!t=rtEhi00^6mI+e7|(%*spZ0yF>n~FOx|l8 zWfq?GlPM)b`wlHFN?vUAX5RQ)JR6=L=im0hT`0@6A)UBPwPPh!_2l>-`0W!AqtBaN zQD@J*h~wCMZoky(&(#w1nV|sN!D)0(iIT6WT84pBTy0ZgasNTH<$SCzm|c5UXsac` zNX#Lmd)b52b&htLhWk;KX@sq&ndm426YN$<)V`>prAle^Y`L3WF|A^uW3@j0+n$(- zSy83{sATyvX1cLc_6)kM+%&r44oBq6q2f2y)OkDw>tzXj$+~f_k;_6?f%avF)|`at zI~a07-Q7mA^3{FvH+6>H`V67-)^>yQ);~Ycyv2l5M+saIvMho?&{Z^+4~D^>t+0rt z=s0D7?DHR~aF3?qiN?(XHNnjXPd!41<36e|mbdT2TqoCmA0A^YNc$SBMH4-k z1Ti#kZP{#P?oKeJiP+sW&n7MD(-7R*EBj?z-MD)EWdCed@r2<^;x}++C(F~-8R_$E zcKqO*tg*z|CO5L!yxQ_jTszQN`%LK%CKa9Vd;|Pz1^M^C)k)||Yu?7=Q+aG-z3vNe za>!=-WO@sr^t=W4Zcm(+P$l@>e8WaRpTKZq=BnK2ds94Oa;2srr@V}O z%V{*o@kU*^$9;s;7HpQv;$<>}?+}>XNM*tp=1ddjAJgU6e6~$W1GV*xck>#z#G6@Z z+>Y3TX#34$!iBqdC z04h$aCU`nF?ukGF6)*5|cK7ulaM=RRXwYVkq02jmITam4)Svp6HLl9j#|)`%9Avn+ z2$SsaTc=nu3clWABr&_IOHHJIcTfDPi4(pEl0|~9rgrWp2!VBD|5UyUmDKdxJf^#y z^dw+%Z{5l~mr&&1D`_D9&Ri$wlHyt~lM&YB_Gf}9h7tq;KzXnJgsNw}+XtR0Xg!BL z$P!A$sOS!=HVIc%6>${TN1LdL?>NXb(bjWSU82r=N}o<2Bi9cnyCtbW+=^lcOpGS1 z9{8F~6j;*17Xk5W*K=Ln{4P6MQt?}Sht2{w-98USLdgd;%!x92N9C6l{8OR8W(!U(yLd(#7q~=3i=_a(4u|g~*$f z+sLK>lViJTb%#hWO^p0L@hT+e#DZtJaaXJANO4$)PeYwwHDwwdyf0(RU8`Y9?Boce z{Ozjr_YQ>bXgX18g2lM-mo3TKw|ROD;sByC`hH~h)Me49n^{bJ?fAKEe2Dm&w8?m0 z=yx+BbTee%D2xoAi;?&1F=DvF3Tt z!whGfngerB39IdhybiN2GG}#DffpaNMnCj>QS=nWo1T~qCllu;D)}D~e|kI3xidHSg0YRu_?Gkm+$*c)9Tt?< zaEX|=qj|LB6tsVCxT3E^&C)|@w$ntohgYZLCa5$?_UX+f*M?3oFIUGhorW{lO;6fM zBp$EaW_`uk+F04Ac;-eKaE`lGA0hv{$-sN^L3nW$AP0w9^UX1 zjIi9d`!Qy0Z)W0LF-T@;{~1A}+`nip!o@c$wND)N^cGP8T5FZN}Pvcw48 zjb@Lk=?Q+*IBMjURP8Z5earms6CTIk1MWpHfqlE2W|$3-$Z_BScSWe!vSb6q%=W4Us*UiRLegi%gfGji@l(5kV?l1!0qd;a>29yvMzf25RMS;>Kh_dojth0C63*`Zlpjq zC)UpqCXH#Npb?DLHJmLWLlJw|A3PLR1t1_Iw{H8<@LmY` zp5f>t2PQsvSNZ|kdvNXr>KUQ9M;rzq4o;?EGJj)vPf7rdHTCyg&OXCe4mw!Z2t)NI z?@Az1+hyJ6T_}C!;^M0}UDl;T2#5uiBv6(dQu2#a6nzA@Pnudi68T%9OENn8o?X5+ zvD{q&I9iFFXxAx>-wA{5_J+4x z5<=rMXV{Y~7|mZaJTgZ}$0*nV>7FyDU$E<9v|T_a7O$EGh6etgsrbPpxkMVISumR5 zPua_)MiXq*OOEmRDbBpmeCK7RT_ofGsZtB3$_~g@#-x7gdVs+SW1{w?Ef*uFN4AHf zx!TO~i;UHTF3C`%5;ZL48#x8q4+l2MT+ZvY=P+Xjo{e{WXQ1UPtLT6$O74WD`0Cdf z_+r<-7X`cTs4bg+1m3SBLe-CXmT_|qC9h5z@Di|Vzf&+H-f;s_K-TT+^l1SmHuYQ$ z88)nMFh=32J0Q)>|G$0!<~u-0|GdHlgK@RzOZ-yB0ZtuL0(U z_WVIDhX%`cPa*k%ZkFT2bhlX~@9aq_>|k^~FA>{Wr_-H?6@4YFNO@NTkxXsW8P0iSPJq9$wOVfV(8w-fl2k&olxBB7x z;}=#{MvY~vE>idf;Tlxvy;PmuuQzuic*d_rGAno4S#ks)AAGD>2EYWCEE?cd`CGOH zypnM$1R$xF6ZqQ-9O}JK7P%9BQ6C7}m%a>u{txj$)K3C^0)A|E%GcA!ox#=1;=!(d z$gT9%9w=f?lV4`5Hwklsx#?&d7+&ptW@?Cwuw7Z#iJgB{MNus|Z_FOIu(_U&?lf+b zCiK)+i^nQUAPaEG2tlQMeRSP{SFS-S1jhXa|mOAxaP1xNcBd&mip3^j*-)$r+!(FDHMG*z}7 z8d?~v=TIijw(a#rH~<(RX1PRSS&Rb+85fO@MWVrTa9u{JG(6W=`_aVJVgS? zxw)E|xz^VZdfupRc<9wMSFbOElOLT;>z8*2Wn&;REn|cPq{k@P3U{BQjf8lNCNzd3 zllZ8F*PH?Z6J+4iI03JBvbmujYqFNKR%ePB3$|td{$DX(VdRbER`%${&c6@PS&D*-iims2s5i2E1rb`cOg@& z(2rLj1A~#gFC7~eo@$2$wO$IM7(`M5uaE$dyGe&RbGAZnG+m6};9!AxX`Xe5h~7o3 zN`MUDzjju4k{|(K&sQNvl$QsF{y^cE2ezf56L^LdWF;+av;ZiUvnY_C-~?ZtXy4D- zY|0~JvvRf+Ugm5;s=4D8ASBF0th^=Gp*)RO?s$!XJlm7N3m41^Ic*wH7>~^QpWjb*z%`8 zaxU(ClVrwG;rAksF)fc0>MInF1}Bo;y6!dZkjMKRd9cT@r2JpQqiG2?3J1}mLr;ZDNYP^ zQ9tLRF-PjniC`LIZ04R7u$pc(k~OlY;em$Ml1jPf6{P`-QW9gP;uyE&G$l5Ro$LAL z3&rjEP`-ASEG?k+6o|ifvth?|c~vzg#x}vd%Cnmd00>CR2jaQSL|@%YW?TmQS@ifQ z?in+2>J_d;kgCp^eS&}~yqT&F@vh&WdNGfC(C4k5)~1 zZo6N+pcAWrw{0#;a0pNZQsxEb{Iy{%N=ssvUhrI+p2h_-%$_ zI3di_w2aGoz9%yodx(bsG*U1oxf9s2p=oM=B5}xd_te2Pvu^hbm^`wh3^#<3p7GK{OkFakGV1^ z8?p8t@L~~(HMS>W85f^upr@`D9q%)^KLPZqBf+@k+f>O?8Sen3dv?s6^wfUeug9i; zd?CB%+#~vnO6UG@vfE&SM@{tl=nu~yCqVd8w(XwKX!@x-ImY6`}q@7b_K(jh$A_tfQe)E;}^kqpFUC^@Dkkbru8Z;7^w+ z*G_O#{4v6sG3N(RfZXIIT@xoA=`kfX!*~XwfdIU8@Xth#gotq8rO0Kqnf)z6%<`#)_GDyc; zD%i}sGBzSuK7_GXZ8tjsPdr40TB{GlB%2uph39#~4 zd_Mbn(=WaCnLFjW5nXPsLmSQ2uGN@H2KMt%0iNN7kh33|5?!xcZgQyIG>5o^1E}jz zdsKIf_Ab8}h1ggjTg@E$NR-DUeBQl$PhIjqqPsk_S+*kz*KSgUwks{rD0Tk&Cpohe z8}Jbh0fqOl>}1ES86y_r6B_GsukNh1C_YY#IIYcr{-OcCJ1K6JYE+_`PL1wl_dYCl zx$`ojq~Qa2(uO+TIuSf1s0qlX)@yJ%bXkVzI zx~SX!Y8gfUN}2k#Cx(mw+C;;_@<$C{EcV|zzlW0Ud^gz6_pIf)??0~=*QKqizTK`v;*-eto ze{cqav;qrTjNP~YKn~Ce;g|kd&8pW(>-_2`B`5$Rw)0|Tr3?$<#hXQUa%GLgtZ1ts zxdmdO_zUeR;HkVxT^@Q(iUothtTC}^!ZE^sq(q;g7Cr$Zs4p+Dd{e%3kbnX@NCM1e zVTWY^DPx2B`|{movb4|cXHV2ffGmGg1&12fS62!P1~pDx2m(hwEE>75fx>}8(naef zW58$%Vf=jS$Ys6d2?GHSCU=3%W-S_KEqk&qPreMSpo#vk46M`1AJWB~Mw|8!Qj zGOXLcFSOQt!~_3j-F9^ zGW*?KnL15zG&Vluh1dB$*4_4(RO%gOMAGY5#p^{M&`5qOG46 zxkw0gX{@;y@&K1kY0c|e*~Qr{Q}YQ}zjqe#xFuUY{4QOYfADyM9tge_$OF&5KdY5# zEp>s#0O_gs*?#;9+X8JgPSak_Hx^fwb7%sZ-AXJ(`V^Ggb>Ja`z_XA0=PH4R`Dc!e zhKX|T7}!Ew*te~#sY}~e=o`uB*XRnP>}skS&2w4>a+FH&jx(1I$|vh_OW$`3{MOPo zS|x++BzX*|e%rvAsj!iPUcR#nn4W<_zUo_r{(MZ$-DUS^V;|3o_R1DXr?h zlaDVEKU=tU_?DXRc=J1azJ`R9pLiiPy;X-UGe2zSjjyAEEBw z8i>)ecb$YJ9L-rn`g1o4BHEIr=al>@p|KdeoOLBR!4`4n25-L#K`M-PpxWCb`?j_& zY&{^Ijn;f9n?Zy z&1d=@wh29KaQdEf+NbmmmgnO!rA^-9n7AU1>CaB=oOLwZ(GUo}8Y1`eR@W_OGtwEc z>^;fikEPDFlmQ+|^qzj=2(WfqVR;W_i~q~(m`~@<%xNN*O;vV@j!tqRkHNAhwRr(Y z3Ckg{?2Ub#$xvVkW)Ae`*Df-^dQaoQETyu~X0|>iX7=XKf%vRVh>-S-(44AHA~~yN zRUuwUVca;G$&jp_imaFQ%G+fVkyz6hkUavh>sl-&+~oCBK1>4&^_*!37z7VsL}*8* zZ(62>E8J7=*UmnRy{$R=zW@MfM`qmT@GJGHiT>5N3Ib379Q0GR*}XuCk41$hKp(&N z0SPGlrPIzJ%Ck=YffzV;^Vq(2cK>MAL4pK&zyXN;=q;w%1^#$miDO%uUHgkHFHr#2 zRa9pN;b6f2ReYf{{8P*q3f99{l@aE$u4H&WNnzhLBVZ;67C0qVdnZZf- z%|_QU8g>!7PG-3^Y#`1H-u68Zjh3t7YrEf-=CvFtPfymsY0vDXGSid6>z;?1=0fWz z1Rs;-4w76b=;0cOvF2_>XnNjhX;)`GZCZ3_2@p_TKOQX}0#Hw=*ZF(~;;eG(sU5P{ zw)vdy{zjF&j}SwTlsJlx$CJv@-5yNev{mye32Am+E&9e2GFlap_cs&K9A4KxvmF z|0tONtv@PO=|U;pA7(~2I*ezGiF^AO6L|9*HH(VB_NQJLp5zhlp06GV;7=LjP+8T$ zw2~y0zeK@*w9l5*&_Y3GyWw9KZNssvO0&(J(bfAR_t%JMrT2{VPpxy9jWtr20+$HW z&E}4M`w4L9caoFYBH^Lj<%1wEfha%6lg(1fP;u>3Qlo6qLiv71;ayz!Vo1gIx- zL;N--Ztmd|;;crE{@*>}MkSY3VfvC^A8RM_*~6~o8Pu5uii9J!sg8JToGO=YGugQ7 zBbwV#hyr5j|JnVn7k2a}?>ezDGh5$=VmiHhu7{thuIPmGR<;X!?FZ&iX9s>ZsC0b% z5=Pq8fHI$UO(R7k=&76@mac=`uMqQYZpr57GY(7(E#vs<>jtGh^3&lK!dO8rs#tOF z0P#f&gm3IHXAGi71V`r}&YJr_C0_QMAHe6~O>7|(K>2)ALxrmFphhn@VR6QPG7;Ns zQI8KNz9=U3I%4(Pa-}CwoUt*Iv#A4n3jzj!W|BG&1D#1ak&Tm*8Ja~tT^l^Z6XOu5 z6x4m3&#H);bdJ|DQO+6W$|VD5YmUP`Di&E^fkH*MohDFIjDjQ7g7Ra^G=X{pM*A{N z6r-YeuTa?`3K9S)@*=Z9h5<8MsBYoM-@DzC)|rNRsXT@*T6os+V4JCrEk>ZQKPrPJ;*dJ-wgVNUSk z9k3Inf?0}IMPxN5yd>-h=<6!wmZD|6&ja@w6tIg-3Ul%(U*TZaB1kajNg5RasYsJu zg6-F3Ja50#n>JVGU<7ECoaqUYCj0QRTUrmKoe+XG^#+(uGR)6p1{KmBwcJqS;W1KF zlyn#=;d8vN&*(3YrjIJ1Z7W)`6#!W2rCb)x4trxiPtiBblKkv}glpcbKc{G4uhFfr zjs*jKRclwj0Yh6M)xaU!njZKa>S-WUiTs6j2trhS732lzUTPHz=`1g-<4G4<3|FJh zW?lc^G$LEN&|iIGft^i7hY@B`O_Nwl-|x3zKAx&o8jL~+0q;pDvR)#HO zc3k+`xX^3FeYN9WdNYNKY?G=BrrUsa24Cv|9fI?gw5sihRyGhaXE2A;TmPPqzs-ND2)#yO0SRA{vr2ZL1 zU>ysT0q4)~CR!oi!q>08m>)(xeI&Fd)abi|^(t47O z%EGpk0oO-r0V2E>I@@M@TXKtJGZ^34?oi6cS18lSerHC`(?|$ZhY>K{r`8~!U9&XY z1`0OZm?)c`us^wYxEn`P*D&CP_i>cW-nY56aM&sUmoe=gww6SChgXTLnEAGA%BwcU zLRLXO-+lStNBlf%kcbw8WsTz9zpkGjC%8#HtLt!KrNS;OTeg)t1ah{MYey=?@qi9Y ze+UOEkDSHhazeCi(j@6^}#ZMjG2AlS5`TZ)Q>5f#DcHc-xQ@V!qcMZzSQGC=Q<6-9QqmLIRg?BP1 zYqy`r+ZeZKHZEl;0m~tajPdN`I%CXg#C-s-T2_BB!4&;*OR{4pETmv~au$fZIdHDO zUrA*#S!sT(UAL8bO1NmKcVyehVQ`)`u-A*=`I5uG4D@sT87Oz(px+lt5Y`jk)t&e|`S+u_FE(E3g$1wx<$v4Z1hc}H7gA2{V&{H<%7sPL;8u%RWiC8PjLP*Y*XD;l?4 z5~+27?erDHf%tJuc;lqkb9GwMxi6J{C=qAiG36W2ossXAqgx|MT)TzWx5`!WZ^Wta z=8S=kKB=}BdMqM;tTN>m@$xvQj4{-yB3@)nY{Rf<5=#X{9PLiA|g zo43+3izj}TF_C#{;=lXy_chdh+%=eR_742^n3BxTNcI!Z>fYJ{5kR{Bup(O5w@X?j zLk;wQ!&q|IoQDp}FPuaF#~;Cd5MVCvr3OQr7ncPOb72298c3Llvtrf2Ls>y9wdmp5 z;^6PLvWz6=h=4y#prPV?>UV6G%?43koD&cknZ9eU^%C-N2Nmt}JZ92t@9^gC6jl|q zMZpOG@Q4UX+G4pRteA6!4t&zH>c#F8J=(L=0FlSNezR@G444&Wf9c1oAA(Yz%$}zO z!e?Wu@wWU3efs^N+<$t!l!BBjt(i1{%b>5!NEKJOZ%u9vg@eA)i&gZTW&J$cg~^tY zJOIhgau1bj_cm-4D52GC+nH&uNtqgT6Q!o-hr{P39r7tH@facR@@svAhSza zls~(qD4vQ}JlcBU6%! zvY7ep%o+45%tEgHYf0utC7PSlpdt8^b*Rah6Aw#48MYQ|vGy)Arbzqsv+JXjE$1@WMAgos1Z?ZE z*+|!}2E@o8ZaSbvt0Ohgq4*tAuV@#?rl^LRSTMMnD(IS_2@!nCwqiy-t{Z@a4rCV0 z?6mp+`6ZH%5E&TUIm|}2Cnj(J`E*(LgARRk1C>iLs*`D9@M_PBz6d(ulJ#Euu(=S? zzm87>H{Pq>Oomhc7pft>Q=H(Z&$V2D>e#ZTZ+N#lag9;RQ)9vVhxqA2ZCtyDYa@_1 zDd@n@II8B&6Zo-@2J`Sj%lv=Aaa#v$rgPDn<3P+aCq-YYNFzp4np!)RPDs$=@-{^D z(zP`uCG2bzX2d&-|m<+PbsW^cg8Sb7rRl{;|QJ0BAcPp<{{3U@qPmv-phWsk<2$oE<9?Oe3K8Jr|B-xx%omnT~{qEY(9PiT~5njLF~&xaTn$`#aio5Id09a}ou>#$qfi zWL-dnAwnB^Rh8f14fA;`5`d5ohZn#7PG}CLYwc?-YXU4|Fb@rx=R1ySX0k(5QAQOK ztV{XQ3oO%d`Pak8X_I^y+tWJT>*TO;b7V=$)QxielYt81&A2IDT{+-)tL*-^>BU+3^zIk zqkv=&CsgRJqWMAJOa2Ka={N0!8g&fn`h0vBf>OG0C(DH!&sHuD-SD8%Tz{=)0HzoM z(O!O29_gPiF0^}?@F{c~$BUaY-lLi|e?9xjkzVsWGQHo5xqWv#&Fb>}V@>9^(w57C zi|YA>vPt+A1(IInv}4KS2NHdjH}>q=gz%Zp10=hvdeQOLq=&0(+ruoJhNh$F+v@cu zj8#e-t4tt^Jbd!3Wb2E~Yl^c}H5ro}65;AJ`Dzudvf|{Q88Ra6C#H!4C9vhop9>-U$Y+~%`8e4XQz%o@*`G}U@hj{Ox(o1$vQjPZiRh~t>BotFkOrvLp7QjtTmrmjS? zRAUu9qX)q%UTA8`j`O<8x)%mh0RpjMhmM`>O&#Z0P6A!Jt3sb8AWOUo<3)!G2|kdk zwXS1hI~g^DOeoHgMK%ozj_2+Ko)I}vei0fl(2s2toHI5hI!#OIUEJ%>T>3f6xcFD~ z+n*RviCgnKC&heLa@9D;tvEC3G}Z=CC7B}vD1guvzyltoBU0_Ae*CVLx_V-6$Y>&| zJHlwDJVf)&c#C8@<#$m^@Q>2Lh80BrIIn$IvBDhr{oI2gxcO-N{{$p?s+Vy+0o!tw zL#l(orpl}NhTp6?8d#J}*bkS{rA~eL-a^Ij%#f(vULvC$@%>yzj)VhAPMAN!;Dc)S zzVpI5Ui>EbrPIgMem}YI=hkvrYu?kMzb-n67YIc4t(lD^)S_g~zP!>(5xRC|8~AE^ zlF2JDa61{Q5B8*J&+c|}(OSQW_z7rTm?i0eu(H5j>8++D7HP17tkZ={?Yy)4^KqN; zkwD)~0bBx0PEU*EU775S_y!8ZuCNm$=+TOK{>WqS*l;{btx+rRC+A3HXtFMPHU}Ua zOVjtBCu6<0MFLff5$b94?{ zDS1);EW(De0T032mX}}E*MD@@D%LsN&Cm<1#~1m>h6#ZFF%W(LiGcOz` zBqL(CUb9^d@;kb@0@BI@HOLqYQlT?h4aKE(B=~U>{5+pbl(1L_MnTPl^Nx$C^0mWV-END`?xm@8#+jI z0rRF!{qx`Zn$KC#A}3YOo)#`H@C{DT?kWe`Sl955(;MyHjiE>kv!J2r&#~wCAA|Dx z&}hT(shT(Mkzo=b=75ac6@C4#j_>8A0_lxbg!e5C7gEOd5xOj6bfxQNKfXWUo$d{9 z4xj$vk*U|v+zQ-oY{VQY3wU}#A5e0qiz`w4-9&g_k^J9m2W!2=FyFTOgUvqHU(s=) zW<5Fg)8;6tzGQ3^PKqY#-p?i*y z2lMbpLA~Jc*U+J|n}59&i~!Ot@a;w+MIWl|PIAv*4ID;afVHqlH8yhP&hjcgGojXWZftn`sNuYb3&j3J#wbso2w# zD4i5t)Q7k3$2WSS>aKmV{h!Rx^w=OL&wta_<(D;Z`d9BO1{g#)qorv`1v14*1R1s9 zUlqnrd_)rFC0XFF75gk=rKR;gPnGGaW-TARkJF+=atv{eU$!R6xMj&aJ2+@$#INo; zsQfTM5F07Dg9}sY=(3Sx;55kT2-7c|j*Jp5uqN*PuqAbhn!VQNpM>=j0iECDXf%Zg zOK2Pb&_2~s7nw`l)2H&}PJnLhV>#X0yPM(ucDJu(_zUkDsS>8XDLR}qQ%?sXG1(1i zwY-KJ3Pq!{j7N&fz(ZRcxWMzDvvprR>N7#nn=GFnb9&F8A|u;fUbs~m-KVd^<_?M; zOE3AB{hrXAXFI-hqCqP3)DPG7pO=ctmY!NoCR7m-0SuOM-wukRn|HKlR1dNT&3OP# zFS#4{{lcv#%W_FxYp{ChK_qORGBClgh$P)%Kd}tIIj~Jkw^qHq{{LzA*?(%O$M5Sr zJ}9asLKad-hn<_o`CUZS!^4rar&O)Hcu|@x>w|L0)#t4~2sN zh~;@;Ltnytw$M}$FT<=e%PfA$IsMGw_A6gHF4r2ReZF26dyf0BSy$KZmV0W+<9U0l z_b3ux7q+Z*ECV*Vz4xTzI}DsC{{P16|MlgANf6^g*`=M1YK{y9OHIC$zKcGs^Y%}5 z#8?YffE(ZXx#bk>s}sx@I?2C2KyZY7OT^5@6-NUQ`wivA$DTijl8sor^Y%raFRdTp{-+(;=V!zh;$rn!EO?B#sgcE7AEMk z(2!dXGkbJ{=NIZHe<^-fcEh}W>-xm$)L_H@PfIa z!?S^hospZDuH^exWGg5AhaS_%q6!WO*53q1D%u=A0}mGmTpLJJx~%*&DDESWKk4r} zD9iy+I8R^ZiS6~JxJ}l^JDczJu<5uwqJ@APNMMk%*ZamEk*@l~95U?R&;O^f?|^Em z=@zvns1X6B7ilUeNbk+iL3;1K6Cjk(1?3<}6O|$$9Vyal=)r)1^ddb1QF;kQAar=a z?|bjw^{>0ueK|R6%AVP?`>f2FGYPk=B|YQal_${)^V6NckaHV+Pj|1JUQam5bGlr* zW;}qMp18t^h?T2S(fU1y*0hZIz~4sAYUUg}7J(TkR(^wIAy;tVfypX$vTq)Lydu#+ z(e{3WJ;nd2%MGz+z7X!M5dG18?|MjL%(V|{FW%@V@09))vTg0Qs7*)5*f;Ze-c5y? zF+Yv{SYa7LK}#(zy~EmcTb61br`R6r@B}8CFSBV4^PvJ1*urkp6MeeddU}QeXAF92 zAnGChhmoA0+#lAMm%O>Dr7kwvkXWY%v2#kXBr}&N`|dr(gy?j%_?U@Rs=!aKr=IFt8UTJC%o}9X6rN}pP%(fq;xFA2#hbjClt#zm(} z2FJdqWy+2}%qyqA!FAN3C$)P7(cC-@_Z@LY?ThU~prWesv zy^RJ%+)A-^{bxZlcb-pjI%xB#^INP>-g@NZQ3zi904A5R85y5Va~hH z=U&8J6>bX@J#nz(V!Q6VI~`KKUtYGqKp_+AcjJ@~=E9sARyzj)#T4K=QK7F&gA3f= z73-~MIYJK*+C)VJDL#N^ILGn2pHzMGN*GoMAtMeqyUEm<^`PG(_jH9U_L{-82I9|W zT0YWv!|s)?Y9t%E?0BL?g?F#V5c6Yx|36y7szs9`QFTWISK{aPy-L-j8-VOOj5DRd za03ioS3LPqgVEYhwRZXXd)*OxHIn!<9n?Je4oE(bpBO989!NGriZTksMxt3 zzqpKCjRoUm0eg+eUefwbEyD9zsWRm+gda#2OFV2M*<|?;BVl@*H=`u=i+9YY?dpt44wh10S}TC0?HMjRb&lkM>l= z*{X})MO^{XdNLC!?}p7StTPiU!o=Ach-6-s`P~o^VXXjHrUi=^#3^JQJ)upZVL$$n zVfQ;p9HgK;(NMH7Z1vru*v5{NU9)w!9K;Nc{B@NGuFGB=DAAeWYjI ztI~!)YCiANu-6VRzKfOXHz~Gs{tD!)g*`8u-ESI(!^7TPKF{o z(xR$L4`F=QL1J;x53FHTG3%&^drqP?n((KRL^Lb6Xqw1xJ;q*wcs~X#EG`!RE~cXY zIY)49<8I7&IQm;X#bl~Sz*(ea>V$hvhOz2xuL)NQzYIIms#=K;R#hPk%ue8cTUWwmZTV`px3M2{>ibU6ZtVl1xy~fp` zKR!1;6}xRUp<)$qu1DBVi89JjxZOVRr{B4w;yzn*or}&5k1knjAw!#cTpyu>mTN>H z%Fl9ATblT6s|jhgO-yEf^A_A|_YA!_&p5S8Z zjrX}1H%&NQM^ufMnp<&n%J&DWvdLp0vu%tNGJ|_DAi2I@(mQ-7i%QUTi|{EOEXbce z{V{^?{n_7f&AsTyQV?g!{TBwPen>-PG0!yrq@GR&l(~BG=*Vg0W(q8S_Zb#)zc!9o z-=d8qS6$OOCr|fA-YznX*ND9;}ah`cql9j|$un=0Kgu5bOmpZ&`` z-smA*{%zwt^S*?GtYcvZ1^khd2nA2qDAyMR{>6ktT8!ufOTkLtSR9>5Cg zPk2vP(Q`E;d(tB${oYLHu$&4C^Y zHzfYYtHGwibA zvT<{NIaZmcd7@OuKF5D|>zn^K>M&f35Ra%j#trTzxv!(FPG!D>7-~sROFtXGw&iq) z?F&Ez`kwAC09O%x3;On`saJl9_|5Wj;R9^#!hQ=%f62E08d86Zs9-0rBCTF%d;AlS zmVdwF_Eh89uT&Y{a9lcVll>;8#ES-)d5hG9-*&C2MOtA*%RmsZakz+pInHY-Uuu*- z+7@B^<1FL2bJp9eDQNrb)egkdz3{2BnRpD&!v3(vGxp59<>a)tLHO{;_vUBLK3&21>2vQNK?;&U)0!*bRnX=~kcqv(G0dAM2kLQnHrya#AUezdd_jQe1-S{LTxy`cOa3UduM@DX;HYV2!?5kJx?lqm1qK zR7Y5_a^uchBdMN>{oLfnAYVlCA)3&VG2?y{ObS(f{Pp}Z)i=hBUk@Nb37~VR*L6!0 zAQTEA1M$3pwlacl00ijYs#eAq&q3qIRICm306r6Td^d!UWe3Jmai)8RCIO0Epa|Cy z?66O=&AzW(to6HPJxCrxz%P4C#-WQkp8n12YGnQ6G_`)#=u|O7T0nN(8vd@_o>r|Z zQi9F+T6D9km>@}?L-5QS^3<*=0%`_eBYHMOz_vb1cf`EP$JHegdPD(v+O-ie z(mT&X7!byJG{$G}Zo9C!H<{D&-LijK*GRE5?^TxSc}TZ6M3BFe0TfX>iq*2Hm@I^a z9rtI##xoV2y8KV52o%6H_&>07@)x0qMrh)3)c4*eF_aLHG~E7^?!pYQddK|+`v0F~ zWB4WtCf&$n@(25T`Uq2t?H{dQZ0OiZ5r{=rZGM5Or^j8ZR$GUZZm^FG97>tqr z{#9o0O$Rb1!W)ga+`1Cs_6YC*>YG!YU!{ICF@V=30lXCY!N*w+@Yz~{&uP5N#3$r2 zayu%T*JdW~rm5LXCa>F^aoodEf0Y4d_C7uv-f!lyH?vY!GaWFx2Y7XsMo}EmOYr`r zNvoV3D?r~BaXV8P@YSvj2YIfl+uuftW(D%#_1fWniF$O&ett6l1Ee13?vsPImQ z3akwB8R&)c*>X25@YR*&3#Efq0Ji}lhuhDKX?a@@87&8y$NnH$#%+psYWbXpNP*|S z(p})5efHd?_{}6xv*EP8xwL6|gokPTMkCsz#C)^%uEK`*2v)PWZgf@f$!du5G%AzV zRpWCMW-`;UJb!PMd>m_1r!%9`iT+i*vHfvk_UQoalb<>H$FAj~w4*uUk7m5r8BR6X z^C&UoOw4}A9j%7IYb{Be?-D#jVbu=y{fEyV=jtbU^=> z%UY}wA=F3}-M9ch(&CHjlmy5f83eV_Sx^D15&F^?Xe)LJF_h{uZlSBU*qay#nyX~` z*4;(`^ANy&1TAmvG{F0jwG;@~D{6wvx5v$}4Q!TMm8$7gII}pV3tvCb1~vbhO2a?i zU%ml+Y?71m>qlB^$XfYVmXNo>D>| z8BHDi{8+&M4-pWG9$>7?zTSAM6`YMOO<{cu5oi&juDma54L2<<=$i}OGa-!1Wyo&g zP1;_KkI;~*nSB^7>n3b!+(#ink)n7xtrO)RM_ds6~OsUs{afo@vX6<@{@H)UnEwVgHH8jzD4c_bAZu3aZSJv_@Lav7Mr zNc-`()rk&yd6e=#g!dV@v_R_fO^19EEQJ*8KQe_(jZ3umIs&r?W7YQea@_%^R%AIS zYAm`HV~rRcX?a2rXdDg4SO;XPh2WGcQ+7p_W&zAJ$<11w@At>}3i4=fu0=i6YpV%_ zo@mfc65;?l!Z80Oi}R4muxZqh>shYy4Nu#8B<-@upLIXHByh;-De+h}IehakIdCn7 zir;!gnBoFwUc@aMb@j1*>E>TvE^B|jl!l$!A(B1ko1+kO+ChMvr)J~!H}T!(@JnZ%}!@XQ*~YxW&Sy!2WU(dn_b`5(c@L+eaMYZ$lO%o5Dh#$6I5}C#sycBh zI`lkG*d>h61pHL@bR7r6zKr!4J4L#wyNx}md~zjxatyWaU?GGVpd2@LG$b$jwb*4w z4#?_T!;PN!&CZf3MoJGQPm7(XeSFC`7gWUdEt85=+at1uZ76{jhLAqAGMXProy~$S zX|9=*8az7b8ddCW4^`eh!i*LN?b3Ls-GF zCUFrxonw>=9Ipc;fsGt%&}%k19>v{u(0N=*neqNZ6-&=wT7EM0+r#ZR*r33&#HvtT zi)p%$9=GCdE+~9|`Qo(B^kg3Wq<^C8QXpX*H~8b+N08DevKJNTsGhFW15hjDv=VT7 z7FgTN8v`Xsj;h!ButuRtKW15QJiEy}{;v;Py*ulisc-uB9_QX9-i5QJOrrjt#9dE+ z1B653CLm7Md^bTEq9D(9d+FaIJdbX~6RALkMuzT10+$}R!#(`Q4#(1}rs>>P=_}0;c_&?4+C`O05ZX?^ ziWuP1p_uS7zC>Hvr{B9!>bQ_6Ej1VxCh1sA#L{3T$Q%YI$$VK~U`zlb&53y2nw`Xl z!kQZ5bq(pAoQLn^)My(;EgmV>nO20<*>co2MG|PrPaI6E!l_r+4j7fun4%;42eWmP zCsxNnt&{UsF%PF@;T1x3tCj!NVfHg$+LkaIUr-+;AJ%|xdZTzf;q8J8LOJ=38FiYz zyiQ-65-Hzv0{$1|1Q~eJfm?oK{oxYzrMpi*Ik)RQAl;b43<+tN-_n&?Ps-&@#Go$d zs4oFmd1`(HVr!U6gob~*y-hi@TX1(_p2-zgcGv^bXnxpziC#dK{bs~sSN7%7L@*5C z{M~$#GG`~T7@uu~i)3LZ#trA}sd;#v=SZ`|1EcRw0^b@L0Gs}8x}RA4NXV%n#*I~{ zLI2)DSBl_woRm(yU5#Izqe5c0fvevIwdyfq_LwRLx|_8Rw41LMZo^UDGOq9X9k>u# z&$yW`nEETUb8&mAL|X8#V?;TxRlPe2${hTJP(9?W5$pOvK-WLDfUxJK{Kyfh267DmYHe@jX#26lL&_9bnmF> zaH+C{L2F2gM$>*Oq4KrU9HY)#;QBlaL!1))6thZvBo_8bPo~lK)@TQ`pkU8pMg)Td z@K2*>#^B{f>JoHp#sz}J((S?q2K~q$=%k+Lvf@++^eft}47}4n=`HL~n?URxk@pYg zB*1S8CPr6{5vAk!+b@^d3Mw5> z{%FcWY!-)QOw|P2&!P~@!j+kN1Gn{zNmX6JX;{W*6^q%fH;$dbbcx9;Q^QbuGo-g< zWjP}E3f(T9J4yeH{~-IXgcRE)Mf*HwG5UDF$+Okz@FJE3ZsHUcCgN7ZnYa~|N9&_m z^n_K)<&-GVKb19e9$tEA=u75UAg*=7$I$#B5%^)Hwyp5(T?lZg-R#cB!x*LtQP(M? zPd4B$pVh*mL#nMg252}QXZJYew?X+#k7dC{kLD7}AGD#^QvJXZ1K?Vj>76!h<3zx& z!TX@EyaU+@T`DOJj5Y9U+C$H~e8FfAb&V?EZ$LDTVMyhE`t%GA`!&2`h zQVuDjT9>;{NVZPKje~V7;<=LvCeKA2rmb6%klFG?0jv;m3-Cld!4J(@6zW^^-T{YO zRrL9CP+;1j!a~HW8@&-p9=Y|BWUfLXH7xXEd@fLyUHF5~{Y`o{muibO;~$s2-dT1Z zguG5ED$y!!n%qaGZ~6G#nYuhf>n>2ep;O#q=6+^#EXzf85eS8e?5EMem!FAf9=H)@ zHq9}fUn3lE$A_^`lEM;ETKk9OU@nHIF#a0@?@FR-e5mv~^@NtL2)%PQv*e+ZtvpI_lsX_n4v8j@5F<+8< zG3|)0)g;XYb?2g1uwzTE=o4wSSYY$pM|FkH&Cmmw@PX1B^}{~7yETdIE!VGRU44=C zWm8Xo)A5nix2sw5=62#EX$>D6fn;o-PWbj!`8XGU@Z{Z+qP_xey4a;f?Cs9G?;2MM z_MXjwYH!51p>tkJo{#Ju%LMyvHy#T5fcf65`soM5#7U;&`H!~n3mLzu&8z(3$NsTy zftyDevC_$@^i2%CKW{8MBbVD^YqeZB-xAOp{~la=jw9gL!qJeoh<+Mj{;+|E13K&RCGr_`48bz=}lbrY=8Sx`UfWu z=FQ2jIdQ;$d{i?Y-&mz9dkb4 z<0D~^5#)LvLc0bPz8kewd!}R6~%U83DqKlCzcYQgpV=@ly~6ekJ82KJnc`8cBW+K zo_7i_)9QChnFX!T-f;lCs2O&uY6ex>cI^(Mx z+@ZpY^WIOo)sc1|!c+2>T-eWprrBk`R)4#uC~Dmbk@%38KRYn|>5T66eUrNLQ~mGQ zQfkwz`y&C7p1$6qJo(BEZG;q~ z;oJ+i3OtwLY4FFmex}x;DV-F*84PQtPS-DMC?V&axc|`nQ-8X)&m!lgF6PIh&UdaK z+n{XUNmAcSggW{o)5iWVOO{N9IkYTWksrxH345T0C6Y%r{Ddy^8W@0_fwSwIU?^xI zpsh#Dzj*Ri6-OeEI%sXA%Q`PEq3`6ulg$*{#X`%wHhdUv0}g5f!(OIpk1|6$6r13) z^0>!H+^;r3!R0YVh+Cy@S4lTww$*QD1uoy39*ND4eqaXC}JJ>4evab(Qcd zQAIl2$1T1+QD4$&hM4cut6n+2Ba{+&63I%t_f5j-z%aF%+rwP)GVr4;e?h?ZD&apV OE-A^W$(Bl6g!~_{uFF0E literal 0 HcmV?d00001 diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go new file mode 100644 index 0000000000000..9979195567bb6 --- /dev/null +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -0,0 +1,51 @@ +package sqlxqueries + +import ( + "bytes" + "embed" + "sync" + "text/template" + + "golang.org/x/xerrors" +) + +//go:embed *.gosql +var sqlxQueries embed.FS + +var ( + // Only parse the queries once. + once sync.Once + cached *template.Template + cachedError error +) + +func queries() (*template.Template, error) { + once.Do(func() { + tpls, err := template.ParseFS(sqlxQueries, "*.gosql") + if err != nil { + cachedError = xerrors.Errorf("developer error parse sqlx queries: %w", err) + } + cached = tpls + }) + + return cached, cachedError +} + +func Query(name string, data interface{}) (string, error) { + tpls, err := queries() + if err != nil { + return "", err + } + + var out bytes.Buffer + // TODO: Should we cache these? + err = tpls.ExecuteTemplate(&out, name, data) + if err != nil { + return "", xerrors.Errorf("execute template %s: %w", name, err) + } + return out.String(), nil +} + +func GetWorkspaceBuildByID() (string, error) { + return Query("GetWorkspaceBuildByID", nil) +} diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql new file mode 100644 index 0000000000000..f81edd2e087aa --- /dev/null +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -0,0 +1,134 @@ +{{ define "workspace_builds_rbac" }} +( +SELECT + workspace_builds.*, + workspaces.organization_id AS organization_id, + workspaces.owner_id AS workspace_owner_id +FROM + workspace_builds +INNER JOIN + workspaces ON workspace_builds.workspace_id = workspaces.id +) +{{ end }} + + +{{ define "GetWorkspaceBuildByID" }} +-- name: GetWorkspaceBuildByID :one +SELECT + * +FROM + {{ template "workspace_builds_rbac" }} +WHERE + id = @build_id +{{ end }} + +{{ define "GetWorkspaceBuildByJobID" }} +-- name: GetWorkspaceBuildByJobID :one +SELECT + * +FROM + {{ template "workspace_builds_rbac" }} +WHERE + job_id = $1 +{{ end }} + +{{ define "GetWorkspaceBuildsCreatedAfter" }} +-- name: GetWorkspaceBuildsCreatedAfter :many +SELECT * FROM {{ template "workspace_builds_rbac" }} WHERE created_at > $1; +{{ end }} + +{{ define "GetWorkspaceBuildByWorkspaceIDAndBuildNumber" }} +-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one +SELECT + * +FROM + {{ template "workspace_builds_rbac" }} +WHERE + workspace_id = $1 + AND build_number = $2; +{{ end }} + +{{ define "GetWorkspaceBuildsByWorkspaceID" }} +-- name: GetWorkspaceBuildsByWorkspaceID :many +SELECT + * +FROM + {{ template "workspace_builds_rbac" }} +WHERE + workspace_builds.workspace_id = $1 + AND workspace_builds.created_at > @since + AND CASE + -- This allows using the last element on a page as effectively a cursor. + -- This is an important option for scripts that need to paginate without + -- duplicating or missing data. + WHEN @after_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ( + -- The pagination cursor is the last ID of the previous page. + -- The query is ordered by the build_number field, so select all + -- rows after the cursor. + build_number > ( + SELECT + build_number + FROM + workspace_builds + WHERE + id = @after_id + ) + ) + ELSE true +END +ORDER BY + build_number desc OFFSET @offset_opt +LIMIT + -- A null limit means "no limit", so 0 means return all + NULLIF(@limit_opt :: int, 0); +{{ end }} + + +{{ define "GetLatestWorkspaceBuildByWorkspaceID" }} +-- name: GetLatestWorkspaceBuildByWorkspaceID :one +SELECT + * +FROM + {{ template "workspace_builds_rbac" }} +WHERE + workspace_id = $1 +ORDER BY + build_number desc +LIMIT + 1; +{{ end }} + + +{{ define "GetLatestWorkspaceBuildsByWorkspaceIDs" }} +-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many +SELECT wb.* +FROM ( + SELECT + workspace_id, MAX(build_number) as max_build_number + FROM + workspace_builds + WHERE + workspace_id = ANY(@ids :: uuid [ ]) + GROUP BY + workspace_id +) m +JOIN + {{ template "workspace_builds_rbac" }} wb +ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; +{{ end }} + +{{ define "GetLatestWorkspaceBuilds" }} +-- name: GetLatestWorkspaceBuilds :many +SELECT wb.* +FROM ( + SELECT + workspace_id, MAX(build_number) as max_build_number + FROM + workspace_builds + GROUP BY + workspace_id +) m +JOIN + {{ template "workspace_builds_rbac" }} wb +ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number; +{{ end }} From 6484021a418a82e98f67ed17e179531d052ecc47 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Mar 2023 17:10:59 -0600 Subject: [PATCH 12/36] Combine all queries into 1 --- coderd/database/dbauthz/querier_test.go | 64 ++++++++-------- coderd/database/dbauthz/system_test.go | 2 +- coderd/database/dbgen/generator.go | 39 ++++++---- coderd/database/modelqueries.go | 28 ++++++- coderd/database/sqlx.go | 2 +- coderd/database/sqlxqueries/workspace.gosql | 81 ++++++++++----------- 6 files changed, 121 insertions(+), 95 deletions(-) diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index a446247f64c72..967392bcdd136 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -190,7 +190,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) check.Args(j.ID).Asserts(w, rbac.ActionRead).Returns(j) })) s.Run("TemplateVersion/GetProvisionerJobByID", s.Subtest(func(db database.Store, check *expects) { @@ -223,7 +223,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.UpdateProvisionerJobWithCancelByIDParams{ID: j.ID}).Asserts(w, rbac.ActionUpdate).Returns() })) s.Run("BuildFalseCancel/UpdateProvisionerJobWithCancelByID", s.Subtest(func(db database.Store, check *expects) { @@ -232,7 +232,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.UpdateProvisionerJobWithCancelByIDParams{ID: j.ID}).Asserts(w, rbac.ActionUpdate).Returns() })) s.Run("TemplateVersion/UpdateProvisionerJobWithCancelByID", s.Subtest(func(db database.Store, check *expects) { @@ -282,7 +282,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.GetProvisionerLogsByIDBetweenParams{ JobID: j.ID, }).Asserts(w, rbac.ActionRead).Returns([]database.ProvisionerJobLog{}) @@ -945,31 +945,31 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}) check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b.WithWorkspace(ws)) })) s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args([]uuid.UUID{ws.ID}).Asserts(ws, rbac.ActionRead).Returns(slice.New(b)) })) s.Run("GetWorkspaceAgentByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(agt) })) s.Run("GetWorkspaceAgentByInstanceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.AuthInstanceID.String).Asserts(ws, rbac.ActionRead).Returns(agt) })) s.Run("GetWorkspaceAgentsByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args([]uuid.UUID{res.ID}).Asserts( /*ws, rbac.ActionRead*/ ). @@ -977,7 +977,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentLifecycleStateByIDParams{ @@ -987,7 +987,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentStartupByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentStartupByIDParams{ @@ -996,7 +996,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppByAgentIDAndSlug", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1008,7 +1008,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1018,13 +1018,13 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { aWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: aWs.ID, JobID: uuid.New()}).WithWorkspace(aWs) + aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: aWs.ID, JobID: uuid.New()}).WithWorkspace(aWs) aRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: aBuild.JobID}) aAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: aRes.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: aAgt.ID}) bWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) + bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) bRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: bBuild.JobID}) bAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: bRes.ID}) b := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: bAgt.ID}) @@ -1035,17 +1035,17 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.ID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.JobID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ WorkspaceID: ws.ID, BuildNumber: build.BuildNumber, @@ -1053,20 +1053,20 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}) check.Args(build.ID).Asserts(ws, rbac.ActionRead). Returns([]database.WorkspaceBuildParameter{}) })) s.Run("GetWorkspaceBuildsByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 1}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 1}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildsByWorkspaceIDParams{WorkspaceID: ws.ID}).Asserts(ws, rbac.ActionRead) // ordering })) s.Run("GetWorkspaceByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(ws) @@ -1081,14 +1081,14 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceResourceByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) check.Args(res.ID).Asserts(ws, rbac.ActionRead).Returns(res) })) s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) a := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) b := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) @@ -1097,7 +1097,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("Build/GetWorkspaceResourcesByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) job := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args(job.ID).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceResource{}) })) @@ -1113,7 +1113,7 @@ func (s *MethodTestSuite) TestWorkspace() { tJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: v.JobID, Type: database.ProvisionerJobTypeTemplateVersionImport}) ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) wJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args([]uuid.UUID{tJob.ID, wJob.ID}). Asserts( /*v.RBACObject(tpl), rbac.ActionRead, ws, rbac.ActionRead*/ ). @@ -1146,7 +1146,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { w := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: w.ID}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: w.ID}) check.Args(database.InsertWorkspaceBuildParametersParams{ WorkspaceBuildID: b.ID, Name: []string{"foo", "bar"}, @@ -1163,7 +1163,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentConnectionByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentConnectionByIDParams{ @@ -1178,7 +1178,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAppHealthByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1195,7 +1195,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) check.Args(database.UpdateWorkspaceBuildByIDParams{ ID: build.ID, UpdatedAt: build.UpdatedAt, @@ -1228,7 +1228,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceByWorkspaceAppID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) diff --git a/coderd/database/dbauthz/system_test.go b/coderd/database/dbauthz/system_test.go index d64727f1b6227..09fcdeba0d330 100644 --- a/coderd/database/dbauthz/system_test.go +++ b/coderd/database/dbauthz/system_test.go @@ -109,7 +109,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts() })) s.Run("GetWorkspaceBuildsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{CreatedAt: time.Now().Add(-time.Hour)}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{CreatedAt: time.Now().Add(-time.Hour)}) check.Args(time.Now()).Asserts() })) s.Run("GetWorkspaceAgentsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index 0ec2bcb025eba..29bd75dddb0bd 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -152,20 +152,33 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas return workspace } -func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuild) database.WorkspaceBuildThin { +type AnyWorkspaceBuild interface { + database.WorkspaceBuild | database.WorkspaceBuildThin +} + +func WorkspaceBuild[B AnyWorkspaceBuild](t testing.TB, db database.Store, orig B) database.WorkspaceBuildThin { + var thin database.WorkspaceBuildThin + switch v := any(orig).(type) { + case database.WorkspaceBuild: + thin = v.ToThin() + case database.WorkspaceBuildThin: + thin = v + default: + panic(fmt.Sprintf("developer error: invalid type %T", v)) + } build, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ - ID: takeFirst(orig.ID, uuid.New()), - CreatedAt: takeFirst(orig.CreatedAt, database.Now()), - UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()), - WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), - TemplateVersionID: takeFirst(orig.TemplateVersionID, uuid.New()), - BuildNumber: takeFirst(orig.BuildNumber, 1), - Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart), - InitiatorID: takeFirst(orig.InitiatorID, uuid.New()), - JobID: takeFirst(orig.JobID, uuid.New()), - ProvisionerState: takeFirstSlice(orig.ProvisionerState, []byte{}), - Deadline: takeFirst(orig.Deadline, database.Now().Add(time.Hour)), - Reason: takeFirst(orig.Reason, database.BuildReasonInitiator), + ID: takeFirst(thin.ID, uuid.New()), + CreatedAt: takeFirst(thin.CreatedAt, database.Now()), + UpdatedAt: takeFirst(thin.UpdatedAt, database.Now()), + WorkspaceID: takeFirst(thin.WorkspaceID, uuid.New()), + TemplateVersionID: takeFirst(thin.TemplateVersionID, uuid.New()), + BuildNumber: takeFirst(thin.BuildNumber, 1), + Transition: takeFirst(thin.Transition, database.WorkspaceTransitionStart), + InitiatorID: takeFirst(thin.InitiatorID, uuid.New()), + JobID: takeFirst(thin.JobID, uuid.New()), + ProvisionerState: takeFirstSlice(thin.ProvisionerState, []byte{}), + Deadline: takeFirst(thin.Deadline, database.Now().Add(time.Hour)), + Reason: takeFirst(thin.Reason, database.BuildReasonInitiator), }) require.NoError(t, err, "insert workspace build") return build diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index ef07d993e5658..10d5843bc993c 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -194,16 +194,36 @@ type WorkspaceBuild struct { WorkspaceOwnerID uuid.UUID `db:"owner_id" json:"owner_id"` } +type getWorkspaceBuildParams struct { + BuildID uuid.UUID `db:"build_id"` + JobID uuid.UUID `db:"job_id"` + CreatedAfter time.Time `db:"created_after"` + WorkspaceID uuid.UUID `db:"workspace_id"` + BuildNumber int32 `db:"build_number"` + LimitOpt int32 `db:"limit_opt"` + Latest bool `db:"-"` +} + +func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { + arg.LimitOpt = 1 + return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuild", arg) +} + +func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuild, error) { + arg.LimitOpt = -1 + return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuild", arg) +} + func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { - return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByID", id) + return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{BuildID: id}) } func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) { - return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByJobID", jobID) + return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{JobID: jobID}) } func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuild, error) { - return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuildsCreatedAfter", after) + return q.selectWorkspaceBuild(ctx, getWorkspaceBuildParams{CreatedAfter: after}) } type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { @@ -228,7 +248,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) { - return sqlxGet[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuildByWorkspaceID", workspacedID) + return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{WorkspaceID: workspacedID, Latest: true}) } func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) { diff --git a/coderd/database/sqlx.go b/coderd/database/sqlx.go index 29fb44d31ecdc..1c3a61492228b 100644 --- a/coderd/database/sqlx.go +++ b/coderd/database/sqlx.go @@ -33,7 +33,7 @@ func sqlxSelect[RT any](ctx context.Context, q *sqlQuerier, queryName string, ar func sqlxGet[RT any](ctx context.Context, q *sqlQuerier, queryName string, argument interface{}) (RT, error) { var empty RT - query, err := sqlxqueries.Query(queryName, nil) + query, err := sqlxqueries.Query(queryName, argument) if err != nil { return empty, xerrors.Errorf("get query: %w", err) } diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql index f81edd2e087aa..cf4ab562655bb 100644 --- a/coderd/database/sqlxqueries/workspace.gosql +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -8,45 +8,54 @@ FROM workspace_builds INNER JOIN workspaces ON workspace_builds.workspace_id = workspaces.id -) +); {{ end }} -{{ define "GetWorkspaceBuildByID" }} --- name: GetWorkspaceBuildByID :one +{{ define "GetWorkspaceBuild" }} +-- name: GetWorkspaceBuild :one SELECT * FROM {{ template "workspace_builds_rbac" }} WHERE - id = @build_id -{{ end }} - -{{ define "GetWorkspaceBuildByJobID" }} --- name: GetWorkspaceBuildByJobID :one -SELECT - * -FROM - {{ template "workspace_builds_rbac" }} -WHERE - job_id = $1 + CASE + WHEN @build_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + id = @build_id + ELSE true + END + AND CASE + WHEN @job_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + job_id = @job_id + ELSE true + END + AND CASE + WHEN @job_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + job_id = @job_id + ELSE true + END + AND CASE + WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + created_at > @created_aftet + ELSE true + END + AND CASE + WHEN @workspace_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + workspace_id = @workspace_id + ELSE true + END + AND CASE + WHEN @build_number :: integer != 0 THEN + build_number = @build_number + ELSE true + END +{{ if .Latest }} +ORDER BY + build_number desc {{ end }} - -{{ define "GetWorkspaceBuildsCreatedAfter" }} --- name: GetWorkspaceBuildsCreatedAfter :many -SELECT * FROM {{ template "workspace_builds_rbac" }} WHERE created_at > $1; +LIMIT @limit_opt; {{ end }} -{{ define "GetWorkspaceBuildByWorkspaceIDAndBuildNumber" }} --- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one -SELECT - * -FROM - {{ template "workspace_builds_rbac" }} -WHERE - workspace_id = $1 - AND build_number = $2; -{{ end }} {{ define "GetWorkspaceBuildsByWorkspaceID" }} -- name: GetWorkspaceBuildsByWorkspaceID :many @@ -83,22 +92,6 @@ LIMIT NULLIF(@limit_opt :: int, 0); {{ end }} - -{{ define "GetLatestWorkspaceBuildByWorkspaceID" }} --- name: GetLatestWorkspaceBuildByWorkspaceID :one -SELECT - * -FROM - {{ template "workspace_builds_rbac" }} -WHERE - workspace_id = $1 -ORDER BY - build_number desc -LIMIT - 1; -{{ end }} - - {{ define "GetLatestWorkspaceBuildsByWorkspaceIDs" }} -- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many SELECT wb.* From a8ceb7227322e5cc323950a692f10c3bb8defbaf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 10:34:09 -0600 Subject: [PATCH 13/36] Refactor sqlx to it's own package --- coderd/database/modelqueries.go | 13 ++++++----- coderd/database/{ => sqlxqueries}/bindvars.go | 8 +++---- coderd/database/{ => sqlxqueries}/sqlx.go | 23 +++++++++++-------- coderd/database/sqlxqueries/sqlxqueries.go | 9 +++----- coderd/database/sqlxqueries/workspace.gosql | 2 +- 5 files changed, 28 insertions(+), 27 deletions(-) rename coderd/database/{ => sqlxqueries}/bindvars.go (93%) rename coderd/database/{ => sqlxqueries}/sqlx.go (51%) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 10d5843bc993c..6c44f70878426 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -10,6 +10,7 @@ import ( "github.com/lib/pq" "golang.org/x/xerrors" + "github.com/coder/coder/coderd/database/sqlxqueries" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/rbac/regosql" ) @@ -206,12 +207,12 @@ type getWorkspaceBuildParams struct { func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { arg.LimitOpt = 1 - return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuild", arg) + return sqlxqueries.GetContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuild", arg) } func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuild, error) { arg.LimitOpt = -1 - return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuild", arg) + return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuild", arg) } func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { @@ -232,7 +233,7 @@ type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { } func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { - return sqlxGet[WorkspaceBuild](ctx, q, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", arg) + return sqlxqueries.GetContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", arg) } type GetWorkspaceBuildsByWorkspaceIDParams struct { @@ -244,7 +245,7 @@ type GetWorkspaceBuildsByWorkspaceIDParams struct { } func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) { - return sqlxSelect[WorkspaceBuild](ctx, q, "GetWorkspaceBuildsByWorkspaceID", arg) + return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuildsByWorkspaceID", arg) } func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) { @@ -252,11 +253,11 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) { - return sqlxSelect[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) + return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) } func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { - return sqlxSelect[WorkspaceBuild](ctx, q, "GetLatestWorkspaceBuilds", nil) + return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetLatestWorkspaceBuilds", nil) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. diff --git a/coderd/database/bindvars.go b/coderd/database/sqlxqueries/bindvars.go similarity index 93% rename from coderd/database/bindvars.go rename to coderd/database/sqlxqueries/bindvars.go index 42d10330fc27f..6992cf94844b0 100644 --- a/coderd/database/bindvars.go +++ b/coderd/database/sqlxqueries/bindvars.go @@ -1,12 +1,13 @@ -package database +package sqlxqueries import ( - "database/sql/driver" "fmt" "reflect" "regexp" "strings" + "golang.org/x/xerrors" + "github.com/google/uuid" "github.com/jmoiron/sqlx/reflectx" "github.com/lib/pq" @@ -18,7 +19,6 @@ var nameRegex = regexp.MustCompile(`@([a-zA-Z0-9_]+)`) // dbmapper grabs struct 'db' tags. var dbmapper = reflectx.NewMapper("db") -var sqlValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem() // bindNamed is an implementation that improves on the SQLx implementation. This // adjusts the query to use "$#" syntax for arguments instead of "@argument". The @@ -63,7 +63,7 @@ func bindNamed(query string, arg interface{}) (newQuery string, args []interface err = dbmapper.TraversalsByNameFunc(v.Type(), names, func(i int, t []int) error { if len(t) == 0 { - return fmt.Errorf("could not find name %s in %#v", names[i], arg) + return xerrors.Errorf("could not find name %s in %#v", names[i], arg) } val := reflectx.FieldByIndexesReadOnly(v, t) diff --git a/coderd/database/sqlx.go b/coderd/database/sqlxqueries/sqlx.go similarity index 51% rename from coderd/database/sqlx.go rename to coderd/database/sqlxqueries/sqlx.go index 1c3a61492228b..c9c2bc9ac10e1 100644 --- a/coderd/database/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -1,16 +1,19 @@ -package database +package sqlxqueries import ( "context" - "github.com/coder/coder/coderd/database/sqlxqueries" + "github.com/jmoiron/sqlx" + "golang.org/x/xerrors" ) -func sqlxSelect[RT any](ctx context.Context, q *sqlQuerier, queryName string, argument interface{}) ([]RT, error) { +// SelectContext runs the named query on the given database. +// If the query returns no rows, an empty slice is returned. +func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) ([]RT, error) { var empty []RT - query, err := sqlxqueries.Query(queryName, nil) + query, err := query(queryName, nil) if err != nil { return empty, xerrors.Errorf("get query: %w", err) } @@ -20,9 +23,7 @@ func sqlxSelect[RT any](ctx context.Context, q *sqlQuerier, queryName string, ar return empty, xerrors.Errorf("bind named: %w", err) } - // GetContext maps the results of the query to the items slice by struct - // db tags. - err = q.sdb.SelectContext(ctx, &empty, query, args...) + err = sqlx.SelectContext(ctx, q, &empty, query, args...) if err != nil { return empty, xerrors.Errorf("%s: %w", queryName, err) } @@ -30,10 +31,12 @@ func sqlxSelect[RT any](ctx context.Context, q *sqlQuerier, queryName string, ar return empty, nil } -func sqlxGet[RT any](ctx context.Context, q *sqlQuerier, queryName string, argument interface{}) (RT, error) { +// GetContext runs the named query on the given database. +// If the query returns no rows, sql.ErrNoRows is returned. +func GetContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) (RT, error) { var empty RT - query, err := sqlxqueries.Query(queryName, argument) + query, err := query(queryName, argument) if err != nil { return empty, xerrors.Errorf("get query: %w", err) } @@ -45,7 +48,7 @@ func sqlxGet[RT any](ctx context.Context, q *sqlQuerier, queryName string, argum // GetContext maps the results of the query to the items slice by struct // db tags. - err = q.sdb.GetContext(ctx, &empty, query, args...) + err = sqlx.GetContext(ctx, q, &empty, query, args...) if err != nil { return empty, xerrors.Errorf("%s: %w", queryName, err) } diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index 9979195567bb6..da3d11a9f509d 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -31,21 +31,18 @@ func queries() (*template.Template, error) { return cached, cachedError } -func Query(name string, data interface{}) (string, error) { +// query executes the named template with the given data and returns the result. +// The returned query string is SQL. +func query(name string, data interface{}) (string, error) { tpls, err := queries() if err != nil { return "", err } var out bytes.Buffer - // TODO: Should we cache these? err = tpls.ExecuteTemplate(&out, name, data) if err != nil { return "", xerrors.Errorf("execute template %s: %w", name, err) } return out.String(), nil } - -func GetWorkspaceBuildByID() (string, error) { - return Query("GetWorkspaceBuildByID", nil) -} diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql index cf4ab562655bb..a344ce8266ca0 100644 --- a/coderd/database/sqlxqueries/workspace.gosql +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -36,7 +36,7 @@ WHERE END AND CASE WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - created_at > @created_aftet + created_at > @created_after ELSE true END AND CASE From 29d41ab1dc8a8e1434b36386591b4123731a0bfa Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 11:54:43 -0600 Subject: [PATCH 14/36] Use sqlx for workspace build queries --- .../executor/lifecycle_executor_test.go | 5 +- coderd/database/dbgen/generator.go | 2 +- coderd/database/dbgen/take.go | 7 +- coderd/database/dbtestutil/db.go | 6 +- coderd/database/modelqueries.go | 8 +- coderd/database/modelqueries_test.go | 188 ++++++++++++++++++ coderd/database/sqlxqueries/bindvars.go | 4 + coderd/database/sqlxqueries/sqlx.go | 5 + coderd/database/sqlxqueries/sqlxqueries.go | 6 +- coderd/database/sqlxqueries/workspace.gosql | 13 +- 10 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 coderd/database/modelqueries_test.go diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index ac0ee6c6254f8..b9bf30e49eae8 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -2,10 +2,11 @@ package executor_test import ( "context" - "os" "testing" "time" + "github.com/coder/coder/coderd/database/dbtestutil" + "go.uber.org/goleak" "github.com/google/uuid" @@ -493,7 +494,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { } func TestExecutorAutostartMultipleOK(t *testing.T) { - if os.Getenv("DB") == "" { + if !dbtestutil.UsingRealDatabase() { t.Skip(`This test only really works when using a "real" database, similar to a HA setup`) } diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index 29bd75dddb0bd..df5c89265270b 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -231,7 +231,7 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()), Roles: takeFirstSlice(orig.Roles, []string{}), }) - require.NoError(t, err, "insert organization") + require.NoError(t, err, "insert organization member") return mem } diff --git a/coderd/database/dbgen/take.go b/coderd/database/dbgen/take.go index caa4a1b4036db..1f0c2ae6a5f46 100644 --- a/coderd/database/dbgen/take.go +++ b/coderd/database/dbgen/take.go @@ -13,9 +13,14 @@ func takeFirstIP(values ...net.IPNet) net.IPNet { // takeFirstSlice implements takeFirst for []any. // []any is not a comparable type. func takeFirstSlice[T any](values ...[]T) []T { - return takeFirstF(values, func(v []T) bool { + out := takeFirstF(values, func(v []T) bool { return len(v) != 0 }) + // Prevent nil slices + if out == nil { + return []T{} + } + return out } // takeFirstF takes the first value that returns true diff --git a/coderd/database/dbtestutil/db.go b/coderd/database/dbtestutil/db.go index d6c2f106b2619..f048c847663bb 100644 --- a/coderd/database/dbtestutil/db.go +++ b/coderd/database/dbtestutil/db.go @@ -13,12 +13,16 @@ import ( "github.com/coder/coder/coderd/database/postgres" ) +func UsingRealDatabase() bool { + return os.Getenv("DB") != "" +} + func NewDB(t *testing.T) (database.Store, database.Pubsub) { t.Helper() db := dbfake.New() pubsub := database.NewPubsubInMemory() - if os.Getenv("DB") != "" { + if UsingRealDatabase() { connectionURL := os.Getenv("CODER_PG_CONNECTION_URL") if connectionURL == "" { var ( diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6c44f70878426..6e4d9e3e2b5c1 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -192,7 +192,7 @@ type workspaceQuerier interface { type WorkspaceBuild struct { WorkspaceBuildThin OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - WorkspaceOwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` } type getWorkspaceBuildParams struct { @@ -203,6 +203,7 @@ type getWorkspaceBuildParams struct { BuildNumber int32 `db:"build_number"` LimitOpt int32 `db:"limit_opt"` Latest bool `db:"-"` + TestLimit int `db:"-"` } func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { @@ -233,7 +234,10 @@ type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { } func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { - return sqlxqueries.GetContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", arg) + return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{ + BuildNumber: arg.BuildNumber, + WorkspaceID: arg.WorkspaceID, + }) } type GetWorkspaceBuildsByWorkspaceIDParams struct { diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go new file mode 100644 index 0000000000000..3bfd56396f854 --- /dev/null +++ b/coderd/database/modelqueries_test.go @@ -0,0 +1,188 @@ +package database_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbgen" + "github.com/coder/coder/coderd/database/dbtestutil" + "github.com/coder/coder/coderd/rbac" +) + +func TestGetWorkspaceBuild(t *testing.T) { + t.Parallel() + if !dbtestutil.UsingRealDatabase() { + t.Skip("Test only runs against a real database") + } + + db, _ := dbtestutil.NewDB(t) + + // Seed the database with some workspace builds. + var ( + org = dbgen.Organization(t, db, database.Organization{}) + user = dbgen.User(t, db, database.User{ + RBACRoles: []string{rbac.RoleOwner()}, + }) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.ID, + OrganizationID: org.ID, + }) + template = dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + version = dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace = dbgen.Workspace(t, db, database.Workspace{ + OwnerID: user.ID, + OrganizationID: org.ID, + TemplateID: template.ID, + }) + jobs = []database.ProvisionerJob{ + dbgen.ProvisionerJob(t, db, database.ProvisionerJob{ + OrganizationID: org.ID, + InitiatorID: user.ID, + }), + dbgen.ProvisionerJob(t, db, database.ProvisionerJob{ + OrganizationID: org.ID, + InitiatorID: user.ID, + }), + } + builds = []database.WorkspaceBuildThin{ + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + WorkspaceID: workspace.ID, + TemplateVersionID: version.ID, + BuildNumber: 1, + Transition: database.WorkspaceTransitionStart, + InitiatorID: user.ID, + JobID: jobs[0].ID, + Reason: database.BuildReasonInitiator, + CreatedAt: time.Now(), + }), + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + WorkspaceID: workspace.ID, + TemplateVersionID: version.ID, + BuildNumber: 2, + Transition: database.WorkspaceTransitionStart, + InitiatorID: user.ID, + JobID: jobs[1].ID, + Reason: database.BuildReasonInitiator, + CreatedAt: time.Now().Add(time.Hour), + }), + } + ctx = context.Background() + ) + + t.Run("GetWorkspaceBuildByID", func(t *testing.T) { + t.Parallel() + for _, expected := range builds { + build, err := db.GetWorkspaceBuildByID(ctx, expected.ID) + if err != nil { + t.Fatal(err) + } + require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + } + }) + + t.Run("GetWorkspaceBuildByJobID", func(t *testing.T) { + t.Parallel() + for i, job := range jobs { + build, err := db.GetWorkspaceBuildByJobID(ctx, job.ID) + if err != nil { + t.Fatal(err) + } + expected := builds[i] + require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + } + }) + + t.Run("GetWorkspaceBuildsCreatedAfter", func(t *testing.T) { + t.Parallel() + builds, err := db.GetWorkspaceBuildsCreatedAfter(ctx, jobs[0].CreatedAt) + if err != nil { + t.Fatal(err) + } + expected := builds[1] + require.Len(t, builds, 1, "should only be one build") + require.Equal(t, expected, builds[0], "builds should be equal") + }) + + t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) { + t.Parallel() + for _, expected := range builds { + build, err := db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ + BuildNumber: expected.BuildNumber, + WorkspaceID: expected.WorkspaceID, + }) + if err != nil { + t.Fatal(err) + } + require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + } + }) + + t.Run("GetWorkspaceBuildsByWorkspaceID", func(t *testing.T) { + t.Parallel() + found, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{ + WorkspaceID: workspace.ID, + Since: builds[0].CreatedAt.Add(-1 * time.Hour), + }) + if err != nil { + t.Fatal(err) + } + require.Len(t, found, 2, "should be two builds") + exp := []database.WorkspaceBuildThin{ + builds[1], + builds[0], + } + require.Equal(t, exp, toThins(found), "builds should be equal") + }) + + t.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", func(t *testing.T) { + t.Parallel() + found, err := db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, []uuid.UUID{workspace.ID}) + if err != nil { + t.Fatal(err) + } + require.Len(t, found, 2, "should be two builds") + require.Equal(t, builds, found, "builds should be equal") + }) + + t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) { + t.Parallel() + found, err := db.GetLatestWorkspaceBuilds(ctx) + if err != nil { + t.Fatal(err) + } + require.Len(t, found, 1, "should be only 1 build") + require.Equal(t, builds[1], toThins(found), "builds should be equal") + }) + + t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) { + t.Parallel() + found, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + t.Fatal(err) + } + require.Equal(t, builds[1], found, "builds should be equal") + }) +} + +func toThins(builds []database.WorkspaceBuild) []database.WorkspaceBuildThin { + thins := make([]database.WorkspaceBuildThin, len(builds)) + for i, build := range builds { + thins[i] = build.WorkspaceBuildThin + } + return thins +} diff --git a/coderd/database/sqlxqueries/bindvars.go b/coderd/database/sqlxqueries/bindvars.go index 6992cf94844b0..fdfd25921b192 100644 --- a/coderd/database/sqlxqueries/bindvars.go +++ b/coderd/database/sqlxqueries/bindvars.go @@ -39,6 +39,10 @@ func bindNamed(query string, arg interface{}) (newQuery string, args []interface // Replace all names with the correct index for i, name := range names { rpl := fmt.Sprintf("$%d", i+1) + if strings.Contains(query, rpl) { + return "", nil, + xerrors.Errorf("query contains both named params %q, and unnamed %q: choose one", name, rpl) + } query = strings.ReplaceAll(query, name, rpl) // Remove the "@" prefix to match to the "db" struct tag. names[i] = strings.TrimPrefix(name, "@") diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index c9c2bc9ac10e1..81f43214ebae2 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -18,6 +18,11 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName return empty, xerrors.Errorf("get query: %w", err) } + // No argument was given, use an empty struct. + if argument == nil { + argument = struct{}{} + } + query, args, err := bindNamed(query, argument) if err != nil { return empty, xerrors.Errorf("bind named: %w", err) diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index da3d11a9f509d..e8ea8e4a61852 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -21,9 +21,13 @@ var ( func queries() (*template.Template, error) { once.Do(func() { - tpls, err := template.ParseFS(sqlxQueries, "*.gosql") + tpls, err := template.New(""). + Funcs(template.FuncMap{ + "int32": func(i int) int32 { return int32(i) }, + }).ParseFS(sqlxQueries, "*.gosql") if err != nil { cachedError = xerrors.Errorf("developer error parse sqlx queries: %w", err) + return } cached = tpls }) diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql index a344ce8266ca0..d35f2993f356e 100644 --- a/coderd/database/sqlxqueries/workspace.gosql +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -8,16 +8,17 @@ FROM workspace_builds INNER JOIN workspaces ON workspace_builds.workspace_id = workspaces.id -); -{{ end }} +) +{{ end }}; {{ define "GetWorkspaceBuild" }} +-- {{ . }}}} -- name: GetWorkspaceBuild :one SELECT * FROM - {{ template "workspace_builds_rbac" }} + {{ template "workspace_builds_rbac" }} workspace_builds WHERE CASE WHEN @build_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN @@ -53,7 +54,7 @@ WHERE ORDER BY build_number desc {{ end }} -LIMIT @limit_opt; +{{ if gt .TestLimit 0 }} LIMIT @limit_opt {{ end }}; {{ end }} @@ -62,9 +63,9 @@ LIMIT @limit_opt; SELECT * FROM - {{ template "workspace_builds_rbac" }} + {{ template "workspace_builds_rbac" }} workspace_builds WHERE - workspace_builds.workspace_id = $1 + workspace_builds.workspace_id = @workspace_id AND workspace_builds.created_at > @since AND CASE -- This allows using the last element on a page as effectively a cursor. From b46813fee41cf9066d103196d96512c0f832a5f0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 11:56:18 -0600 Subject: [PATCH 15/36] Pass arguments --- coderd/database/sqlxqueries/sqlx.go | 2 +- coderd/database/sqlxqueries/workspace.gosql | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index 81f43214ebae2..7dbd552b1b2aa 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -13,7 +13,7 @@ import ( func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) ([]RT, error) { var empty []RT - query, err := query(queryName, nil) + query, err := query(queryName, argument) if err != nil { return empty, xerrors.Errorf("get query: %w", err) } diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql index d35f2993f356e..e93093f5a63ba 100644 --- a/coderd/database/sqlxqueries/workspace.gosql +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -13,7 +13,6 @@ INNER JOIN {{ define "GetWorkspaceBuild" }} --- {{ . }}}} -- name: GetWorkspaceBuild :one SELECT * @@ -54,7 +53,7 @@ WHERE ORDER BY build_number desc {{ end }} -{{ if gt .TestLimit 0 }} LIMIT @limit_opt {{ end }}; +LIMIT @limit_opt; {{ end }} From 8dc8c42dbcfbe4fa3548ea24966e0223d7355b5b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 11:57:28 -0600 Subject: [PATCH 16/36] Handle nonzero limit --- coderd/database/modelqueries.go | 3 +-- coderd/database/sqlxqueries/workspace.gosql | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6e4d9e3e2b5c1..99cce7a2e0696 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -202,8 +202,7 @@ type getWorkspaceBuildParams struct { WorkspaceID uuid.UUID `db:"workspace_id"` BuildNumber int32 `db:"build_number"` LimitOpt int32 `db:"limit_opt"` - Latest bool `db:"-"` - TestLimit int `db:"-"` + Latest bool `db:"-"` statu } func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql index e93093f5a63ba..acf274b8a5abd 100644 --- a/coderd/database/sqlxqueries/workspace.gosql +++ b/coderd/database/sqlxqueries/workspace.gosql @@ -53,7 +53,8 @@ WHERE ORDER BY build_number desc {{ end }} -LIMIT @limit_opt; +{{ if gt .LimitOpt 0 }} LIMIT @limit_opt {{ end }} +; {{ end }} From e907a0f49a23b22b768060d10d582c41dae5f42d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 14:39:23 -0600 Subject: [PATCH 17/36] Fix sqlx unit test --- coderd/database/modelqueries.go | 2 +- coderd/database/modelqueries_test.go | 24 ++++++++++++------------ coderd/database/sqlxqueries/sqlx.go | 15 ++++++++++----- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 99cce7a2e0696..0ec4c5cfed3c4 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -202,7 +202,7 @@ type getWorkspaceBuildParams struct { WorkspaceID uuid.UUID `db:"workspace_id"` BuildNumber int32 `db:"build_number"` LimitOpt int32 `db:"limit_opt"` - Latest bool `db:"-"` statu + Latest bool `db:"-"` } func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go index 3bfd56396f854..8da1cb8880678 100644 --- a/coderd/database/modelqueries_test.go +++ b/coderd/database/modelqueries_test.go @@ -81,6 +81,10 @@ func TestGetWorkspaceBuild(t *testing.T) { CreatedAt: time.Now().Add(time.Hour), }), } + orderBuilds = []database.WorkspaceBuildThin{ + builds[1], + builds[0], + } ctx = context.Background() ) @@ -109,13 +113,13 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Run("GetWorkspaceBuildsCreatedAfter", func(t *testing.T) { t.Parallel() - builds, err := db.GetWorkspaceBuildsCreatedAfter(ctx, jobs[0].CreatedAt) + found, err := db.GetWorkspaceBuildsCreatedAfter(ctx, builds[0].CreatedAt.Add(time.Second)) if err != nil { t.Fatal(err) } expected := builds[1] - require.Len(t, builds, 1, "should only be one build") - require.Equal(t, expected, builds[0], "builds should be equal") + require.Len(t, found, 1, "should only be one build") + require.Equal(t, expected, found[0].ToThin(), "builds should be equal") }) t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) { @@ -142,11 +146,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 2, "should be two builds") - exp := []database.WorkspaceBuildThin{ - builds[1], - builds[0], - } - require.Equal(t, exp, toThins(found), "builds should be equal") + require.Equal(t, orderBuilds, toThins(found), "builds should be equal") }) t.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", func(t *testing.T) { @@ -155,8 +155,8 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Len(t, found, 2, "should be two builds") - require.Equal(t, builds, found, "builds should be equal") + require.Len(t, found, 1, "should be only one build") + require.Equal(t, builds[1], found[0].ToThin(), "builds should be equal") }) t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) { @@ -166,7 +166,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only 1 build") - require.Equal(t, builds[1], toThins(found), "builds should be equal") + require.Equal(t, builds[1], found[0].ToThin(), "builds should be equal") }) t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) { @@ -175,7 +175,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, builds[1], found, "builds should be equal") + require.Equal(t, builds[1], found.ToThin(), "builds should be equal") }) } diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index 7dbd552b1b2aa..766ce06db9e44 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -13,16 +13,16 @@ import ( func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) ([]RT, error) { var empty []RT - query, err := query(queryName, argument) - if err != nil { - return empty, xerrors.Errorf("get query: %w", err) - } - // No argument was given, use an empty struct. if argument == nil { argument = struct{}{} } + query, err := query(queryName, argument) + if err != nil { + return empty, xerrors.Errorf("get query: %w", err) + } + query, args, err := bindNamed(query, argument) if err != nil { return empty, xerrors.Errorf("bind named: %w", err) @@ -41,6 +41,11 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName func GetContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) (RT, error) { var empty RT + // No argument was given, use an empty struct. + if argument == nil { + argument = struct{}{} + } + query, err := query(queryName, argument) if err != nil { return empty, xerrors.Errorf("get query: %w", err) From fe135499f4a0b53de2f75bbbb306810340cc6062 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 14:58:13 -0600 Subject: [PATCH 18/36] Fix unit tests --- coderd/database/modelqueries_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go index 2b459ad6ba339..91d5807d0b610 100644 --- a/coderd/database/modelqueries_test.go +++ b/coderd/database/modelqueries_test.go @@ -24,6 +24,7 @@ func TestGetWorkspaceBuild(t *testing.T) { // Seed the database with some workspace builds. var ( + now = database.Now() org = dbgen.Organization(t, db, database.Organization{}) user = dbgen.User(t, db, database.User{ RBACRoles: []string{rbac.RoleOwner()}, @@ -68,7 +69,7 @@ func TestGetWorkspaceBuild(t *testing.T) { InitiatorID: user.ID, JobID: jobs[0].ID, Reason: database.BuildReasonInitiator, - CreatedAt: time.Now(), + CreatedAt: now, }), dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ WorkspaceID: workspace.ID, @@ -78,7 +79,7 @@ func TestGetWorkspaceBuild(t *testing.T) { InitiatorID: user.ID, JobID: jobs[1].ID, Reason: database.BuildReasonInitiator, - CreatedAt: time.Now().Add(time.Hour), + CreatedAt: now.Add(time.Hour), }), } orderBuilds = []database.WorkspaceBuildThin{ @@ -119,7 +120,7 @@ func TestGetWorkspaceBuild(t *testing.T) { } expected := builds[1] require.Len(t, found, 1, "should only be one build") - require.Equal(t, expected, found[0].ToThin(), "builds should be equal") + require.Equal(t, expected.ID, found[0].ID, "builds should be equal") }) t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) { @@ -156,7 +157,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only one build") - require.Equal(t, builds[1], found[0].ToThin(), "builds should be equal") + require.Equal(t, builds[1].ID, found[0].ID, "builds should be equal") }) t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) { @@ -166,7 +167,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only 1 build") - require.Equal(t, builds[1], found[0].ToThin(), "builds should be equal") + require.Equal(t, builds[1].ID, found[0].ID, "builds should be equal") }) t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) { @@ -175,7 +176,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, builds[1], found.ToThin(), "builds should be equal") + require.Equal(t, builds[1].ID, found.ID, "builds should be equal") }) } @@ -186,4 +187,3 @@ func toThins(builds []database.WorkspaceBuild) []database.WorkspaceBuildThin { } return thins } - From e91476d8d0846976df5e454365b56949a462eb8a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Mar 2023 15:11:05 -0600 Subject: [PATCH 19/36] Fix unit tests --- coderd/database/dbfake/databasefake.go | 1 + coderd/database/dbgen/generator.go | 3 +- coderd/database/dbgen/generator_test.go | 2 +- coderd/database/modelmethods.go | 38 ++----------------------- coderd/database/modelqueries_test.go | 8 +++--- 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index b43477ad9952e..140d4637bffa9 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -3196,6 +3196,7 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser JobID: arg.JobID, ProvisionerState: arg.ProvisionerState, Deadline: arg.Deadline, + MaxDeadline: arg.MaxDeadline, Reason: arg.Reason, } q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild) diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index f5601c2275753..f21c74cab8541 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -160,7 +160,7 @@ func WorkspaceBuild[B AnyWorkspaceBuild](t testing.TB, db database.Store, orig B var thin database.WorkspaceBuildThin switch v := any(orig).(type) { case database.WorkspaceBuild: - thin = v.ToThin() + thin = v.WorkspaceBuildThin case database.WorkspaceBuildThin: thin = v default: @@ -178,6 +178,7 @@ func WorkspaceBuild[B AnyWorkspaceBuild](t testing.TB, db database.Store, orig B JobID: takeFirst(thin.JobID, uuid.New()), ProvisionerState: takeFirstSlice(thin.ProvisionerState, []byte{}), Deadline: takeFirst(thin.Deadline, database.Now().Add(time.Hour)), + MaxDeadline: takeFirst(thin.MaxDeadline, database.Now().Add(time.Hour*24*7)), Reason: takeFirst(thin.Reason, database.BuildReasonInitiator), }) require.NoError(t, err, "insert workspace build") diff --git a/coderd/database/dbgen/generator_test.go b/coderd/database/dbgen/generator_test.go index 6082d1d33addb..f85a5cfb821b3 100644 --- a/coderd/database/dbgen/generator_test.go +++ b/coderd/database/dbgen/generator_test.go @@ -167,7 +167,7 @@ func TestGenerator(t *testing.T) { t.Parallel() db := dbfake.New() exp := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{}) - require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).ToThin()) + require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).WorkspaceBuildThin) }) t.Run("User", func(t *testing.T) { diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index a793787651dd0..5702c61476f36 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -191,47 +191,15 @@ func (l License) RBACObject() rbac.Object { return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10)) } -func (b WorkspaceBuild) ToThin() WorkspaceBuildThin { - return WorkspaceBuildThin{ - ID: b.ID, - CreatedAt: b.CreatedAt, - UpdatedAt: b.UpdatedAt, - WorkspaceID: b.WorkspaceID, - TemplateVersionID: b.TemplateVersionID, - BuildNumber: b.BuildNumber, - Transition: b.Transition, - InitiatorID: b.InitiatorID, - ProvisionerState: b.ProvisionerState, - JobID: b.JobID, - Deadline: b.Deadline, - Reason: b.Reason, - DailyCost: b.DailyCost, - } -} - func (b WorkspaceBuildThin) WithWorkspace(workspace Workspace) WorkspaceBuild { return b.Expand(workspace.OrganizationID, workspace.OwnerID) } func (b WorkspaceBuildThin) Expand(orgID, ownerID uuid.UUID) WorkspaceBuild { return WorkspaceBuild{ - WorkspaceBuildThin: WorkspaceBuildThin{ - ID: b.ID, - CreatedAt: b.CreatedAt, - UpdatedAt: b.UpdatedAt, - WorkspaceID: b.WorkspaceID, - TemplateVersionID: b.TemplateVersionID, - BuildNumber: b.BuildNumber, - Transition: b.Transition, - InitiatorID: b.InitiatorID, - ProvisionerState: b.ProvisionerState, - JobID: b.JobID, - Deadline: b.Deadline, - Reason: b.Reason, - DailyCost: b.DailyCost, - }, - OrganizationID: orgID, - WorkspaceOwnerID: ownerID, + WorkspaceBuildThin: b, + OrganizationID: orgID, + WorkspaceOwnerID: ownerID, } } diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go index 91d5807d0b610..09c2f67ed80df 100644 --- a/coderd/database/modelqueries_test.go +++ b/coderd/database/modelqueries_test.go @@ -120,7 +120,7 @@ func TestGetWorkspaceBuild(t *testing.T) { } expected := builds[1] require.Len(t, found, 1, "should only be one build") - require.Equal(t, expected.ID, found[0].ID, "builds should be equal") + require.Equal(t, expected, found[0], "builds should be equal") }) t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) { @@ -157,7 +157,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only one build") - require.Equal(t, builds[1].ID, found[0].ID, "builds should be equal") + require.Equal(t, builds[1], found[0], "builds should be equal") }) t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) { @@ -167,7 +167,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only 1 build") - require.Equal(t, builds[1].ID, found[0].ID, "builds should be equal") + require.Equal(t, builds[1], found[0].WorkspaceBuildThin, "builds should be equal") }) t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) { @@ -176,7 +176,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, builds[1].ID, found.ID, "builds should be equal") + require.Equal(t, builds[1], found, "builds should be equal") }) } From 03547a343dcbb679a22f5e5e27fa363a57550528 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 08:59:34 -0600 Subject: [PATCH 20/36] cleanup --- .../executor/lifecycle_executor_test.go | 6 +-- coderd/database/dbauthz/querier.go | 2 +- coderd/database/modelqueries.go | 4 +- coderd/database/sqlxqueries/sqlx.go | 37 ++++++++++--------- coderd/database/sqlxqueries/sqlxqueries.go | 6 ++- .../provisionerdserver/provisionerdserver.go | 4 +- coderd/telemetry/telemetry_test.go | 2 +- 7 files changed, 32 insertions(+), 29 deletions(-) diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 52e1ce7b3b6e6..7ffbecbd4c23c 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -5,15 +5,13 @@ import ( "testing" "time" - "github.com/coder/coder/coderd/database/dbtestutil" - - "go.uber.org/goleak" - "github.com/google/uuid" + "go.uber.org/goleak" "github.com/coder/coder/coderd/autobuild/executor" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbtestutil" "github.com/coder/coder/coderd/schedule" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index f26bf724da29d..21112201a8982 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1279,7 +1279,7 @@ func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg datab return nil, err } if len(builds) == 0 { - return nil, sql.ErrNoRows + return []database.WorkspaceBuild{}, nil } // All builds come from the same workspace, so we only need to check the first one. err = q.authorizeContext(ctx, rbac.ActionRead, builds[0]) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index af98bfe409f44..3d5cdcac6b908 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -229,8 +229,8 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after t } type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { - BuildNumber int32 `db:"build_number"` - WorkspaceID uuid.UUID `db:"workspace_id""` + BuildNumber int32 + WorkspaceID uuid.UUID } func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index 766ce06db9e44..dd92f33acbb63 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -8,11 +8,10 @@ import ( "golang.org/x/xerrors" ) -// SelectContext runs the named query on the given database. -// If the query returns no rows, an empty slice is returned. -func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) ([]RT, error) { - var empty []RT - +// constructQuery will return a SQL query by the given template name. +// It will also return the arguments in order for the query based on the input +// argument. +func constructQuery(queryName string, argument any) (string, []any, error) { // No argument was given, use an empty struct. if argument == nil { argument = struct{}{} @@ -20,12 +19,24 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName query, err := query(queryName, argument) if err != nil { - return empty, xerrors.Errorf("get query: %w", err) + return "", nil, xerrors.Errorf("get query: %w", err) } query, args, err := bindNamed(query, argument) if err != nil { - return empty, xerrors.Errorf("bind named: %w", err) + return "", nil, xerrors.Errorf("bind named: %w", err) + } + return query, args, nil +} + +// SelectContext runs the named query on the given database. +// If the query returns no rows, an empty slice is returned. +func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument any) ([]RT, error) { + var empty []RT + + query, args, err := constructQuery(queryName, argument) + if err != nil { + return empty, xerrors.Errorf("get query: %w", err) } err = sqlx.SelectContext(ctx, q, &empty, query, args...) @@ -41,21 +52,11 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName func GetContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) (RT, error) { var empty RT - // No argument was given, use an empty struct. - if argument == nil { - argument = struct{}{} - } - - query, err := query(queryName, argument) + query, args, err := constructQuery(queryName, argument) if err != nil { return empty, xerrors.Errorf("get query: %w", err) } - query, args, err := bindNamed(query, argument) - if err != nil { - return empty, xerrors.Errorf("bind named: %w", err) - } - // GetContext maps the results of the query to the items slice by struct // db tags. err = sqlx.GetContext(ctx, q, &empty, query, args...) diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index e8ea8e4a61852..5c90bdd242275 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -19,7 +19,9 @@ var ( cachedError error ) -func queries() (*template.Template, error) { +// loadQueries parses the embedded queries and returns the template. +// Results are cached. +func loadQueries() (*template.Template, error) { once.Do(func() { tpls, err := template.New(""). Funcs(template.FuncMap{ @@ -38,7 +40,7 @@ func queries() (*template.Template, error) { // query executes the named template with the given data and returns the result. // The returned query string is SQL. func query(name string, data interface{}) (string, error) { - tpls, err := queries() + tpls, err := loadQueries() if err != nil { return "", err } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 14b5797eeebfd..ee20214ec2440 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -671,7 +671,7 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p return xerrors.Errorf("get workspace build: %w", err) } - build, err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ + thinBuild, err := db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ ID: input.WorkspaceBuildID, UpdatedAt: database.Now(), ProvisionerState: jobType.WorkspaceBuild.State, @@ -681,6 +681,8 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p if err != nil { return xerrors.Errorf("update workspace build state: %w", err) } + // Keep the same owner args as the original build. + build = thinBuild.Expand(workspaceBuild.OrganizationID, workspaceBuild.WorkspaceOwnerID) return nil }, nil) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index cdd1d42f8b67e..62f8b76e5a29b 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -60,7 +60,7 @@ func TestTelemetry(t *testing.T) { Health: database.WorkspaceAppHealthDisabled, }) _ = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonAutostart, }) From 449596231874995caf38b88666442a74417603a3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:07:08 -0600 Subject: [PATCH 21/36] Test fixes --- coderd/httpmw/workspaceagent_test.go | 2 +- coderd/httpmw/workspaceagentparam_test.go | 2 +- coderd/httpmw/workspacebuildparam_test.go | 2 +- coderd/httpmw/workspaceparam_test.go | 2 +- coderd/httpmw/workspaceresourceparam_test.go | 2 +- coderd/provisionerdserver/provisionerdserver_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index bcf6ee2f7e0e2..e15a6858b8c1c 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -54,7 +54,7 @@ func TestWorkspaceAgent(t *testing.T) { resource = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: job.ID, }) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ WorkspaceID: workspace.ID, JobID: job.ID, }) diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index f6a4c3fe1eaa7..f8863eb718142 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { workspace = dbgen.Workspace(t, db, database.Workspace{ OwnerID: user.ID, }) - build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ WorkspaceID: workspace.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index ac495537a1de3..5300decdc0516 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -91,7 +91,7 @@ func TestWorkspaceBuildParam(t *testing.T) { }) r, workspace := setupAuthentication(db) - workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, WorkspaceID: workspace.ID, diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index dd19abe8ebfcc..660bcb4819ade 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -350,7 +350,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h OwnerID: user.ID, Name: cfg.WorkspaceName, }) - build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ WorkspaceID: workspace.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/httpmw/workspaceresourceparam_test.go b/coderd/httpmw/workspaceresourceparam_test.go index bee32b0d304c3..6070eb0f7099c 100644 --- a/coderd/httpmw/workspaceresourceparam_test.go +++ b/coderd/httpmw/workspaceresourceparam_test.go @@ -27,7 +27,7 @@ func TestWorkspaceResourceParam(t *testing.T) { StorageMethod: database.ProvisionerStorageMethodFile, }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ JobID: job.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 1449655d228b6..bae94b15565f1 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -147,7 +147,7 @@ func TestAcquireJob(t *testing.T) { TemplateID: template.ID, OwnerID: user.ID, }) - build := dbgen.WorkspaceBuild(t, srv.Database, database.WorkspaceBuild{ + build := dbgen.WorkspaceBuild(t, srv.Database, database.WorkspaceBuildThin{ WorkspaceID: workspace.ID, BuildNumber: 1, JobID: uuid.New(), From fd867d0ac4e975858185c9b56a87de6464cc70c8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:13:35 -0600 Subject: [PATCH 22/36] remove unused function --- coderd/database/dbfake/databasefake.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 140d4637bffa9..32093453ba934 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -4551,15 +4551,6 @@ func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, return sql.ErrNoRows } -// expandWorkspaceThins must be called from a locked context. -func (q *fakeQuerier) expandWorkspaceThins(thins []database.WorkspaceBuildThin) []database.WorkspaceBuild { - cpy := make([]database.WorkspaceBuild, 0, len(thins)) - for _, thin := range thins { - cpy = append(cpy, q.expandWorkspaceThin(thin)) - } - return cpy -} - // expandWorkspaceThin must be called from a locked context. func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuildThin) database.WorkspaceBuild { w, _ := q.GetWorkspaceByID(context.Background(), thin.WorkspaceID) From 3a116ee037b37b4695c06b0838d7716edc735c42 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:18:44 -0600 Subject: [PATCH 23/36] Rename back to WorkspaceBuild --- coderd/audit/diff.go | 2 +- coderd/audit/request.go | 6 +- .../autobuild/executor/lifecycle_executor.go | 4 +- coderd/database/dbauthz/querier.go | 26 +++---- coderd/database/dbauthz/querier_test.go | 64 ++++++++--------- coderd/database/dbauthz/system.go | 6 +- coderd/database/dbauthz/system_test.go | 8 +-- coderd/database/dbfake/databasefake.go | 70 +++++++++---------- coderd/database/dbgen/generator.go | 10 +-- coderd/database/dbgen/generator_test.go | 4 +- coderd/database/modelmethods.go | 14 ++-- coderd/database/modelqueries.go | 50 ++++++------- coderd/database/modelqueries_test.go | 22 +++--- coderd/database/models.go | 2 +- coderd/database/querier.go | 6 +- coderd/database/queries.sql.go | 12 ++-- coderd/database/sqlc.yaml | 1 - coderd/httpmw/workspaceagent_test.go | 2 +- coderd/httpmw/workspaceagentparam_test.go | 2 +- coderd/httpmw/workspacebuildparam.go | 4 +- coderd/httpmw/workspacebuildparam_test.go | 2 +- coderd/httpmw/workspaceparam_test.go | 2 +- coderd/httpmw/workspaceresourceparam_test.go | 2 +- .../provisionerdserver/provisionerdserver.go | 10 +-- .../provisionerdserver_test.go | 2 +- coderd/telemetry/telemetry.go | 2 +- coderd/telemetry/telemetry_test.go | 2 +- coderd/workspacebuilds.go | 14 ++-- coderd/workspaces.go | 2 +- enterprise/audit/table.go | 2 +- 30 files changed, 177 insertions(+), 178 deletions(-) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 1cc6702d1b06c..26134576cdf1f 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -14,7 +14,7 @@ type Auditable interface { database.User | database.Workspace | database.GitSSHKey | - database.WorkspaceBuild | + database.WorkspaceBuildRBAC | database.AuditableGroup | database.License } diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 8aba0ffc30adf..7e7690d44b5d7 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -62,7 +62,7 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Username case database.Workspace: return typed.Name - case database.WorkspaceBuild: + case database.WorkspaceBuildRBAC: // this isn't used return "" case database.GitSSHKey: @@ -89,7 +89,7 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.Workspace: return typed.ID - case database.WorkspaceBuild: + case database.WorkspaceBuildRBAC: return typed.ID case database.GitSSHKey: return typed.UserID @@ -114,7 +114,7 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeUser case database.Workspace: return database.ResourceTypeWorkspace - case database.WorkspaceBuild: + case database.WorkspaceBuildRBAC: return database.ResourceTypeWorkspaceBuild case database.GitSSHKey: return database.ResourceTypeGitSshKey diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go index f6b4d0db12d87..c818dcf6d4dd3 100644 --- a/coderd/autobuild/executor/lifecycle_executor.go +++ b/coderd/autobuild/executor/lifecycle_executor.go @@ -204,7 +204,7 @@ func isEligibleForAutoStartStop(ws database.Workspace) bool { func getNextTransition( ws database.Workspace, - priorHistory database.WorkspaceBuild, + priorHistory database.WorkspaceBuildRBAC, priorJob database.ProvisionerJob, ) ( validTransition database.WorkspaceTransition, @@ -239,7 +239,7 @@ func getNextTransition( // TODO(cian): this function duplicates most of api.postWorkspaceBuilds. Refactor. // See: https://github.com/coder/coder/issues/1401 -func build(ctx context.Context, store database.Store, workspace database.Workspace, trans database.WorkspaceTransition, priorHistory database.WorkspaceBuild, priorJob database.ProvisionerJob) error { +func build(ctx context.Context, store database.Store, workspace database.Workspace, trans database.WorkspaceTransition, priorHistory database.WorkspaceBuildRBAC, priorJob database.ProvisionerJob) error { template, err := store.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { return xerrors.Errorf("get workspace template: %w", err) diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go index 21112201a8982..35e2685c8623a 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1167,11 +1167,11 @@ func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesP return q.db.GetAuthorizedWorkspaces(ctx, arg, prep) } -func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) { return fetch(q.log, q.auth, q.db.GetLatestWorkspaceBuildByWorkspaceID)(ctx, workspaceID) } -func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { +func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuildRBAC, error) { return fetchWithPostFilter(q.auth, q.db.GetLatestWorkspaceBuildsByWorkspaceIDs)(ctx, ids) } @@ -1250,15 +1250,15 @@ func (q *querier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UU return q.db.GetWorkspaceAppsByAgentID(ctx, agentID) } -func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *querier) GetWorkspaceBuildByID(ctx context.Context, buildID uuid.UUID) (database.WorkspaceBuildRBAC, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByID)(ctx, buildID) } -func (q *querier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *querier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuildRBAC, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByJobID)(ctx, jobID) } -func (q *querier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { +func (q *querier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuildRBAC, error) { return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber)(ctx, arg) } @@ -1273,13 +1273,13 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID) } -func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { +func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuildRBAC, error) { builds, err := q.db.GetWorkspaceBuildsByWorkspaceID(ctx, arg) if err != nil { return nil, err } if len(builds) == 0 { - return []database.WorkspaceBuild{}, nil + return []database.WorkspaceBuildRBAC{}, nil } // All builds come from the same workspace, so we only need to check the first one. err = q.authorizeContext(ctx, rbac.ActionRead, builds[0]) @@ -1362,10 +1362,10 @@ func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorksp return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg) } -func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuildThin, error) { +func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } var action rbac.Action = rbac.ActionUpdate @@ -1374,7 +1374,7 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW } if err = q.authorizeContext(ctx, action, w); err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } return q.db.InsertWorkspaceBuild(ctx, arg) @@ -1445,15 +1445,15 @@ func (q *querier) UpdateWorkspaceAutostart(ctx context.Context, arg database.Upd return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceAutostart)(ctx, arg) } -func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuildThin, error) { +func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { build, err := q.db.GetWorkspaceBuildByID(ctx, arg.ID) if err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } err = q.authorizeContext(ctx, rbac.ActionUpdate, build) if err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } return q.db.UpdateWorkspaceBuildByID(ctx, arg) diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index dba2176db8217..de11bb298e3aa 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -200,7 +200,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) check.Args(j.ID).Asserts(w, rbac.ActionRead).Returns(j) })) s.Run("TemplateVersion/GetProvisionerJobByID", s.Subtest(func(db database.Store, check *expects) { @@ -233,7 +233,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.UpdateProvisionerJobWithCancelByIDParams{ID: j.ID}).Asserts(w, rbac.ActionUpdate).Returns() })) s.Run("BuildFalseCancel/UpdateProvisionerJobWithCancelByID", s.Subtest(func(db database.Store, check *expects) { @@ -242,7 +242,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.UpdateProvisionerJobWithCancelByIDParams{ID: j.ID}).Asserts(w, rbac.ActionUpdate).Returns() })) s.Run("TemplateVersion/UpdateProvisionerJobWithCancelByID", s.Subtest(func(db database.Store, check *expects) { @@ -292,7 +292,7 @@ func (s *MethodTestSuite) TestProvsionerJob() { j := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{JobID: j.ID, WorkspaceID: w.ID}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID}) check.Args(database.GetProvisionerLogsByIDBetweenParams{ JobID: j.ID, }).Asserts(w, rbac.ActionRead).Returns([]database.ProvisionerJobLog{}) @@ -955,31 +955,31 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b.WithWorkspace(ws)) })) s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args([]uuid.UUID{ws.ID}).Asserts(ws, rbac.ActionRead).Returns(slice.New(b)) })) s.Run("GetWorkspaceAgentByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(agt) })) s.Run("GetWorkspaceAgentByInstanceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.AuthInstanceID.String).Asserts(ws, rbac.ActionRead).Returns(agt) })) s.Run("GetWorkspaceAgentsByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args([]uuid.UUID{res.ID}).Asserts( /*ws, rbac.ActionRead*/ ). @@ -987,7 +987,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentLifecycleStateByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentLifecycleStateByIDParams{ @@ -997,7 +997,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentStartupByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentStartupByIDParams{ @@ -1006,7 +1006,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppByAgentIDAndSlug", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1018,7 +1018,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1028,13 +1028,13 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { aWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: aWs.ID, JobID: uuid.New()}).WithWorkspace(aWs) + aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: aWs.ID, JobID: uuid.New()}).WithWorkspace(aWs) aRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: aBuild.JobID}) aAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: aRes.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: aAgt.ID}) bWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) + bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) bRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: bBuild.JobID}) bAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: bRes.ID}) b := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: bAgt.ID}) @@ -1045,17 +1045,17 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.ID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) check.Args(build.JobID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ WorkspaceID: ws.ID, BuildNumber: build.BuildNumber, @@ -1063,20 +1063,20 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) check.Args(build.ID).Asserts(ws, rbac.ActionRead). Returns([]database.WorkspaceBuildParameter{}) })) s.Run("GetWorkspaceBuildsByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 1}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 1}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) check.Args(database.GetWorkspaceBuildsByWorkspaceIDParams{WorkspaceID: ws.ID}).Asserts(ws, rbac.ActionRead) // ordering })) s.Run("GetWorkspaceByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.ID).Asserts(ws, rbac.ActionRead).Returns(ws) @@ -1091,14 +1091,14 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceResourceByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) check.Args(res.ID).Asserts(ws, rbac.ActionRead).Returns(res) })) s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) a := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) b := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) @@ -1107,7 +1107,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("Build/GetWorkspaceResourcesByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) job := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args(job.ID).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceResource{}) })) @@ -1123,7 +1123,7 @@ func (s *MethodTestSuite) TestWorkspace() { tJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: v.JobID, Type: database.ProvisionerJobTypeTemplateVersionImport}) ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) wJob := dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) check.Args([]uuid.UUID{tJob.ID, wJob.ID}). Asserts( /*v.RBACObject(tpl), rbac.ActionRead, ws, rbac.ActionRead*/ ). @@ -1156,7 +1156,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) { w := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: w.ID}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: w.ID}) check.Args(database.InsertWorkspaceBuildParametersParams{ WorkspaceBuildID: b.ID, Name: []string{"foo", "bar"}, @@ -1173,7 +1173,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAgentConnectionByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.UpdateWorkspaceAgentConnectionByIDParams{ @@ -1188,7 +1188,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceAppHealthByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -1205,7 +1205,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("UpdateWorkspaceBuildByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) check.Args(database.UpdateWorkspaceBuildByIDParams{ ID: build.ID, UpdatedAt: build.UpdatedAt, @@ -1238,7 +1238,7 @@ func (s *MethodTestSuite) TestWorkspace() { })) s.Run("GetWorkspaceByWorkspaceAppID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{WorkspaceID: ws.ID, JobID: uuid.New()}) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index e1c8ba9b2b7d3..3fcae20ac387b 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -76,7 +76,7 @@ func (q *querier) GetUserLinkByUserIDLoginType(ctx context.Context, arg database return q.db.GetUserLinkByUserIDLoginType(ctx, arg) } -func (q *querier) GetLatestWorkspaceBuilds(ctx context.Context) ([]database.WorkspaceBuild, error) { +func (q *querier) GetLatestWorkspaceBuilds(ctx context.Context) ([]database.WorkspaceBuildRBAC, error) { // This function is a system function until we implement a join for workspace builds. // This is because we need to query for all related workspaces to the returned builds. // This is a very inefficient method of fetching the latest workspace builds. @@ -162,7 +162,7 @@ func (q *querier) GetDeploymentDAUs(ctx context.Context) ([]database.GetDeployme } // UpdateWorkspaceBuildCostByID is used by the provisioning system to update the cost of a workspace build. -func (q *querier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuildThin, error) { +func (q *querier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { return q.db.UpdateWorkspaceBuildCostByID(ctx, arg) } @@ -177,7 +177,7 @@ func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) { // Telemetry related functions. These functions are system functions for returning // telemetry data. Never called by a user. -func (q *querier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceBuild, error) { +func (q *querier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceBuildRBAC, error) { return q.db.GetWorkspaceBuildsCreatedAfter(ctx, createdAt) } diff --git a/coderd/database/dbauthz/system_test.go b/coderd/database/dbauthz/system_test.go index 09fcdeba0d330..9965ae73a26c1 100644 --- a/coderd/database/dbauthz/system_test.go +++ b/coderd/database/dbauthz/system_test.go @@ -34,8 +34,8 @@ func (s *MethodTestSuite) TestSystemFunctions() { }).Asserts().Returns(l) })) s.Run("GetLatestWorkspaceBuilds", s.Subtest(func(db database.Store, check *expects) { - dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) - dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) + dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildRBAC{}) + dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildRBAC{}) check.Args().Asserts() })) s.Run("GetWorkspaceAgentByAuthToken", s.Subtest(func(db database.Store, check *expects) { @@ -92,7 +92,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts() })) s.Run("UpdateWorkspaceBuildCostByID", s.Subtest(func(db database.Store, check *expects) { - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildRBAC{}) o := b o.DailyCost = 10 check.Args(database.UpdateWorkspaceBuildCostByIDParams{ @@ -109,7 +109,7 @@ func (s *MethodTestSuite) TestSystemFunctions() { check.Args().Asserts() })) s.Run("GetWorkspaceBuildsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildThin{CreatedAt: time.Now().Add(-time.Hour)}) + _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{CreatedAt: time.Now().Add(-time.Hour)}) check.Args(time.Now()).Asserts() })) s.Run("GetWorkspaceAgentsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 32093453ba934..df96d9ce5a4af 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -60,7 +60,7 @@ func New() database.Store { templateVersions: make([]database.TemplateVersion, 0), templates: make([]database.Template, 0), workspaceAgentStats: make([]database.WorkspaceAgentStat, 0), - workspaceBuilds: make([]database.WorkspaceBuildThin, 0), + workspaceBuilds: make([]database.WorkspaceBuild, 0), workspaceApps: make([]database.WorkspaceApp, 0), workspaces: make([]database.Workspace, 0), licenses: make([]database.License, 0), @@ -124,7 +124,7 @@ type data struct { templates []database.Template workspaceAgents []database.WorkspaceAgent workspaceApps []database.WorkspaceApp - workspaceBuilds []database.WorkspaceBuildThin + workspaceBuilds []database.WorkspaceBuild workspaceBuildParameters []database.WorkspaceBuildParameter workspaceResourceMetadata []database.WorkspaceResourceMetadatum workspaceResources []database.WorkspaceResource @@ -1288,7 +1288,7 @@ func (q *fakeQuerier) GetWorkspaceByAgentID(_ context.Context, agentID uuid.UUID return database.Workspace{}, sql.ErrNoRows } - var build database.WorkspaceBuildThin + var build database.WorkspaceBuild for _, _build := range q.workspaceBuilds { if _build.JobID == resource.JobID { build = _build @@ -1402,7 +1402,7 @@ func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.U return apps, nil } -func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1411,10 +1411,10 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da return q.expandWorkspaceThin(history), nil } } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildRBAC{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -1423,14 +1423,14 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI return q.expandWorkspaceThin(build), nil } } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildRBAC{}, sql.ErrNoRows } -func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() - var row database.WorkspaceBuild + var row database.WorkspaceBuildRBAC var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { @@ -1439,16 +1439,16 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo } } if buildNum == -1 { - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildRBAC{}, sql.ErrNoRows } return row, nil } -func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuild) + builds := make(map[uuid.UUID]database.WorkspaceBuildRBAC) buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { id := workspaceBuild.WorkspaceID @@ -1457,7 +1457,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.Wo buildNumbers[id] = workspaceBuild.BuildNumber } } - var returnBuilds []database.WorkspaceBuild + var returnBuilds []database.WorkspaceBuildRBAC for i, n := range buildNumbers { if n > 0 { b := builds[i] @@ -1470,11 +1470,11 @@ func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.Wo return returnBuilds, nil } -func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuild) + builds := make(map[uuid.UUID]database.WorkspaceBuildRBAC) buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { for _, id := range ids { @@ -1484,7 +1484,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, } } } - var returnBuilds []database.WorkspaceBuild + var returnBuilds []database.WorkspaceBuildRBAC for i, n := range buildNumbers { if n > 0 { b := builds[i] @@ -1499,7 +1499,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, params database.GetWorkspaceBuildsByWorkspaceIDParams, -) ([]database.WorkspaceBuild, error) { +) ([]database.WorkspaceBuildRBAC, error) { if err := validateDatabaseType(params); err != nil { return nil, err } @@ -1507,7 +1507,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, q.mutex.RLock() defer q.mutex.RUnlock() - history := make([]database.WorkspaceBuild, 0) + history := make([]database.WorkspaceBuildRBAC, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.Before(params.Since) { continue @@ -1518,7 +1518,7 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, } // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { + slices.SortFunc(history, func(a, b database.WorkspaceBuildRBAC) bool { // use greater than since we want descending order return a.BuildNumber > b.BuildNumber }) @@ -1560,9 +1560,9 @@ func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, return history, nil } -func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuildRBAC, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceBuildRBAC{}, err } q.mutex.RLock() @@ -1577,7 +1577,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con } return q.expandWorkspaceThin(workspaceBuild), nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.WorkspaceBuildRBAC{}, sql.ErrNoRows } func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { @@ -1594,11 +1594,11 @@ func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu return params, nil } -func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuildRBAC, error) { q.mutex.RLock() defer q.mutex.RUnlock() - workspaceBuilds := make([]database.WorkspaceBuild, 0) + workspaceBuilds := make([]database.WorkspaceBuildRBAC, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.After(after) { workspaceBuilds = append(workspaceBuilds, q.expandWorkspaceThin(workspaceBuild)) @@ -3176,15 +3176,15 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork return workspace, nil } -func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuildThin, error) { +func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } q.mutex.Lock() defer q.mutex.Unlock() - workspaceBuild := database.WorkspaceBuildThin{ + workspaceBuild := database.WorkspaceBuild{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -3629,9 +3629,9 @@ func (q *fakeQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(_ context.Context, return nil } -func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuildThin, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } q.mutex.Lock() @@ -3648,12 +3648,12 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuildThin{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuildThin, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuildThin{}, err + return database.WorkspaceBuild{}, err } q.mutex.Lock() @@ -3667,7 +3667,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg databa q.workspaceBuilds[index] = workspaceBuild return workspaceBuild, nil } - return database.WorkspaceBuildThin{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { @@ -4520,7 +4520,7 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI continue } - var lastBuild database.WorkspaceBuild + var lastBuild database.WorkspaceBuildRBAC for _, build := range q.workspaceBuilds { if build.WorkspaceID != workspace.ID { continue @@ -4552,7 +4552,7 @@ func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, } // expandWorkspaceThin must be called from a locked context. -func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuildThin) database.WorkspaceBuild { +func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuild) database.WorkspaceBuildRBAC { w, _ := q.GetWorkspaceByID(context.Background(), thin.WorkspaceID) return thin.Expand(w.OrganizationID, w.OwnerID) } diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index f21c74cab8541..5f60da7c033bd 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -153,15 +153,15 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas } type AnyWorkspaceBuild interface { - database.WorkspaceBuild | database.WorkspaceBuildThin + database.WorkspaceBuildRBAC | database.WorkspaceBuild } -func WorkspaceBuild[B AnyWorkspaceBuild](t testing.TB, db database.Store, orig B) database.WorkspaceBuildThin { - var thin database.WorkspaceBuildThin +func WorkspaceBuild[B AnyWorkspaceBuild](t testing.TB, db database.Store, orig B) database.WorkspaceBuild { + var thin database.WorkspaceBuild switch v := any(orig).(type) { + case database.WorkspaceBuildRBAC: + thin = v.WorkspaceBuild case database.WorkspaceBuild: - thin = v.WorkspaceBuildThin - case database.WorkspaceBuildThin: thin = v default: panic(fmt.Sprintf("developer error: invalid type %T", v)) diff --git a/coderd/database/dbgen/generator_test.go b/coderd/database/dbgen/generator_test.go index f85a5cfb821b3..8fc5ed3976c1b 100644 --- a/coderd/database/dbgen/generator_test.go +++ b/coderd/database/dbgen/generator_test.go @@ -166,8 +166,8 @@ func TestGenerator(t *testing.T) { t.Run("WorkspaceBuild", func(t *testing.T) { t.Parallel() db := dbfake.New() - exp := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{}) - require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).WorkspaceBuildThin) + exp := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildRBAC{}) + require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).WorkspaceBuild) }) t.Run("User", func(t *testing.T) { diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 5702c61476f36..bbc0b0815e328 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -103,7 +103,7 @@ func (g Group) RBACObject() rbac.Object { InOrg(g.OrganizationID) } -func (b WorkspaceBuild) RBACObject() rbac.Object { +func (b WorkspaceBuildRBAC) RBACObject() rbac.Object { return rbac.ResourceWorkspace.WithID(b.WorkspaceID). InOrg(b.OrganizationID). WithOwner(b.WorkspaceOwnerID.String()) @@ -191,15 +191,15 @@ func (l License) RBACObject() rbac.Object { return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10)) } -func (b WorkspaceBuildThin) WithWorkspace(workspace Workspace) WorkspaceBuild { +func (b WorkspaceBuild) WithWorkspace(workspace Workspace) WorkspaceBuildRBAC { return b.Expand(workspace.OrganizationID, workspace.OwnerID) } -func (b WorkspaceBuildThin) Expand(orgID, ownerID uuid.UUID) WorkspaceBuild { - return WorkspaceBuild{ - WorkspaceBuildThin: b, - OrganizationID: orgID, - WorkspaceOwnerID: ownerID, +func (b WorkspaceBuild) Expand(orgID, ownerID uuid.UUID) WorkspaceBuildRBAC { + return WorkspaceBuildRBAC{ + WorkspaceBuild: b, + OrganizationID: orgID, + WorkspaceOwnerID: ownerID, } } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 3d5cdcac6b908..44aa7a4d2e31e 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -180,18 +180,18 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([ type workspaceQuerier interface { GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) - GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuild, error) - GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) - GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuildRBAC, error) + GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuildRBAC, error) + GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuildRBAC, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuildRBAC, error) + GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuildRBAC, error) + GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuildRBAC, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildRBAC, error) + GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuildRBAC, error) } -type WorkspaceBuild struct { - WorkspaceBuildThin +type WorkspaceBuildRBAC struct { + WorkspaceBuild OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` } @@ -206,25 +206,25 @@ type getWorkspaceBuildParams struct { Latest bool `db:"-"` } -func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) { +func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuildRBAC, error) { arg.LimitOpt = 1 - return sqlxqueries.GetContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuild", arg) + return sqlxqueries.GetContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuild", arg) } -func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuild, error) { +func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuildRBAC, error) { arg.LimitOpt = -1 - return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuild", arg) + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuild", arg) } -func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { +func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuildRBAC, error) { return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{BuildID: id}) } -func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) { +func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuildRBAC, error) { return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{JobID: jobID}) } -func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuild, error) { +func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuildRBAC, error) { return q.selectWorkspaceBuild(ctx, getWorkspaceBuildParams{CreatedAfter: after}) } @@ -233,7 +233,7 @@ type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { WorkspaceID uuid.UUID } -func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuildRBAC, error) { return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{ BuildNumber: arg.BuildNumber, WorkspaceID: arg.WorkspaceID, @@ -248,20 +248,20 @@ type GetWorkspaceBuildsByWorkspaceIDParams struct { LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } -func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) { - return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuildsByWorkspaceID", arg) +func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuildRBAC, error) { + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuildsByWorkspaceID", arg) } -func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuild, error) { +func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuildRBAC, error) { return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{WorkspaceID: workspacedID, Latest: true}) } -func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) { - return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) +func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuildRBAC, error) { + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) } -func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) { - return sqlxqueries.SelectContext[WorkspaceBuild](ctx, q.sdb, "GetLatestWorkspaceBuilds", nil) +func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildRBAC, error) { + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetLatestWorkspaceBuilds", nil) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go index 09c2f67ed80df..ee77f65ae34de 100644 --- a/coderd/database/modelqueries_test.go +++ b/coderd/database/modelqueries_test.go @@ -60,8 +60,8 @@ func TestGetWorkspaceBuild(t *testing.T) { InitiatorID: user.ID, }), } - builds = []database.WorkspaceBuildThin{ - dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + builds = []database.WorkspaceBuild{ + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspace.ID, TemplateVersionID: version.ID, BuildNumber: 1, @@ -71,7 +71,7 @@ func TestGetWorkspaceBuild(t *testing.T) { Reason: database.BuildReasonInitiator, CreatedAt: now, }), - dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspace.ID, TemplateVersionID: version.ID, BuildNumber: 2, @@ -82,7 +82,7 @@ func TestGetWorkspaceBuild(t *testing.T) { CreatedAt: now.Add(time.Hour), }), } - orderBuilds = []database.WorkspaceBuildThin{ + orderBuilds = []database.WorkspaceBuild{ builds[1], builds[0], } @@ -96,7 +96,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + require.Equal(t, expected, build.WorkspaceBuild, "builds should be equal") } }) @@ -108,7 +108,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } expected := builds[i] - require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + require.Equal(t, expected, build.WorkspaceBuild, "builds should be equal") } }) @@ -133,7 +133,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal") + require.Equal(t, expected, build.WorkspaceBuild, "builds should be equal") } }) @@ -167,7 +167,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only 1 build") - require.Equal(t, builds[1], found[0].WorkspaceBuildThin, "builds should be equal") + require.Equal(t, builds[1], found[0].WorkspaceBuild, "builds should be equal") }) t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) { @@ -180,10 +180,10 @@ func TestGetWorkspaceBuild(t *testing.T) { }) } -func toThins(builds []database.WorkspaceBuild) []database.WorkspaceBuildThin { - thins := make([]database.WorkspaceBuildThin, len(builds)) +func toThins(builds []database.WorkspaceBuildRBAC) []database.WorkspaceBuild { + thins := make([]database.WorkspaceBuild, len(builds)) for i, build := range builds { - thins[i] = build.WorkspaceBuildThin + thins[i] = build.WorkspaceBuild } return thins } diff --git a/coderd/database/models.go b/coderd/database/models.go index cca69d1a24a1a..11c742aadd77a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1614,7 +1614,7 @@ type WorkspaceBuildParameter struct { Value string `db:"value" json:"value"` } -type WorkspaceBuildThin struct { +type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index ec86638d1107d..fcb7db36299df 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -172,7 +172,7 @@ type sqlcQuerier interface { InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) - InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuildThin, error) + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) @@ -216,8 +216,8 @@ type sqlcQuerier interface { UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error - UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuildThin, error) - UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuildThin, error) + UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) + UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e91bb77ac84f9..7f84585d77e45 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6017,7 +6017,7 @@ type InsertWorkspaceBuildParams struct { Reason BuildReason `db:"reason" json:"reason"` } -func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuildThin, error) { +func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) { row := q.db.QueryRowContext(ctx, insertWorkspaceBuild, arg.ID, arg.CreatedAt, @@ -6033,7 +6033,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa arg.MaxDeadline, arg.Reason, ) - var i WorkspaceBuildThin + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -6073,7 +6073,7 @@ type UpdateWorkspaceBuildByIDParams struct { MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` } -func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuildThin, error) { +func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) { row := q.db.QueryRowContext(ctx, updateWorkspaceBuildByID, arg.ID, arg.UpdatedAt, @@ -6081,7 +6081,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor arg.Deadline, arg.MaxDeadline, ) - var i WorkspaceBuildThin + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -6115,9 +6115,9 @@ type UpdateWorkspaceBuildCostByIDParams struct { DailyCost int32 `db:"daily_cost" json:"daily_cost"` } -func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuildThin, error) { +func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) { row := q.db.QueryRowContext(ctx, updateWorkspaceBuildCostByID, arg.ID, arg.DailyCost) - var i WorkspaceBuildThin + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index c6908323a7734..fcc5d777003ca 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -20,7 +20,6 @@ overrides: go_type: type: "TemplateACL" rename: - workspace_build: WorkspaceBuildThin api_key: APIKey api_key_scope: APIKeyScope api_key_scope_all: APIKeyScopeAll diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index e15a6858b8c1c..bcf6ee2f7e0e2 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -54,7 +54,7 @@ func TestWorkspaceAgent(t *testing.T) { resource = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: job.ID, }) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspace.ID, JobID: job.ID, }) diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index f8863eb718142..f6a4c3fe1eaa7 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,7 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { workspace = dbgen.Workspace(t, db, database.Workspace{ OwnerID: user.ID, }) - build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspace.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/httpmw/workspacebuildparam.go b/coderd/httpmw/workspacebuildparam.go index 7ae728dfa6734..7ee9589768fb4 100644 --- a/coderd/httpmw/workspacebuildparam.go +++ b/coderd/httpmw/workspacebuildparam.go @@ -16,8 +16,8 @@ import ( type workspaceBuildParamContextKey struct{} // WorkspaceBuildParam returns the workspace build from the ExtractWorkspaceBuildParam handler. -func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild { - workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild) +func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuildRBAC { + workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuildRBAC) if !ok { panic("developer error: workspace build param middleware not provided") } diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index 5300decdc0516..ac495537a1de3 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -91,7 +91,7 @@ func TestWorkspaceBuildParam(t *testing.T) { }) r, workspace := setupAuthentication(db) - workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, WorkspaceID: workspace.ID, diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index 660bcb4819ade..dd19abe8ebfcc 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -350,7 +350,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h OwnerID: user.ID, Name: cfg.WorkspaceName, }) - build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + build = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ WorkspaceID: workspace.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/httpmw/workspaceresourceparam_test.go b/coderd/httpmw/workspaceresourceparam_test.go index 6070eb0f7099c..bee32b0d304c3 100644 --- a/coderd/httpmw/workspaceresourceparam_test.go +++ b/coderd/httpmw/workspaceresourceparam_test.go @@ -27,7 +27,7 @@ func TestWorkspaceResourceParam(t *testing.T) { StorageMethod: database.ProvisionerStorageMethodFile, }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ JobID: job.ID, Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonInitiator, diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index ee20214ec2440..6b79ad84324a5 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -664,7 +664,7 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p return nil, xerrors.Errorf("unmarshal workspace provision input: %w", err) } - var build database.WorkspaceBuild + var build database.WorkspaceBuildRBAC err := server.Database.InTx(func(db database.Store) error { workspaceBuild, err := db.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID) if err != nil { @@ -715,7 +715,7 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p BuildNumber: previousBuildNumber, }) if prevBuildErr != nil { - previousBuild = database.WorkspaceBuild{} + previousBuild = database.WorkspaceBuildRBAC{} } // We pass the below information to the Auditor so that it @@ -731,7 +731,7 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p server.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err)) } - audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ + audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuildRBAC]{ Audit: *auditor, Log: server.Logger, UserID: job.InitiatorID, @@ -1035,7 +1035,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete BuildNumber: previousBuildNumber, }) if prevBuildErr != nil { - previousBuild = database.WorkspaceBuild{} + previousBuild = database.WorkspaceBuildRBAC{} } // We pass the below information to the Auditor so that it @@ -1051,7 +1051,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete server.Logger.Error(ctx, "marshal resource info for successful job", slog.Error(err)) } - audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ + audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuildRBAC]{ Audit: *auditor, Log: server.Logger, UserID: job.InitiatorID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index bae94b15565f1..1449655d228b6 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -147,7 +147,7 @@ func TestAcquireJob(t *testing.T) { TemplateID: template.ID, OwnerID: user.ID, }) - build := dbgen.WorkspaceBuild(t, srv.Database, database.WorkspaceBuildThin{ + build := dbgen.WorkspaceBuild(t, srv.Database, database.WorkspaceBuild{ WorkspaceID: workspace.ID, BuildNumber: 1, JobID: uuid.New(), diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 368a10fb9a9c4..c2a8e7fbff20b 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -503,7 +503,7 @@ func ConvertWorkspace(workspace database.Workspace) Workspace { } // ConvertWorkspaceBuild anonymizes a workspace build. -func ConvertWorkspaceBuild(build database.WorkspaceBuild) WorkspaceBuild { +func ConvertWorkspaceBuild(build database.WorkspaceBuildRBAC) WorkspaceBuild { return WorkspaceBuild{ ID: build.ID, CreatedAt: build.CreatedAt, diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 62f8b76e5a29b..cdd1d42f8b67e 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -60,7 +60,7 @@ func TestTelemetry(t *testing.T) { Health: database.WorkspaceAppHealthDisabled, }) _ = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{}) - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{ + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ Transition: database.WorkspaceTransitionStart, Reason: database.BuildReasonAutostart, }) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7db5fbf4e3faf..1f21d3a0d3fc2 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -41,7 +41,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuild{workspaceBuild}) + data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuildRBAC{workspaceBuild}) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -113,7 +113,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { } } - var workspaceBuilds []database.WorkspaceBuild + var workspaceBuilds []database.WorkspaceBuildRBAC // Ensure all db calls happen in the same tx err := api.Database.InTx(func(store database.Store) error { var err error @@ -253,7 +253,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuild{workspaceBuild}) + data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuildRBAC{workspaceBuild}) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -526,7 +526,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - var workspaceBuild database.WorkspaceBuild + var workspaceBuild database.WorkspaceBuildRBAC var provisionerJob database.ProvisionerJob // This must happen in a transaction to ensure history can be inserted, and // the prior history can update it's "after" column to point at the new. @@ -926,7 +926,7 @@ type workspaceBuildsData struct { apps []database.WorkspaceApp } -func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) { +func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuildRBAC) (workspaceBuildsData, error) { userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) for _, build := range workspaceBuilds { userIDs = append(userIDs, build.InitiatorID) @@ -1017,7 +1017,7 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W } func (api *API) convertWorkspaceBuilds( - workspaceBuilds []database.WorkspaceBuild, + workspaceBuilds []database.WorkspaceBuildRBAC, workspaces []database.Workspace, jobs []database.ProvisionerJob, users []database.User, @@ -1078,7 +1078,7 @@ func (api *API) convertWorkspaceBuilds( } func (api *API) convertWorkspaceBuild( - build database.WorkspaceBuild, + build database.WorkspaceBuildRBAC, workspace database.Workspace, job database.ProvisionerJob, users []database.User, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index d89623d4de586..4aab14c1b12a4 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -470,7 +470,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req tags := provisionerdserver.MutateTags(user.ID, templateVersionJob.Tags) var ( provisionerJob database.ProvisionerJob - workspaceBuild database.WorkspaceBuild + workspaceBuild database.WorkspaceBuildRBAC ) err = api.Database.InTx(func(db database.Store) error { now := database.Now() diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 442a8b0354b98..ff3b84b156367 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -111,7 +111,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "ttl": ActionTrack, "last_used_at": ActionIgnore, }, - &database.WorkspaceBuild{}: { + &database.WorkspaceBuildRBAC{}: { "id": ActionIgnore, "created_at": ActionIgnore, "updated_at": ActionIgnore, From fc838bf0a04829b5533bdc3a72729b96a359fb28 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:20:03 -0600 Subject: [PATCH 24/36] Make fmt --- coderd/database/sqlxqueries/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coderd/database/sqlxqueries/README.md b/coderd/database/sqlxqueries/README.md index c7d5ac49b5fb0..c003dee679f3e 100644 --- a/coderd/database/sqlxqueries/README.md +++ b/coderd/database/sqlxqueries/README.md @@ -19,14 +19,13 @@ Unfortunately, the VSCode extension does not support both go template and postgr 1. `ctl + shift + p` 1. "Change language Mode" 1. "Postgres" or "Go Template File" - - Feel free to create a permanent file association with `*.gosql` files. +- Feel free to create a permanent file association with `*.gosql` files. ## Goland Goland supports [template highlighting](https://www.jetbrains.com/help/go/integration-with-go-templates.html) out of the box. To associate sql files, add a new file type in **Editor** settings. Select "Go template files". Add a new filename of `*.gosql` and select "postgres" as the "Template Data Language". - ![Goland language configuration](./imgs/goland-gosql.png) It also helps to support the sqlc type variables. You can do this by adding ["User Parameters"](https://www.jetbrains.com/help/datagrip/settings-tools-database-user-parameters.html) in database queries. From 4dc0a3493c27d44104c3df2426113d93310e5266 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:27:14 -0600 Subject: [PATCH 25/36] Make gens and make fmts --- coderd/database/dump.sql | 19 ----------------- coderd/database/modelqueries.go | 2 ++ coderd/database/models.go | 30 ++++++--------------------- coderd/database/sqlxqueries/README.md | 4 ++-- docs/admin/audit-logs.md | 2 +- enterprise/audit/table.go | 2 ++ 6 files changed, 13 insertions(+), 46 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 24986b1b6c24c..0fb8bc942c74f 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -601,25 +601,6 @@ CREATE TABLE workspaces ( last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL ); -CREATE VIEW workspace_builds_rbac AS - SELECT workspace_builds.id, - workspace_builds.created_at, - workspace_builds.updated_at, - workspace_builds.workspace_id, - workspace_builds.template_version_id, - workspace_builds.build_number, - workspace_builds.transition, - workspace_builds.initiator_id, - workspace_builds.provisioner_state, - workspace_builds.job_id, - workspace_builds.deadline, - workspace_builds.reason, - workspace_builds.daily_cost, - workspaces.organization_id, - workspaces.owner_id AS workspace_owner_id - FROM (public.workspace_builds - JOIN workspaces ON ((workspace_builds.workspace_id = workspaces.id))); - CREATE TABLE workspace_resource_metadata ( workspace_resource_id uuid NOT NULL, key character varying(1024) NOT NULL, diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 44aa7a4d2e31e..14c9bf8351672 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -190,6 +190,8 @@ type workspaceQuerier interface { GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuildRBAC, error) } +// WorkspaceBuildRBAC extends WorkspaceBuild with fields that are used for RBAC. +// This allows WorkspaceBuild to be used in Authorize() calls. type WorkspaceBuildRBAC struct { WorkspaceBuild OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` diff --git a/coderd/database/models.go b/coderd/database/models.go index 11c742aadd77a..6e177b3a2d27f 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1606,14 +1606,6 @@ type WorkspaceApp struct { External bool `db:"external" json:"external"` } -type WorkspaceBuildParameter struct { - WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"` - // Parameter name - Name string `db:"name" json:"name"` - // Parameter value - Value string `db:"value" json:"value"` -} - type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` @@ -1631,22 +1623,12 @@ type WorkspaceBuild struct { MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` } -type WorkspaceBuildsRbac struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - BuildNumber int32 `db:"build_number" json:"build_number"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"` - 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"` - DailyCost int32 `db:"daily_cost" json:"daily_cost"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"` +type WorkspaceBuildParameter struct { + WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"` + // Parameter name + Name string `db:"name" json:"name"` + // Parameter value + Value string `db:"value" json:"value"` } type WorkspaceResource struct { diff --git a/coderd/database/sqlxqueries/README.md b/coderd/database/sqlxqueries/README.md index c003dee679f3e..7a88fa8963d52 100644 --- a/coderd/database/sqlxqueries/README.md +++ b/coderd/database/sqlxqueries/README.md @@ -1,6 +1,6 @@ # Editor/IDE config -To edit template files, it is best to configure your IDE to work with go template files. +To edit template files, it is best to configure your IDE to work with go template files. VSCode gives better highlighting support, as the Goland highlighting tends to recognize the sql as invalid and shows many sql errors in the template file. ## VSCode @@ -14,7 +14,7 @@ The default extension [supports syntax highlighting](https://github.com/golang/v }, ``` -Unfortunately, the VSCode extension does not support both go template and postgres highlighting. You can switch between the two with: +The VSCode extension does not support both go template and postgres highlighting. I suggest you use Postgres highlighting, as it is much easier to work with. You can switch between the two with: 1. `ctl + shift + p` 1. "Change language Mode" diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index f3950f0419174..be5682e8615df 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -19,7 +19,7 @@ We track the following resources: | TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceBuildRBAC
|
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
organization_idfalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
workspace_owner_idfalse
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index ff3b84b156367..c6052b28b956b 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -125,6 +125,8 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "deadline": ActionIgnore, "reason": ActionIgnore, "daily_cost": ActionIgnore, + "organization_id": ActionIgnore, + "workspace_owner_id": ActionIgnore, }, &database.AuditableGroup{}: { "id": ActionTrack, From 98f8c56a8dee619a669848674bfe90f817fa94f4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:30:26 -0600 Subject: [PATCH 26/36] Make gen --- docs/admin/audit-logs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 3e06f307c2efd..51a239b7e6cd9 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -19,7 +19,7 @@ We track the following resources: | TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
organization_idfalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
workspace_owner_idfalse
| From e0a09f0f3fa4710f3958c4fbae6086bc10b3c705 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:37:07 -0600 Subject: [PATCH 27/36] fix linting errors --- coderd/database/sqlxqueries/bindvars.go | 2 +- coderd/database/sqlxqueries/sqlxqueries.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/coderd/database/sqlxqueries/bindvars.go b/coderd/database/sqlxqueries/bindvars.go index fdfd25921b192..04dc7a791da51 100644 --- a/coderd/database/sqlxqueries/bindvars.go +++ b/coderd/database/sqlxqueries/bindvars.go @@ -52,7 +52,7 @@ func bindNamed(query string, arg interface{}) (newQuery string, args []interface // This comes straight from SQLx's implementation to get the values // of the struct fields. - v := reflect.ValueOf(arg) + var v reflect.Value for v = reflect.ValueOf(arg); v.Kind() == reflect.Ptr; { v = v.Elem() } diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index 5c90bdd242275..83a0c646cc687 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -14,8 +14,9 @@ var sqlxQueries embed.FS var ( // Only parse the queries once. - once sync.Once - cached *template.Template + once sync.Once + cached *template.Template + //nolint:errname cachedError error ) From 1eebb14b5a41a936663109c1e6f14c5777d94cf4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 09:52:29 -0600 Subject: [PATCH 28/36] Make dump.sql --- coderd/database/dump.sql | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 58ec334d28ece..1e906e26273b5 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -588,20 +588,6 @@ CREATE TABLE workspace_builds ( max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL ); -CREATE TABLE workspaces ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - owner_id uuid NOT NULL, - organization_id uuid NOT NULL, - template_id uuid NOT NULL, - deleted boolean DEFAULT false NOT NULL, - name character varying(64) NOT NULL, - autostart_schedule text, - ttl bigint, - last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL -); - CREATE TABLE workspace_resource_metadata ( workspace_resource_id uuid NOT NULL, key character varying(1024) NOT NULL, @@ -632,6 +618,20 @@ CREATE TABLE workspace_resources ( daily_cost integer DEFAULT 0 NOT NULL ); +CREATE TABLE workspaces ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + owner_id uuid NOT NULL, + organization_id uuid NOT NULL, + template_id uuid NOT NULL, + deleted boolean DEFAULT false NOT NULL, + name character varying(64) NOT NULL, + autostart_schedule text, + ttl bigint, + last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL +); + ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass); From b13f5930fe1fe55a483f12096f0c1d9b0d904606 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 10:06:20 -0600 Subject: [PATCH 29/36] Handle transactions --- coderd/database/db.go | 3 +++ coderd/database/modelqueries.go | 10 +++++----- coderd/database/sqlxqueries/sqlx.go | 6 ++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/coderd/database/db.go b/coderd/database/db.go index f8de976e92f72..c6c7f712ac697 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -37,6 +37,9 @@ type DBTX interface { QueryRowContext(context.Context, string, ...interface{}) *sql.Row SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + + // Extends the sqlx interface + sqlx.QueryerContext } // New creates a new database store using a SQL database connection. diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 14c9bf8351672..2c7e195c6a31d 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -210,12 +210,12 @@ type getWorkspaceBuildParams struct { func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuildRBAC, error) { arg.LimitOpt = 1 - return sqlxqueries.GetContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuild", arg) + return sqlxqueries.GetContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuild", arg) } func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuildRBAC, error) { arg.LimitOpt = -1 - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuild", arg) + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuild", arg) } func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuildRBAC, error) { @@ -251,7 +251,7 @@ type GetWorkspaceBuildsByWorkspaceIDParams struct { } func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetWorkspaceBuildsByWorkspaceID", arg) + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuildsByWorkspaceID", arg) } func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuildRBAC, error) { @@ -259,11 +259,11 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) } func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.sdb, "GetLatestWorkspaceBuilds", nil) + return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetLatestWorkspaceBuilds", nil) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index dd92f33acbb63..4756dc9b89714 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -33,6 +33,9 @@ func constructQuery(queryName string, argument any) (string, []any, error) { // If the query returns no rows, an empty slice is returned. func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument any) ([]RT, error) { var empty []RT + if q == nil { + return empty, xerrors.New("queryer is nil") + } query, args, err := constructQuery(queryName, argument) if err != nil { @@ -51,6 +54,9 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName // If the query returns no rows, sql.ErrNoRows is returned. func GetContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) (RT, error) { var empty RT + if q == nil { + return empty, xerrors.New("queryer is nil") + } query, args, err := constructQuery(queryName, argument) if err != nil { From 56b847e5b13105666929d630c578e4fa912002bf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 10:42:44 -0600 Subject: [PATCH 30/36] Fix a unit ttest --- coderd/database/dbauthz/querier_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index 24f4aabede3a1..d8be79596a6a4 100644 --- a/coderd/database/dbauthz/querier_test.go +++ b/coderd/database/dbauthz/querier_test.go @@ -1207,9 +1207,11 @@ func (s *MethodTestSuite) TestWorkspace() { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) check.Args(database.UpdateWorkspaceBuildByIDParams{ - ID: build.ID, - UpdatedAt: build.UpdatedAt, - Deadline: build.Deadline, + ID: build.ID, + UpdatedAt: build.UpdatedAt, + Deadline: build.Deadline, + ProvisionerState: build.ProvisionerState, + MaxDeadline: build.MaxDeadline, }).Asserts(ws, rbac.ActionUpdate).Returns(build) })) s.Run("SoftDeleteWorkspaceByID", s.Subtest(func(db database.Store, check *expects) { From 2cce422b76346783e3c5bcd42124e23f2e747139 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 10:52:50 -0600 Subject: [PATCH 31/36] Fix a unit ttest --- coderd/database/modelqueries_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/modelqueries_test.go b/coderd/database/modelqueries_test.go index ee77f65ae34de..5e85ec984cac2 100644 --- a/coderd/database/modelqueries_test.go +++ b/coderd/database/modelqueries_test.go @@ -120,7 +120,7 @@ func TestGetWorkspaceBuild(t *testing.T) { } expected := builds[1] require.Len(t, found, 1, "should only be one build") - require.Equal(t, expected, found[0], "builds should be equal") + require.Equal(t, expected, found[0].WorkspaceBuild, "builds should be equal") }) t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) { @@ -157,7 +157,7 @@ func TestGetWorkspaceBuild(t *testing.T) { t.Fatal(err) } require.Len(t, found, 1, "should be only one build") - require.Equal(t, builds[1], found[0], "builds should be equal") + require.Equal(t, builds[1], found[0].WorkspaceBuild, "builds should be equal") }) t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) { @@ -176,7 +176,7 @@ func TestGetWorkspaceBuild(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal(t, builds[1], found, "builds should be equal") + require.Equal(t, builds[1], found.WorkspaceBuild, "builds should be equal") }) } From f5a87cabd7577663d47b998114f2320d5299fe1c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 8 Mar 2023 11:17:15 -0600 Subject: [PATCH 32/36] Fix deadlock --- coderd/database/dbfake/databasefake.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 8341ba95e3ce9..1b1fdc49ff25e 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -4554,6 +4554,13 @@ func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, // expandWorkspaceThin must be called from a locked context. func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuild) database.WorkspaceBuildRBAC { - w, _ := q.GetWorkspaceByID(context.Background(), thin.WorkspaceID) - return thin.Expand(w.OrganizationID, w.OwnerID) + for _, workspace := range q.workspaces { + if workspace.ID == thin.WorkspaceID { + return thin.WithWorkspace(workspace) + } + } + + return database.WorkspaceBuildRBAC{ + WorkspaceBuild: thin, + } } From d80dd10fd5f3c52adf5a245fa9d275014fcc8bba Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Mar 2023 09:45:22 -0600 Subject: [PATCH 33/36] Panic instead of error --- coderd/database/sqlxqueries/sqlxqueries.go | 15 +++++---------- .../sqlxqueries/sqlxqueries_internal_test.go | 9 +++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 coderd/database/sqlxqueries/sqlxqueries_internal_test.go diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index 83a0c646cc687..29387df7f3b95 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -16,38 +16,33 @@ var ( // Only parse the queries once. once sync.Once cached *template.Template - //nolint:errname - cachedError error ) // loadQueries parses the embedded queries and returns the template. // Results are cached. -func loadQueries() (*template.Template, error) { +func loadQueries() *template.Template { once.Do(func() { tpls, err := template.New(""). Funcs(template.FuncMap{ "int32": func(i int) int32 { return int32(i) }, }).ParseFS(sqlxQueries, "*.gosql") if err != nil { - cachedError = xerrors.Errorf("developer error parse sqlx queries: %w", err) + panic(xerrors.Errorf("developer error parse sqlx queries: %w", err)) return } cached = tpls }) - return cached, cachedError + return cached } // query executes the named template with the given data and returns the result. // The returned query string is SQL. func query(name string, data interface{}) (string, error) { - tpls, err := loadQueries() - if err != nil { - return "", err - } + tpls := loadQueries() var out bytes.Buffer - err = tpls.ExecuteTemplate(&out, name, data) + err := tpls.ExecuteTemplate(&out, name, data) if err != nil { return "", xerrors.Errorf("execute template %s: %w", name, err) } diff --git a/coderd/database/sqlxqueries/sqlxqueries_internal_test.go b/coderd/database/sqlxqueries/sqlxqueries_internal_test.go new file mode 100644 index 0000000000000..20aa36943adb2 --- /dev/null +++ b/coderd/database/sqlxqueries/sqlxqueries_internal_test.go @@ -0,0 +1,9 @@ +package sqlxqueries + +import "testing" + +func Test_loadQueries(t *testing.T) { + t.Parallel() + // If this panics, the test will fail. + loadQueries() +} From 2949513c4ddac9b9f946e2ab3e0d84f66d208bf6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Mar 2023 09:47:30 -0600 Subject: [PATCH 34/36] Put back in error --- coderd/database/sqlxqueries/sqlxqueries.go | 15 ++++++++++----- .../sqlxqueries/sqlxqueries_internal_test.go | 10 +++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index 29387df7f3b95..83a0c646cc687 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -16,33 +16,38 @@ var ( // Only parse the queries once. once sync.Once cached *template.Template + //nolint:errname + cachedError error ) // loadQueries parses the embedded queries and returns the template. // Results are cached. -func loadQueries() *template.Template { +func loadQueries() (*template.Template, error) { once.Do(func() { tpls, err := template.New(""). Funcs(template.FuncMap{ "int32": func(i int) int32 { return int32(i) }, }).ParseFS(sqlxQueries, "*.gosql") if err != nil { - panic(xerrors.Errorf("developer error parse sqlx queries: %w", err)) + cachedError = xerrors.Errorf("developer error parse sqlx queries: %w", err) return } cached = tpls }) - return cached + return cached, cachedError } // query executes the named template with the given data and returns the result. // The returned query string is SQL. func query(name string, data interface{}) (string, error) { - tpls := loadQueries() + tpls, err := loadQueries() + if err != nil { + return "", err + } var out bytes.Buffer - err := tpls.ExecuteTemplate(&out, name, data) + err = tpls.ExecuteTemplate(&out, name, data) if err != nil { return "", xerrors.Errorf("execute template %s: %w", name, err) } diff --git a/coderd/database/sqlxqueries/sqlxqueries_internal_test.go b/coderd/database/sqlxqueries/sqlxqueries_internal_test.go index 20aa36943adb2..effc94bbe5e8b 100644 --- a/coderd/database/sqlxqueries/sqlxqueries_internal_test.go +++ b/coderd/database/sqlxqueries/sqlxqueries_internal_test.go @@ -1,9 +1,13 @@ package sqlxqueries -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" +) func Test_loadQueries(t *testing.T) { t.Parallel() - // If this panics, the test will fail. - loadQueries() + _, err := loadQueries() + require.NoError(t, err) } From 7f480fbc48be1c9da09b48ff06cbc755fc66a8cd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Mar 2023 10:52:11 -0600 Subject: [PATCH 35/36] Remove generic function use --- coderd/database/modelqueries.go | 15 ++++++++++----- coderd/database/sqlxqueries/sqlx.go | 26 ++++++++++++-------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 2c7e195c6a31d..3ce2526fa029e 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -209,13 +209,15 @@ type getWorkspaceBuildParams struct { } func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuildRBAC, error) { + var res WorkspaceBuildRBAC arg.LimitOpt = 1 - return sqlxqueries.GetContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuild", arg) + return res, sqlxqueries.GetContext(ctx, q.db, "GetWorkspaceBuild", arg, &res) } func (q *sqlQuerier) selectWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) ([]WorkspaceBuildRBAC, error) { + var res []WorkspaceBuildRBAC arg.LimitOpt = -1 - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuild", arg) + return res, sqlxqueries.SelectContext(ctx, q.db, "GetWorkspaceBuild", arg, &res) } func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuildRBAC, error) { @@ -251,7 +253,8 @@ type GetWorkspaceBuildsByWorkspaceIDParams struct { } func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetWorkspaceBuildsByWorkspaceID", arg) + var res []WorkspaceBuildRBAC + return res, sqlxqueries.SelectContext(ctx, q.db, "GetWorkspaceBuildsByWorkspaceID", arg, &res) } func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspacedID uuid.UUID) (WorkspaceBuildRBAC, error) { @@ -259,11 +262,13 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids) + var res []WorkspaceBuildRBAC + return res, sqlxqueries.SelectContext(ctx, q.db, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids, &res) } func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildRBAC, error) { - return sqlxqueries.SelectContext[WorkspaceBuildRBAC](ctx, q.db, "GetLatestWorkspaceBuilds", nil) + var res []WorkspaceBuildRBAC + return res, sqlxqueries.SelectContext(ctx, q.db, "GetLatestWorkspaceBuilds", nil, &res) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go index 4756dc9b89714..eae23e00dbc4c 100644 --- a/coderd/database/sqlxqueries/sqlx.go +++ b/coderd/database/sqlxqueries/sqlx.go @@ -31,44 +31,42 @@ func constructQuery(queryName string, argument any) (string, []any, error) { // SelectContext runs the named query on the given database. // If the query returns no rows, an empty slice is returned. -func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument any) ([]RT, error) { - var empty []RT +func SelectContext(ctx context.Context, q sqlx.QueryerContext, queryName string, argument any, res any) error { if q == nil { - return empty, xerrors.New("queryer is nil") + return xerrors.New("queryer is nil") } query, args, err := constructQuery(queryName, argument) if err != nil { - return empty, xerrors.Errorf("get query: %w", err) + return xerrors.Errorf("get query: %w", err) } - err = sqlx.SelectContext(ctx, q, &empty, query, args...) + err = sqlx.SelectContext(ctx, q, res, query, args...) if err != nil { - return empty, xerrors.Errorf("%s: %w", queryName, err) + return xerrors.Errorf("%s: %w", queryName, err) } - return empty, nil + return nil } // GetContext runs the named query on the given database. // If the query returns no rows, sql.ErrNoRows is returned. -func GetContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}) (RT, error) { - var empty RT +func GetContext(ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}, res any) error { if q == nil { - return empty, xerrors.New("queryer is nil") + return xerrors.New("queryer is nil") } query, args, err := constructQuery(queryName, argument) if err != nil { - return empty, xerrors.Errorf("get query: %w", err) + return xerrors.Errorf("get query: %w", err) } // GetContext maps the results of the query to the items slice by struct // db tags. - err = sqlx.GetContext(ctx, q, &empty, query, args...) + err = sqlx.GetContext(ctx, q, res, query, args...) if err != nil { - return empty, xerrors.Errorf("%s: %w", queryName, err) + return xerrors.Errorf("%s: %w", queryName, err) } - return empty, nil + return nil } From 7d6a0da9509d7be441b543a47983f6c5c823828d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Mar 2023 23:12:09 -0600 Subject: [PATCH 36/36] Panic when query loading fails --- coderd/database/db.go | 9 +++++++++ coderd/database/sqlxqueries/sqlxqueries.go | 6 +++--- ...{sqlxqueries_internal_test.go => sqlxqueries_test.go} | 6 ++++-- 3 files changed, 16 insertions(+), 5 deletions(-) rename coderd/database/sqlxqueries/{sqlxqueries_internal_test.go => sqlxqueries_test.go} (55%) diff --git a/coderd/database/db.go b/coderd/database/db.go index c6c7f712ac697..6af2c459a9b7c 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -16,6 +16,8 @@ import ( "github.com/jmoiron/sqlx" "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/database/sqlxqueries" ) // Store contains all queryable database functions. @@ -45,6 +47,13 @@ type DBTX interface { // New creates a new database store using a SQL database connection. func New(sdb *sql.DB) Store { dbx := sqlx.NewDb(sdb, "postgres") + // Load the embedded queries. If this fails, some of our queries + // will never work. This is a fatal developer error that should never + // happen. + _, err := sqlxqueries.LoadQueries() + if err != nil { + panic(xerrors.Errorf("load queries: %w", err)) + } return &sqlQuerier{ db: dbx, sdb: dbx, diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go index 83a0c646cc687..abcad453290cf 100644 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ b/coderd/database/sqlxqueries/sqlxqueries.go @@ -20,9 +20,9 @@ var ( cachedError error ) -// loadQueries parses the embedded queries and returns the template. +// LoadQueries parses the embedded queries and returns the template. // Results are cached. -func loadQueries() (*template.Template, error) { +func LoadQueries() (*template.Template, error) { once.Do(func() { tpls, err := template.New(""). Funcs(template.FuncMap{ @@ -41,7 +41,7 @@ func loadQueries() (*template.Template, error) { // query executes the named template with the given data and returns the result. // The returned query string is SQL. func query(name string, data interface{}) (string, error) { - tpls, err := loadQueries() + tpls, err := LoadQueries() if err != nil { return "", err } diff --git a/coderd/database/sqlxqueries/sqlxqueries_internal_test.go b/coderd/database/sqlxqueries/sqlxqueries_test.go similarity index 55% rename from coderd/database/sqlxqueries/sqlxqueries_internal_test.go rename to coderd/database/sqlxqueries/sqlxqueries_test.go index effc94bbe5e8b..b273ce6fbc18b 100644 --- a/coderd/database/sqlxqueries/sqlxqueries_internal_test.go +++ b/coderd/database/sqlxqueries/sqlxqueries_test.go @@ -1,13 +1,15 @@ -package sqlxqueries +package sqlxqueries_test import ( "testing" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database/sqlxqueries" ) func Test_loadQueries(t *testing.T) { t.Parallel() - _, err := loadQueries() + _, err := sqlxqueries.LoadQueries() require.NoError(t, err) }