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/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go
index 2548e317b69ff..7ffbecbd4c23c 100644
--- a/coderd/autobuild/executor/lifecycle_executor_test.go
+++ b/coderd/autobuild/executor/lifecycle_executor_test.go
@@ -2,17 +2,16 @@ package executor_test
import (
"context"
- "os"
"testing"
"time"
- "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"
@@ -493,7 +492,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/db.go b/coderd/database/db.go
index f8de976e92f72..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.
@@ -37,11 +39,21 @@ 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.
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/dbauthz/querier.go b/coderd/database/dbauthz/querier.go
index 052863e2edc74..f0616a2810265 100644
--- a/coderd/database/dbauthz/querier.go
+++ b/coderd/database/dbauthz/querier.go
@@ -1167,25 +1167,12 @@ 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) {
- if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil {
- return database.WorkspaceBuild{}, err
- }
- return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
+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) {
- // 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)
+func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuildRBAC, error) {
+ return fetchWithPostFilter(q.auth, q.db.GetLatestWorkspaceBuildsByWorkspaceIDs)(ctx, ids)
}
func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) {
@@ -1263,35 +1250,16 @@ 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) {
- 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
+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) {
- 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
+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) {
- if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil {
- return database.WorkspaceBuild{}, err
- }
- return q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, arg)
+func (q *querier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuildRBAC, error) {
+ return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber)(ctx, arg)
}
func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
@@ -1305,11 +1273,20 @@ 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) {
- if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil {
+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.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])
+ 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) {
@@ -1369,11 +1346,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)
}
@@ -1414,12 +1387,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
}
@@ -1483,11 +1451,7 @@ func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.Upd
return database.WorkspaceBuild{}, err
}
- workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID)
- if err != nil {
- return database.WorkspaceBuild{}, err
- }
- err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace.RBACObject())
+ err = q.authorizeContext(ctx, rbac.ActionUpdate, build)
if err != nil {
return database.WorkspaceBuild{}, err
}
diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go
index d89a93319d9e0..d8be79596a6a4 100644
--- a/coderd/database/dbauthz/querier_test.go
+++ b/coderd/database/dbauthz/querier_test.go
@@ -956,16 +956,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)
@@ -979,7 +979,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*/ ).
@@ -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.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{
@@ -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.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{
@@ -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.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})
@@ -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.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})
@@ -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.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})
@@ -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.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,
@@ -1069,14 +1069,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)
@@ -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.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})
@@ -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.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{})
}))
@@ -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.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*/ ).
@@ -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) {
diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go
index e83b0b0771f9d..55176abfb7eac 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.
@@ -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 d64727f1b6227..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{
diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go
index 40e004e157527..9d2d37e5b6421 100644
--- a/coderd/database/dbfake/databasefake.go
+++ b/coderd/database/dbfake/databasefake.go
@@ -1452,66 +1452,66 @@ 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()
for _, history := range q.workspaceBuilds {
if history.ID == id {
- return history, nil
+ 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()
for _, build := range q.workspaceBuilds {
if build.JobID == jobID {
- return build, nil
+ return q.expandWorkspaceThin(build), nil
}
}
- return database.WorkspaceBuild{}, sql.ErrNoRows
+ return database.WorkspaceBuildRBAC{}, sql.ErrNoRows
}
-func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
+func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
return q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID)
}
-func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
- var row database.WorkspaceBuild
+func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) {
+ var row database.WorkspaceBuildRBAC
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
}
}
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
if workspaceBuild.BuildNumber > buildNumbers[id] {
- builds[id] = workspaceBuild
+ builds[id] = q.expandWorkspaceThin(workspaceBuild)
buildNumbers[id] = workspaceBuild.BuildNumber
}
}
- var returnBuilds []database.WorkspaceBuild
+ var returnBuilds []database.WorkspaceBuildRBAC
for i, n := range buildNumbers {
if n > 0 {
b := builds[i]
@@ -1524,21 +1524,21 @@ 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 {
if id == workspaceBuild.WorkspaceID && workspaceBuild.BuildNumber > buildNumbers[id] {
- builds[id] = workspaceBuild
+ builds[id] = q.expandWorkspaceThin(workspaceBuild)
buildNumbers[id] = workspaceBuild.BuildNumber
}
}
}
- var returnBuilds []database.WorkspaceBuild
+ var returnBuilds []database.WorkspaceBuildRBAC
for i, n := range buildNumbers {
if n > 0 {
b := builds[i]
@@ -1553,7 +1553,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
}
@@ -1561,18 +1561,18 @@ 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
}
if workspaceBuild.WorkspaceID == params.WorkspaceID {
- history = append(history, workspaceBuild)
+ history = append(history, q.expandWorkspaceThin(workspaceBuild))
}
}
// 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
})
@@ -1614,9 +1614,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()
@@ -1629,9 +1629,9 @@ 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
+ return database.WorkspaceBuildRBAC{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
@@ -1648,14 +1648,14 @@ 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, workspaceBuild)
+ workspaceBuilds = append(workspaceBuilds, q.expandWorkspaceThin(workspaceBuild))
}
}
return workspaceBuilds, nil
@@ -3250,6 +3250,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)
@@ -4618,13 +4619,13 @@ 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
}
if build.CreatedAt.After(lastBuild.CreatedAt) {
- lastBuild = build
+ lastBuild = q.expandWorkspaceThin(build)
}
}
sum += int64(lastBuild.DailyCost)
@@ -4648,3 +4649,16 @@ func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context,
}
return sql.ErrNoRows
}
+
+// expandWorkspaceThin must be called from a locked context.
+func (q *fakeQuerier) expandWorkspaceThin(thin database.WorkspaceBuild) database.WorkspaceBuildRBAC {
+ for _, workspace := range q.workspaces {
+ if workspace.ID == thin.WorkspaceID {
+ return thin.WithWorkspace(workspace)
+ }
+ }
+
+ return database.WorkspaceBuildRBAC{
+ WorkspaceBuild: thin,
+ }
+}
diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go
index 42dbb599dadcb..1c7105623aaca 100644
--- a/coderd/database/dbgen/generator.go
+++ b/coderd/database/dbgen/generator.go
@@ -153,20 +153,34 @@ 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 AnyWorkspaceBuild interface {
+ database.WorkspaceBuildRBAC | database.WorkspaceBuild
+}
+
+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
+ 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)),
+ MaxDeadline: takeFirst(thin.MaxDeadline, database.Now().Add(time.Hour*24*7)),
+ Reason: takeFirst(thin.Reason, database.BuildReasonInitiator),
})
require.NoError(t, err, "insert workspace build")
return build
@@ -219,7 +233,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/generator_test.go b/coderd/database/dbgen/generator_test.go
index c09cc6df8a466..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)))
+ 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/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/modelmethods.go b/coderd/database/modelmethods.go
index 9a5051bd7d5ac..bbc0b0815e328 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"
)
@@ -101,6 +103,12 @@ func (g Group) RBACObject() rbac.Object {
InOrg(g.OrganizationID)
}
+func (b WorkspaceBuildRBAC) 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).
@@ -183,6 +191,18 @@ func (l License) RBACObject() rbac.Object {
return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10))
}
+func (b WorkspaceBuild) WithWorkspace(workspace Workspace) WorkspaceBuildRBAC {
+ return b.Expand(workspace.OrganizationID, workspace.OwnerID)
+}
+
+func (b WorkspaceBuild) Expand(orgID, ownerID uuid.UUID) WorkspaceBuildRBAC {
+ return WorkspaceBuildRBAC{
+ WorkspaceBuild: b,
+ OrganizationID: orgID,
+ WorkspaceOwnerID: ownerID,
+ }
+}
+
func ConvertUserRows(rows []GetUsersRow) []User {
users := make([]User, len(rows))
for i, r := range rows {
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index a9bdfca75d46f..3ce2526fa029e 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -4,11 +4,13 @@ import (
"context"
"fmt"
"strings"
+ "time"
"github.com/google/uuid"
"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"
)
@@ -178,6 +180,95 @@ 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) (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)
+}
+
+// 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"`
+ WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_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) (WorkspaceBuildRBAC, error) {
+ var res WorkspaceBuildRBAC
+ arg.LimitOpt = 1
+ 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 res, sqlxqueries.SelectContext(ctx, q.db, "GetWorkspaceBuild", arg, &res)
+}
+
+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) (WorkspaceBuildRBAC, error) {
+ return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{JobID: jobID})
+}
+
+func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, after time.Time) ([]WorkspaceBuildRBAC, error) {
+ return q.selectWorkspaceBuild(ctx, getWorkspaceBuildParams{CreatedAfter: after})
+}
+
+type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct {
+ BuildNumber int32
+ WorkspaceID uuid.UUID
+}
+
+func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuildRBAC, error) {
+ return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{
+ BuildNumber: arg.BuildNumber,
+ WorkspaceID: arg.WorkspaceID,
+ })
+}
+
+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) ([]WorkspaceBuildRBAC, error) {
+ 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) {
+ return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{WorkspaceID: workspacedID, Latest: true})
+}
+
+func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuildRBAC, error) {
+ var res []WorkspaceBuildRBAC
+ return res, sqlxqueries.SelectContext(ctx, q.db, "GetLatestWorkspaceBuildsByWorkspaceIDs", ids, &res)
+}
+
+func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuildRBAC, error) {
+ 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/modelqueries_test.go b/coderd/database/modelqueries_test.go
new file mode 100644
index 0000000000000..5e85ec984cac2
--- /dev/null
+++ b/coderd/database/modelqueries_test.go
@@ -0,0 +1,189 @@
+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 (
+ now = database.Now()
+ 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.WorkspaceBuild{
+ dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
+ WorkspaceID: workspace.ID,
+ TemplateVersionID: version.ID,
+ BuildNumber: 1,
+ Transition: database.WorkspaceTransitionStart,
+ InitiatorID: user.ID,
+ JobID: jobs[0].ID,
+ Reason: database.BuildReasonInitiator,
+ CreatedAt: now,
+ }),
+ dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
+ WorkspaceID: workspace.ID,
+ TemplateVersionID: version.ID,
+ BuildNumber: 2,
+ Transition: database.WorkspaceTransitionStart,
+ InitiatorID: user.ID,
+ JobID: jobs[1].ID,
+ Reason: database.BuildReasonInitiator,
+ CreatedAt: now.Add(time.Hour),
+ }),
+ }
+ orderBuilds = []database.WorkspaceBuild{
+ builds[1],
+ builds[0],
+ }
+ 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.WorkspaceBuild, "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.WorkspaceBuild, "builds should be equal")
+ }
+ })
+
+ t.Run("GetWorkspaceBuildsCreatedAfter", func(t *testing.T) {
+ t.Parallel()
+ found, err := db.GetWorkspaceBuildsCreatedAfter(ctx, builds[0].CreatedAt.Add(time.Second))
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected := builds[1]
+ require.Len(t, found, 1, "should only be one build")
+ require.Equal(t, expected, found[0].WorkspaceBuild, "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.WorkspaceBuild, "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")
+ require.Equal(t, orderBuilds, 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, 1, "should be only one build")
+ require.Equal(t, builds[1], found[0].WorkspaceBuild, "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], found[0].WorkspaceBuild, "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.WorkspaceBuild, "builds should be equal")
+ })
+}
+
+func toThins(builds []database.WorkspaceBuildRBAC) []database.WorkspaceBuild {
+ thins := make([]database.WorkspaceBuild, len(builds))
+ for i, build := range builds {
+ thins[i] = build.WorkspaceBuild
+ }
+ return thins
+}
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 14a0f2d7053f2..1dddd7dd26146 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -66,9 +66,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)
@@ -127,12 +124,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 707e87ee6f4e7..b9443ad61cbb6 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -6039,380 +6039,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, max_deadline
-FROM
- workspace_builds
-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.MaxDeadline,
- )
- 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.max_deadline
-FROM (
- SELECT
- workspace_id, MAX(build_number) as max_build_number
- FROM
- workspace_builds
- GROUP BY
- workspace_id
-) m
-JOIN
- workspace_builds 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.MaxDeadline,
- ); 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.max_deadline
-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 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.MaxDeadline,
- ); 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, max_deadline
-FROM
- workspace_builds
-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.MaxDeadline,
- )
- 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, max_deadline
-FROM
- workspace_builds
-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.MaxDeadline,
- )
- 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, max_deadline
-FROM
- workspace_builds
-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.MaxDeadline,
- )
- 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, max_deadline
-FROM
- workspace_builds
-WHERE
- workspace_builds.workspace_id = $1
- AND workspace_builds.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.MaxDeadline,
- ); 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, max_deadline FROM workspace_builds 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.MaxDeadline,
- ); 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 b56be8f1d1de5..376dc36f31011 100644
--- a/coderd/database/queries/workspacebuilds.sql
+++ b/coderd/database/queries/workspacebuilds.sql
@@ -1,110 +1,3 @@
--- name: GetWorkspaceBuildByID :one
-SELECT
- *
-FROM
- workspace_builds
-WHERE
- id = $1
-LIMIT
- 1;
-
--- name: GetWorkspaceBuildByJobID :one
-SELECT
- *
-FROM
- workspace_builds
-WHERE
- job_id = $1
-LIMIT
- 1;
-
--- name: GetWorkspaceBuildsCreatedAfter :many
-SELECT * FROM workspace_builds WHERE created_at > $1;
-
--- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
-SELECT
- *
-FROM
- workspace_builds
-WHERE
- workspace_id = $1
- AND build_number = $2;
-
--- name: GetWorkspaceBuildsByWorkspaceID :many
-SELECT
- *
-FROM
- workspace_builds
-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);
-
--- name: GetLatestWorkspaceBuildByWorkspaceID :one
-SELECT
- *
-FROM
- workspace_builds
-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 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 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/sqlxqueries/README.md b/coderd/database/sqlxqueries/README.md
new file mode 100644
index 0000000000000..7a88fa8963d52
--- /dev/null
+++ b/coderd/database/sqlxqueries/README.md
@@ -0,0 +1,35 @@
+# Editor/IDE config
+
+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
+
+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
+ },
+```
+
+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"
+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".
+
+
+
+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.
+
+
+
+You can also add `dump.sql` as a DDL data source for proper table column recognition.
diff --git a/coderd/database/sqlxqueries/bindvars.go b/coderd/database/sqlxqueries/bindvars.go
new file mode 100644
index 0000000000000..04dc7a791da51
--- /dev/null
+++ b/coderd/database/sqlxqueries/bindvars.go
@@ -0,0 +1,95 @@
+package sqlxqueries
+
+import (
+ "fmt"
+ "reflect"
+ "regexp"
+ "strings"
+
+ "golang.org/x/xerrors"
+
+ "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")
+
+// 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)
+ 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, "@")
+ }
+
+ arglist := make([]interface{}, 0, len(names))
+
+ // This comes straight from SQLx's implementation to get the values
+ // of the struct fields.
+ var v reflect.Value
+ 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 xerrors.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/sqlxqueries/imgs/goland-gosql.png b/coderd/database/sqlxqueries/imgs/goland-gosql.png
new file mode 100644
index 0000000000000..ce66ce025376a
Binary files /dev/null and b/coderd/database/sqlxqueries/imgs/goland-gosql.png differ
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 0000000000000..594d208c4a8b0
Binary files /dev/null and b/coderd/database/sqlxqueries/imgs/goland-user-params.png differ
diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go
new file mode 100644
index 0000000000000..eae23e00dbc4c
--- /dev/null
+++ b/coderd/database/sqlxqueries/sqlx.go
@@ -0,0 +1,72 @@
+package sqlxqueries
+
+import (
+ "context"
+
+ "github.com/jmoiron/sqlx"
+
+ "golang.org/x/xerrors"
+)
+
+// 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{}{}
+ }
+
+ query, err := query(queryName, argument)
+ if err != nil {
+ return "", nil, xerrors.Errorf("get query: %w", err)
+ }
+
+ query, args, err := bindNamed(query, argument)
+ if err != nil {
+ 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(ctx context.Context, q sqlx.QueryerContext, queryName string, argument any, res any) error {
+ if q == nil {
+ return xerrors.New("queryer is nil")
+ }
+
+ query, args, err := constructQuery(queryName, argument)
+ if err != nil {
+ return xerrors.Errorf("get query: %w", err)
+ }
+
+ err = sqlx.SelectContext(ctx, q, res, query, args...)
+ if err != nil {
+ return xerrors.Errorf("%s: %w", queryName, err)
+ }
+
+ return nil
+}
+
+// GetContext runs the named query on the given database.
+// If the query returns no rows, sql.ErrNoRows is returned.
+func GetContext(ctx context.Context, q sqlx.QueryerContext, queryName string, argument interface{}, res any) error {
+ if q == nil {
+ return xerrors.New("queryer is nil")
+ }
+
+ query, args, err := constructQuery(queryName, argument)
+ if err != nil {
+ 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, res, query, args...)
+ if err != nil {
+ return xerrors.Errorf("%s: %w", queryName, err)
+ }
+
+ return nil
+}
diff --git a/coderd/database/sqlxqueries/sqlxqueries.go b/coderd/database/sqlxqueries/sqlxqueries.go
new file mode 100644
index 0000000000000..abcad453290cf
--- /dev/null
+++ b/coderd/database/sqlxqueries/sqlxqueries.go
@@ -0,0 +1,55 @@
+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
+ //nolint:errname
+ cachedError 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{
+ "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
+ })
+
+ 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, err := LoadQueries()
+ if err != nil {
+ return "", err
+ }
+
+ var out bytes.Buffer
+ err = tpls.ExecuteTemplate(&out, name, data)
+ if err != nil {
+ return "", xerrors.Errorf("execute template %s: %w", name, err)
+ }
+ return out.String(), nil
+}
diff --git a/coderd/database/sqlxqueries/sqlxqueries_test.go b/coderd/database/sqlxqueries/sqlxqueries_test.go
new file mode 100644
index 0000000000000..b273ce6fbc18b
--- /dev/null
+++ b/coderd/database/sqlxqueries/sqlxqueries_test.go
@@ -0,0 +1,15 @@
+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 := sqlxqueries.LoadQueries()
+ require.NoError(t, err)
+}
diff --git a/coderd/database/sqlxqueries/workspace.gosql b/coderd/database/sqlxqueries/workspace.gosql
new file mode 100644
index 0000000000000..acf274b8a5abd
--- /dev/null
+++ b/coderd/database/sqlxqueries/workspace.gosql
@@ -0,0 +1,128 @@
+{{ 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 "GetWorkspaceBuild" }}
+-- name: GetWorkspaceBuild :one
+SELECT
+ *
+FROM
+ {{ template "workspace_builds_rbac" }} workspace_builds
+WHERE
+ 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_after
+ 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 }}
+{{ if gt .LimitOpt 0 }} LIMIT @limit_opt {{ end }}
+;
+{{ end }}
+
+
+{{ define "GetWorkspaceBuildsByWorkspaceID" }}
+-- name: GetWorkspaceBuildsByWorkspaceID :many
+SELECT
+ *
+FROM
+ {{ template "workspace_builds_rbac" }} workspace_builds
+WHERE
+ 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.
+ -- 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 "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 }}
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/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index 71580400dbf55..de4b8bf2d3423 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -670,14 +670,14 @@ 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 {
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,
@@ -687,6 +687,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)
@@ -719,7 +721,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
@@ -735,7 +737,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,
@@ -1039,7 +1041,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
@@ -1055,7 +1057,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 7663dd6d50090..1449655d228b6 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -644,13 +644,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,
})
diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go
index 1200ddbb42464..e92296178aff8 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/workspacebuilds.go b/coderd/workspacebuilds.go
index 064d966941473..01486e9b61207 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.
@@ -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 {
@@ -923,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)
@@ -1014,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,
@@ -1075,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 44032ec915772..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()
@@ -535,7 +535,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,
@@ -551,6 +551,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))
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 |
Field | Tracked |
---|
created_at | false |
created_by | true |
git_auth_providers | false |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
-| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
+| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
max_deadline | false |
organization_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
workspace_owner_id | false |
|
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index 7e4611cc2d6c3..16ec0ee3be19b 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -126,6 +126,8 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"reason": ActionIgnore,
"daily_cost": ActionIgnore,
"max_deadline": ActionIgnore,
+ "organization_id": ActionIgnore,
+ "workspace_owner_id": ActionIgnore,
},
&database.AuditableGroup{}: {
"id": ActionTrack,