From 06deb3fa36787c6918113870f0fa78f202835a6b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 10 Mar 2023 10:37:38 -0600 Subject: [PATCH] Revert "chore: Implement joins with golang templates (#6429)" This reverts commit 8b125d6c5dbe275f2598a657f541ebfc2d64de54. --- coderd/audit/diff.go | 2 +- coderd/audit/request.go | 6 +- .../autobuild/executor/lifecycle_executor.go | 4 +- .../executor/lifecycle_executor_test.go | 7 +- coderd/database/db.go | 12 - coderd/database/dbauthz/querier.go | 86 ++-- coderd/database/dbauthz/querier_test.go | 50 ++- coderd/database/dbauthz/system.go | 4 +- coderd/database/dbauthz/system_test.go | 6 +- coderd/database/dbfake/databasefake.go | 78 ++-- coderd/database/dbgen/generator.go | 42 +- coderd/database/dbgen/generator_test.go | 4 +- coderd/database/dbgen/take.go | 7 +- coderd/database/dbtestutil/db.go | 6 +- coderd/database/modelmethods.go | 20 - coderd/database/modelqueries.go | 91 ----- coderd/database/modelqueries_test.go | 189 --------- coderd/database/querier.go | 8 + coderd/database/queries.sql.go | 374 ++++++++++++++++++ coderd/database/queries/workspacebuilds.sql | 107 +++++ coderd/database/sqlxqueries/README.md | 35 -- coderd/database/sqlxqueries/bindvars.go | 95 ----- .../sqlxqueries/imgs/goland-gosql.png | Bin 32805 -> 0 bytes .../sqlxqueries/imgs/goland-user-params.png | Bin 25338 -> 0 bytes coderd/database/sqlxqueries/sqlx.go | 72 ---- coderd/database/sqlxqueries/sqlxqueries.go | 55 --- .../database/sqlxqueries/sqlxqueries_test.go | 15 - coderd/database/sqlxqueries/workspace.gosql | 128 ------ coderd/httpmw/workspacebuildparam.go | 4 +- .../provisionerdserver/provisionerdserver.go | 14 +- .../provisionerdserver_test.go | 3 +- coderd/telemetry/telemetry.go | 2 +- coderd/workspacebuilds.go | 19 +- coderd/workspaces.go | 5 +- docs/admin/audit-logs.md | 2 +- enterprise/audit/table.go | 2 - 36 files changed, 660 insertions(+), 894 deletions(-) delete mode 100644 coderd/database/modelqueries_test.go delete mode 100644 coderd/database/sqlxqueries/README.md delete mode 100644 coderd/database/sqlxqueries/bindvars.go delete mode 100644 coderd/database/sqlxqueries/imgs/goland-gosql.png delete mode 100644 coderd/database/sqlxqueries/imgs/goland-user-params.png delete mode 100644 coderd/database/sqlxqueries/sqlx.go delete mode 100644 coderd/database/sqlxqueries/sqlxqueries.go delete mode 100644 coderd/database/sqlxqueries/sqlxqueries_test.go delete mode 100644 coderd/database/sqlxqueries/workspace.gosql diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 26134576cdf1f..1cc6702d1b06c 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.WorkspaceBuildRBAC | + database.WorkspaceBuild | database.AuditableGroup | database.License } diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 7e7690d44b5d7..8aba0ffc30adf 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.WorkspaceBuildRBAC: + case database.WorkspaceBuild: // 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.WorkspaceBuildRBAC: + case database.WorkspaceBuild: 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.WorkspaceBuildRBAC: + case database.WorkspaceBuild: 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 c818dcf6d4dd3..f6b4d0db12d87 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.WorkspaceBuildRBAC, + priorHistory database.WorkspaceBuild, 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.WorkspaceBuildRBAC, priorJob database.ProvisionerJob) error { +func build(ctx context.Context, store database.Store, workspace database.Workspace, trans database.WorkspaceTransition, priorHistory database.WorkspaceBuild, 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 7ffbecbd4c23c..2548e317b69ff 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -2,16 +2,17 @@ package executor_test import ( "context" + "os" "testing" "time" - "github.com/google/uuid" "go.uber.org/goleak" + "github.com/google/uuid" + "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" @@ -492,7 +493,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { } func TestExecutorAutostartMultipleOK(t *testing.T) { - if !dbtestutil.UsingRealDatabase() { + if os.Getenv("DB") == "" { 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 6af2c459a9b7c..f8de976e92f72 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -16,8 +16,6 @@ import ( "github.com/jmoiron/sqlx" "golang.org/x/xerrors" - - "github.com/coder/coder/coderd/database/sqlxqueries" ) // Store contains all queryable database functions. @@ -39,21 +37,11 @@ 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 f0616a2810265..052863e2edc74 100644 --- a/coderd/database/dbauthz/querier.go +++ b/coderd/database/dbauthz/querier.go @@ -1167,12 +1167,25 @@ 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.WorkspaceBuildRBAC, error) { - return fetch(q.log, q.auth, q.db.GetLatestWorkspaceBuildByWorkspaceID)(ctx, workspaceID) +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) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuildRBAC, error) { - return fetchWithPostFilter(q.auth, q.db.GetLatestWorkspaceBuildsByWorkspaceIDs)(ctx, ids) +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) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { @@ -1250,16 +1263,35 @@ 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.WorkspaceBuildRBAC, error) { - return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByID)(ctx, buildID) +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) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuildRBAC, error) { - return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByJobID)(ctx, jobID) +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) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuildRBAC, error) { - return fetch(q.log, q.auth, q.db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber)(ctx, arg) +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) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { @@ -1273,20 +1305,11 @@ 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.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 { +func (q *querier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg database.GetWorkspaceBuildsByWorkspaceIDParams) ([]database.WorkspaceBuild, error) { + if _, err := q.GetWorkspaceByID(ctx, arg.WorkspaceID); err != nil { return nil, err } - return builds, nil + return q.db.GetWorkspaceBuildsByWorkspaceID(ctx, arg) } func (q *querier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) { @@ -1346,7 +1369,11 @@ func (q *querier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.U if err != nil { return nil, err } - obj = build + workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) + if err != nil { + return nil, err + } + obj = workspace default: return nil, xerrors.Errorf("unknown job type: %s", job.Type) } @@ -1387,7 +1414,12 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa return err } - err = q.authorizeContext(ctx, rbac.ActionUpdate, build) + workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) + if err != nil { + return err + } + + err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace) if err != nil { return err } @@ -1451,7 +1483,11 @@ func (q *querier) UpdateWorkspaceBuildByID(ctx context.Context, arg database.Upd return database.WorkspaceBuild{}, err } - err = q.authorizeContext(ctx, rbac.ActionUpdate, build) + workspace, err := q.db.GetWorkspaceByID(ctx, build.WorkspaceID) + if err != nil { + return database.WorkspaceBuild{}, err + } + err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace.RBACObject()) if err != nil { return database.WorkspaceBuild{}, err } diff --git a/coderd/database/dbauthz/querier_test.go b/coderd/database/dbauthz/querier_test.go index d8be79596a6a4..d89a93319d9e0 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.WithWorkspace(ws)) + check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b) })) s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) check.Args([]uuid.UUID{ws.ID}).Asserts(ws, rbac.ActionRead).Returns(slice.New(b)) })) s.Run("GetWorkspaceAgentByID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args([]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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(database.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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) @@ -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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) 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()}).WithWorkspace(aWs) + aBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: aWs.ID, JobID: uuid.New()}) aRes := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: aBuild.JobID}) aAgt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: aRes.ID}) a := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: aAgt.ID}) bWs := dbgen.Workspace(s.T(), db, database.Workspace{}) - bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}).WithWorkspace(bWs) + bBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: bWs.ID, JobID: uuid.New()}) 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}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) check.Args(build.ID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByJobID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) check.Args(build.JobID).Asserts(ws, rbac.ActionRead).Returns(build) })) s.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 10}) 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}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 2}).WithWorkspace(ws) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, BuildNumber: 3}).WithWorkspace(ws) + _ = dbgen.WorkspaceBuild(s.T(), db, database.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}) check.Args(database.GetWorkspaceBuildsByWorkspaceIDParams{WorkspaceID: ws.ID}).Asserts(ws, rbac.ActionRead) // ordering })) s.Run("GetWorkspaceByAgentID", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) check.Args(agt.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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) _ = dbgen.ProvisionerJob(s.T(), db, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) check.Args(res.ID).Asserts(ws, rbac.ActionRead).Returns(res) })) s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) _ = 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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) 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()}).WithWorkspace(ws) + build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) 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,11 +1207,9 @@ 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, - ProvisionerState: build.ProvisionerState, - MaxDeadline: build.MaxDeadline, + ID: build.ID, + UpdatedAt: build.UpdatedAt, + Deadline: build.Deadline, }).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 55176abfb7eac..e83b0b0771f9d 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.WorkspaceBuildRBAC, error) { +func (q *querier) GetLatestWorkspaceBuilds(ctx context.Context) ([]database.WorkspaceBuild, 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.WorkspaceBuildRBAC, error) { +func (q *querier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceBuild, error) { return q.db.GetWorkspaceBuildsCreatedAfter(ctx, createdAt) } diff --git a/coderd/database/dbauthz/system_test.go b/coderd/database/dbauthz/system_test.go index 9965ae73a26c1..d64727f1b6227 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.WorkspaceBuildRBAC{}) - dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuildRBAC{}) + dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) + dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) 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.WorkspaceBuildRBAC{}) + b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) o := b o.DailyCost = 10 check.Args(database.UpdateWorkspaceBuildCostByIDParams{ diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 3273d27bf97ac..8a8eb652ddc7e 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.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() for _, history := range q.workspaceBuilds { if history.ID == id { - return q.expandWorkspaceThin(history), nil + return history, nil } } - return database.WorkspaceBuildRBAC{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() for _, build := range q.workspaceBuilds { if build.JobID == jobID { - return q.expandWorkspaceThin(build), nil + return build, nil } } - return database.WorkspaceBuildRBAC{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() return q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) } -func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuildRBAC, error) { - var row database.WorkspaceBuildRBAC +func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { + var row database.WorkspaceBuild var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { - row = q.expandWorkspaceThin(workspaceBuild) + row = workspaceBuild buildNum = workspaceBuild.BuildNumber } } if buildNum == -1 { - return database.WorkspaceBuildRBAC{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } return row, nil } -func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuildRBAC) + builds := make(map[uuid.UUID]database.WorkspaceBuild) buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { id := workspaceBuild.WorkspaceID if workspaceBuild.BuildNumber > buildNumbers[id] { - builds[id] = q.expandWorkspaceThin(workspaceBuild) + builds[id] = workspaceBuild buildNumbers[id] = workspaceBuild.BuildNumber } } - var returnBuilds []database.WorkspaceBuildRBAC + var returnBuilds []database.WorkspaceBuild 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.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuildRBAC) + builds := make(map[uuid.UUID]database.WorkspaceBuild) 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] = q.expandWorkspaceThin(workspaceBuild) + builds[id] = workspaceBuild buildNumbers[id] = workspaceBuild.BuildNumber } } } - var returnBuilds []database.WorkspaceBuildRBAC + var returnBuilds []database.WorkspaceBuild 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.WorkspaceBuildRBAC, error) { +) ([]database.WorkspaceBuild, 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.WorkspaceBuildRBAC, 0) + history := make([]database.WorkspaceBuild, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.Before(params.Since) { continue } if workspaceBuild.WorkspaceID == params.WorkspaceID { - history = append(history, q.expandWorkspaceThin(workspaceBuild)) + history = append(history, workspaceBuild) } } // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuildRBAC) bool { + slices.SortFunc(history, func(a, b database.WorkspaceBuild) 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.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuildRBAC{}, err + return database.WorkspaceBuild{}, err } q.mutex.RLock() @@ -1629,9 +1629,9 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Con if workspaceBuild.BuildNumber != arg.BuildNumber { continue } - return q.expandWorkspaceThin(workspaceBuild), nil + return workspaceBuild, nil } - return database.WorkspaceBuildRBAC{}, sql.ErrNoRows + return database.WorkspaceBuild{}, 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.WorkspaceBuildRBAC, error) { +func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() - workspaceBuilds := make([]database.WorkspaceBuildRBAC, 0) + workspaceBuilds := make([]database.WorkspaceBuild, 0) for _, workspaceBuild := range q.workspaceBuilds { if workspaceBuild.CreatedAt.After(after) { - workspaceBuilds = append(workspaceBuilds, q.expandWorkspaceThin(workspaceBuild)) + workspaceBuilds = append(workspaceBuilds, workspaceBuild) } } return workspaceBuilds, nil @@ -3250,7 +3250,6 @@ 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) @@ -4628,13 +4627,13 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI continue } - var lastBuild database.WorkspaceBuildRBAC + var lastBuild database.WorkspaceBuild for _, build := range q.workspaceBuilds { if build.WorkspaceID != workspace.ID { continue } if build.CreatedAt.After(lastBuild.CreatedAt) { - lastBuild = q.expandWorkspaceThin(build) + lastBuild = build } } sum += int64(lastBuild.DailyCost) @@ -4658,16 +4657,3 @@ 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 1c7105623aaca..42dbb599dadcb 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -153,34 +153,20 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas return workspace } -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)) - } +func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuild) database.WorkspaceBuild { build, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ - 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), + ID: takeFirst(orig.ID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, database.Now()), + UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()), + WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), + TemplateVersionID: takeFirst(orig.TemplateVersionID, uuid.New()), + BuildNumber: takeFirst(orig.BuildNumber, 1), + Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart), + InitiatorID: takeFirst(orig.InitiatorID, uuid.New()), + JobID: takeFirst(orig.JobID, uuid.New()), + ProvisionerState: takeFirstSlice(orig.ProvisionerState, []byte{}), + Deadline: takeFirst(orig.Deadline, database.Now().Add(time.Hour)), + Reason: takeFirst(orig.Reason, database.BuildReasonInitiator), }) require.NoError(t, err, "insert workspace build") return build @@ -233,7 +219,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 member") + require.NoError(t, err, "insert organization") return mem } diff --git a/coderd/database/dbgen/generator_test.go b/coderd/database/dbgen/generator_test.go index 8fc5ed3976c1b..c09cc6df8a466 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.WorkspaceBuildRBAC{}) - require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID)).WorkspaceBuild) + exp := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{}) + require.Equal(t, exp, must(db.GetWorkspaceBuildByID(context.Background(), exp.ID))) }) t.Run("User", func(t *testing.T) { diff --git a/coderd/database/dbgen/take.go b/coderd/database/dbgen/take.go index 1f0c2ae6a5f46..caa4a1b4036db 100644 --- a/coderd/database/dbgen/take.go +++ b/coderd/database/dbgen/take.go @@ -13,14 +13,9 @@ 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 { - out := takeFirstF(values, func(v []T) bool { + return 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 f048c847663bb..d6c2f106b2619 100644 --- a/coderd/database/dbtestutil/db.go +++ b/coderd/database/dbtestutil/db.go @@ -13,16 +13,12 @@ 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 UsingRealDatabase() { + if os.Getenv("DB") != "" { connectionURL := os.Getenv("CODER_PG_CONNECTION_URL") if connectionURL == "" { var ( diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index bbc0b0815e328..9a5051bd7d5ac 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -4,8 +4,6 @@ import ( "sort" "strconv" - "github.com/google/uuid" - "github.com/coder/coder/coderd/rbac" ) @@ -103,12 +101,6 @@ 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). @@ -191,18 +183,6 @@ 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 3ce2526fa029e..a9bdfca75d46f 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -4,13 +4,11 @@ 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" ) @@ -180,95 +178,6 @@ 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 deleted file mode 100644 index 5e85ec984cac2..0000000000000 --- a/coderd/database/modelqueries_test.go +++ /dev/null @@ -1,189 +0,0 @@ -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 1dddd7dd26146..14a0f2d7053f2 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -66,6 +66,9 @@ 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) @@ -124,7 +127,12 @@ 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 e3a5006b066f2..b13551978fad6 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6033,6 +6033,380 @@ 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 376dc36f31011..b56be8f1d1de5 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -1,3 +1,110 @@ +-- 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 deleted file mode 100644 index 7a88fa8963d52..0000000000000 --- a/coderd/database/sqlxqueries/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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". - -![Goland language configuration](./imgs/goland-gosql.png) - -It also helps to support the sqlc type variables. You can do this by adding ["User Parameters"](https://www.jetbrains.com/help/datagrip/settings-tools-database-user-parameters.html) in database queries. - -![Goland language configuration](./imgs/goland-user-params.png) - -You can also add `dump.sql` as a DDL data source for proper table column recognition. diff --git a/coderd/database/sqlxqueries/bindvars.go b/coderd/database/sqlxqueries/bindvars.go deleted file mode 100644 index 04dc7a791da51..0000000000000 --- a/coderd/database/sqlxqueries/bindvars.go +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index ce66ce025376ad7699677616f825ac6768e257ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32805 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 A82|tP diff --git a/coderd/database/sqlxqueries/imgs/goland-user-params.png b/coderd/database/sqlxqueries/imgs/goland-user-params.png deleted file mode 100644 index 594d208c4a8b04f95d12a7cd9a178b5e6057169c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25338 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 OE-A^W$(Bl6g!~_{uFF0E diff --git a/coderd/database/sqlxqueries/sqlx.go b/coderd/database/sqlxqueries/sqlx.go deleted file mode 100644 index eae23e00dbc4c..0000000000000 --- a/coderd/database/sqlxqueries/sqlx.go +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index abcad453290cf..0000000000000 --- a/coderd/database/sqlxqueries/sqlxqueries.go +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index b273ce6fbc18b..0000000000000 --- a/coderd/database/sqlxqueries/sqlxqueries_test.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index acf274b8a5abd..0000000000000 --- a/coderd/database/sqlxqueries/workspace.gosql +++ /dev/null @@ -1,128 +0,0 @@ -{{ 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 7ee9589768fb4..7ae728dfa6734 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.WorkspaceBuildRBAC { - workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuildRBAC) +func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild { + workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild) if !ok { panic("developer error: workspace build param middleware not provided") } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index de4b8bf2d3423..71580400dbf55 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.WorkspaceBuildRBAC + var build database.WorkspaceBuild 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) } - thinBuild, err := db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ + build, err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ ID: input.WorkspaceBuildID, UpdatedAt: database.Now(), ProvisionerState: jobType.WorkspaceBuild.State, @@ -687,8 +687,6 @@ 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) @@ -721,7 +719,7 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p BuildNumber: previousBuildNumber, }) if prevBuildErr != nil { - previousBuild = database.WorkspaceBuildRBAC{} + previousBuild = database.WorkspaceBuild{} } // We pass the below information to the Auditor so that it @@ -737,7 +735,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.WorkspaceBuildRBAC]{ + audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ Audit: *auditor, Log: server.Logger, UserID: job.InitiatorID, @@ -1041,7 +1039,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete BuildNumber: previousBuildNumber, }) if prevBuildErr != nil { - previousBuild = database.WorkspaceBuildRBAC{} + previousBuild = database.WorkspaceBuild{} } // We pass the below information to the Auditor so that it @@ -1057,7 +1055,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.WorkspaceBuildRBAC]{ + audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ Audit: *auditor, Log: server.Logger, UserID: job.InitiatorID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 1449655d228b6..7663dd6d50090 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -644,14 +644,13 @@ func TestFailJob(t *testing.T) { ID: uuid.New(), }) require.NoError(t, err) - buildThin, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + build, 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 e92296178aff8..1200ddbb42464 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.WorkspaceBuildRBAC) WorkspaceBuild { +func ConvertWorkspaceBuild(build database.WorkspaceBuild) WorkspaceBuild { return WorkspaceBuild{ ID: build.ID, CreatedAt: build.CreatedAt, diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index dae70f0c3c470..8c89a47b63754 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.WorkspaceBuildRBAC{workspaceBuild}) + data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuild{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.WorkspaceBuildRBAC + var workspaceBuilds []database.WorkspaceBuild // 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.WorkspaceBuildRBAC{workspaceBuild}) + data, err := api.workspaceBuildsData(ctx, []database.Workspace{workspace}, []database.WorkspaceBuild{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.WorkspaceBuildRBAC + var workspaceBuild database.WorkspaceBuild 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) } - thinBuild, err := db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: workspaceBuildID, CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -601,9 +601,6 @@ 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 { @@ -926,7 +923,7 @@ type workspaceBuildsData struct { apps []database.WorkspaceApp } -func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuildRBAC) (workspaceBuildsData, error) { +func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) { userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) for _, build := range workspaceBuilds { userIDs = append(userIDs, build.InitiatorID) @@ -1017,7 +1014,7 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W } func (api *API) convertWorkspaceBuilds( - workspaceBuilds []database.WorkspaceBuildRBAC, + workspaceBuilds []database.WorkspaceBuild, workspaces []database.Workspace, jobs []database.ProvisionerJob, users []database.User, @@ -1078,7 +1075,7 @@ func (api *API) convertWorkspaceBuilds( } func (api *API) convertWorkspaceBuild( - build database.WorkspaceBuildRBAC, + build database.WorkspaceBuild, workspace database.Workspace, job database.ProvisionerJob, users []database.User, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 1472f1815d6cd..f97759d0e9d7e 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -475,7 +475,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req tags := provisionerdserver.MutateTags(user.ID, templateVersionJob.Tags) var ( provisionerJob database.ProvisionerJob - workspaceBuild database.WorkspaceBuildRBAC + workspaceBuild database.WorkspaceBuild ) err = api.Database.InTx(func(db database.Store) error { now := database.Now() @@ -540,7 +540,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req if err != nil { return xerrors.Errorf("insert provisioner job: %w", err) } - workspaceBuildThin, err := db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ ID: workspaceBuildID, CreatedAt: now, UpdatedAt: now, @@ -556,7 +556,6 @@ 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 51a239b7e6cd9..3e06f307c2efd 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -19,7 +19,7 @@ We track the following resources: | TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
git_auth_providersfalse
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | Workspace
create, write, delete |
FieldTracked
autostart_scheduletrue
created_atfalse
deletedfalse
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| -| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
organization_idfalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
workspace_owner_idfalse
| +| WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 16ec0ee3be19b..7e4611cc2d6c3 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -126,8 +126,6 @@ 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,