diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 67f54ca1194df..466cedd4097d3 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -17,6 +17,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpapi/httperror" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -154,8 +155,9 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { // This can be optimized. It exists as it is now for code simplicity. // The most common case is to create a workspace for 'Me'. Which does // not enter this code branch. - template, ok := requestTemplate(ctx, rw, createReq, api.Database) - if !ok { + template, err := requestTemplate(ctx, createReq, api.Database) + if err != nil { + httperror.WriteResponseError(ctx, rw, err) return } @@ -188,7 +190,13 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { }) defer commitAudit() - createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, rw, r) + w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, r) + if err != nil { + httperror.WriteResponseError(ctx, rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, w) } // tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching @@ -464,3 +472,78 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, tasks[0]) } + +// taskDelete is an experimental endpoint to delete a task by ID (workspace ID). +// It creates a delete workspace build and returns 202 Accepted if the build was +// created. +func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + apiKey := httpmw.APIKey(r) + + idStr := chi.URLParam(r, "id") + taskID, err := uuid.Parse(idStr) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Invalid UUID %q for task ID.", idStr), + }) + return + } + + // For now, taskID = workspaceID, once we have a task data model in + // the DB, we can change this lookup. + workspaceID := taskID + workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace.", + Detail: err.Error(), + }) + return + } + + data, err := api.workspaceData(ctx, []database.Workspace{workspace}) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + if len(data.builds) == 0 || len(data.templates) == 0 { + httpapi.ResourceNotFound(rw) + return + } + if data.builds[0].HasAITask == nil || !*data.builds[0].HasAITask { + httpapi.ResourceNotFound(rw) + return + } + + // Construct a request to the workspace build creation handler to + // initiate deletion. + buildReq := codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + Reason: "Deleted via tasks API", + } + + _, err = api.postWorkspaceBuildsInternal( + ctx, + apiKey, + workspace, + buildReq, + func(action policy.Action, object rbac.Objecter) bool { + return api.Authorize(r, action, object) + }, + audit.WorkspaceBuildBaggageFromRequest(r), + ) + if err != nil { + httperror.WriteWorkspaceBuildError(ctx, rw, err) + return + } + + // Delete build created successfully. + rw.WriteHeader(http.StatusAccepted) +} diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 131238de8a5bd..802d738162854 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -3,6 +3,7 @@ package coderd_test import ( "net/http" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -265,6 +266,125 @@ func TestTasks(t *testing.T) { assert.Equal(t, workspace.ID, task.WorkspaceID.UUID, "workspace id should match") assert.NotEmpty(t, task.Status, "task status should not be empty") }) + + t.Run("Delete", func(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + template := createAITemplate(t, client, user) + + ctx := testutil.Context(t, testutil.WaitLong) + + exp := codersdk.NewExperimentalClient(client) + task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Prompt: "delete me", + }) + require.NoError(t, err) + ws, err := client.Workspace(ctx, task.ID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + + err = exp.DeleteTask(ctx, "me", task.ID) + require.NoError(t, err, "delete task request should be accepted") + + // Poll until the workspace is deleted. + for { + dws, derr := client.DeletedWorkspace(ctx, task.ID) + if derr == nil && dws.LatestBuild.Status == codersdk.WorkspaceStatusDeleted { + break + } + if ctx.Err() != nil { + require.NoError(t, derr, "expected to fetch deleted workspace before deadline") + require.Equal(t, codersdk.WorkspaceStatusDeleted, dws.LatestBuild.Status, "workspace should be deleted before deadline") + break + } + time.Sleep(testutil.IntervalMedium) + } + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + _ = coderdtest.CreateFirstUser(t, client) + + ctx := testutil.Context(t, testutil.WaitShort) + + exp := codersdk.NewExperimentalClient(client) + err := exp.DeleteTask(ctx, "me", uuid.New()) + + var sdkErr *codersdk.Error + require.Error(t, err, "expected an error for non-existent task") + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, 404, sdkErr.StatusCode()) + }) + + t.Run("NotTaskWorkspace", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + + ctx := testutil.Context(t, testutil.WaitShort) + + // Create a template without AI tasks support and a workspace from it. + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + ws := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + + exp := codersdk.NewExperimentalClient(client) + err := exp.DeleteTask(ctx, "me", ws.ID) + + var sdkErr *codersdk.Error + require.Error(t, err, "expected an error for non-task workspace delete via tasks endpoint") + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, 404, sdkErr.StatusCode()) + }) + + t.Run("UnauthorizedUserCannotDeleteOthersTask", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + + // Owner's AI-capable template and workspace (task). + template := createAITemplate(t, client, owner) + + ctx := testutil.Context(t, testutil.WaitShort) + + exp := codersdk.NewExperimentalClient(client) + task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Prompt: "delete me not", + }) + require.NoError(t, err) + ws, err := client.Workspace(ctx, task.ID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + + // Another regular org member without elevated permissions. + otherClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + expOther := codersdk.NewExperimentalClient(otherClient) + + // Attempt to delete the owner's task as a non-owner without permissions. + err = expOther.DeleteTask(ctx, "me", task.ID) + + var authErr *codersdk.Error + require.Error(t, err, "expected an authorization error when deleting another user's task") + require.ErrorAs(t, err, &authErr) + // Accept either 403 or 404 depending on authz behavior. + if authErr.StatusCode() != 403 && authErr.StatusCode() != 404 { + t.Fatalf("unexpected status code: %d (expected 403 or 404)", authErr.StatusCode()) + } + }) + }) } func TestTasksCreate(t *testing.T) { diff --git a/coderd/coderd.go b/coderd/coderd.go index 053880ce31b89..c06f44b10b40e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1015,6 +1015,7 @@ func New(options *Options) *API { r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize)) r.Get("/{id}", api.taskGet) + r.Delete("/{id}", api.taskDelete) r.Post("/", api.tasksCreate) }) }) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 68bed8f2ef5e9..40caad0818802 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2653,835 +2653,716 @@ func (s *MethodTestSuite) TestCryptoKeys() { } func (s *MethodTestSuite) TestSystemFunctions() { - s.Run("UpdateUserLinkedID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - l := dbgen.UserLink(s.T(), db, database.UserLink{UserID: u.ID}) - check.Args(database.UpdateUserLinkedIDParams{ - UserID: u.ID, - LinkedID: l.LinkedID, - LoginType: database.LoginTypeGithub, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(l) - })) - s.Run("GetLatestWorkspaceAppStatusesByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { - check.Args([]uuid.UUID{}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("UpdateUserLinkedID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + u := testutil.Fake(s.T(), faker, database.User{}) + l := testutil.Fake(s.T(), faker, database.UserLink{UserID: u.ID}) + arg := database.UpdateUserLinkedIDParams{UserID: u.ID, LinkedID: l.LinkedID, LoginType: database.LoginTypeGithub} + dbm.EXPECT().UpdateUserLinkedID(gomock.Any(), arg).Return(l, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(l) + })) + s.Run("GetLatestWorkspaceAppStatusesByWorkspaceIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New()} + dbm.EXPECT().GetLatestWorkspaceAppStatusesByWorkspaceIDs(gomock.Any(), ids).Return([]database.WorkspaceAppStatus{}, nil).AnyTimes() + check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceAppStatusesByAppIDs", s.Subtest(func(db database.Store, check *expects) { - check.Args([]uuid.UUID{}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceAppStatusesByAppIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New()} + dbm.EXPECT().GetWorkspaceAppStatusesByAppIDs(gomock.Any(), ids).Return([]database.WorkspaceAppStatus{}, nil).AnyTimes() + check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID}) - check.Args([]uuid.UUID{ws.ID}).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(slice.New(b)) + s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + wsID := uuid.New() + b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{}) + dbm.EXPECT().GetLatestWorkspaceBuildsByWorkspaceIDs(gomock.Any(), []uuid.UUID{wsID}).Return([]database.WorkspaceBuild{b}, nil).AnyTimes() + check.Args([]uuid.UUID{wsID}).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(slice.New(b)) })) - s.Run("UpsertDefaultProxy", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertDefaultProxyParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns() + s.Run("UpsertDefaultProxy", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertDefaultProxyParams{} + dbm.EXPECT().UpsertDefaultProxy(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns() })) - s.Run("GetUserLinkByLinkedID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - l := dbgen.UserLink(s.T(), db, database.UserLink{UserID: u.ID}) + s.Run("GetUserLinkByLinkedID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + l := testutil.Fake(s.T(), faker, database.UserLink{}) + dbm.EXPECT().GetUserLinkByLinkedID(gomock.Any(), l.LinkedID).Return(l, nil).AnyTimes() check.Args(l.LinkedID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(l) })) - s.Run("GetUserLinkByUserIDLoginType", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - l := dbgen.UserLink(s.T(), db, database.UserLink{}) - check.Args(database.GetUserLinkByUserIDLoginTypeParams{ - UserID: l.UserID, - LoginType: l.LoginType, - }).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(l) + s.Run("GetUserLinkByUserIDLoginType", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + l := testutil.Fake(s.T(), faker, database.UserLink{}) + arg := database.GetUserLinkByUserIDLoginTypeParams{UserID: l.UserID, LoginType: l.LoginType} + dbm.EXPECT().GetUserLinkByUserIDLoginType(gomock.Any(), arg).Return(l, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(l) })) - s.Run("GetActiveUserCount", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetActiveUserCount", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetActiveUserCount(gomock.Any(), false).Return(int64(0), nil).AnyTimes() check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) })) - s.Run("GetAuthorizationUserRoles", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) + s.Run("GetAuthorizationUserRoles", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + u := testutil.Fake(s.T(), faker, database.User{}) + dbm.EXPECT().GetAuthorizationUserRoles(gomock.Any(), u.ID).Return(database.GetAuthorizationUserRolesRow{}, nil).AnyTimes() check.Args(u.ID).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetDERPMeshKey", s.Subtest(func(db database.Store, check *expects) { - db.InsertDERPMeshKey(context.Background(), "testing") + s.Run("GetDERPMeshKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetDERPMeshKey(gomock.Any()).Return("testing", nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("InsertDERPMeshKey", s.Subtest(func(db database.Store, check *expects) { + s.Run("InsertDERPMeshKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().InsertDERPMeshKey(gomock.Any(), "value").Return(nil).AnyTimes() check.Args("value").Asserts(rbac.ResourceSystem, policy.ActionCreate).Returns() })) - s.Run("InsertDeploymentID", s.Subtest(func(db database.Store, check *expects) { + s.Run("InsertDeploymentID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().InsertDeploymentID(gomock.Any(), "value").Return(nil).AnyTimes() check.Args("value").Asserts(rbac.ResourceSystem, policy.ActionCreate).Returns() })) - s.Run("InsertReplica", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertReplicaParams{ - ID: uuid.New(), - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertReplica", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertReplicaParams{ID: uuid.New()} + dbm.EXPECT().InsertReplica(gomock.Any(), arg).Return(database.Replica{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("UpdateReplica", s.Subtest(func(db database.Store, check *expects) { - replica, err := db.InsertReplica(context.Background(), database.InsertReplicaParams{ID: uuid.New()}) - require.NoError(s.T(), err) - check.Args(database.UpdateReplicaParams{ - ID: replica.ID, - DatabaseLatency: 100, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + s.Run("UpdateReplica", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + rep := testutil.Fake(s.T(), faker, database.Replica{}) + arg := database.UpdateReplicaParams{ID: rep.ID, DatabaseLatency: 100} + dbm.EXPECT().UpdateReplica(gomock.Any(), arg).Return(rep, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("DeleteReplicasUpdatedBefore", s.Subtest(func(db database.Store, check *expects) { - _, err := db.InsertReplica(context.Background(), database.InsertReplicaParams{ID: uuid.New(), UpdatedAt: time.Now()}) - require.NoError(s.T(), err) - check.Args(time.Now().Add(time.Hour)).Asserts(rbac.ResourceSystem, policy.ActionDelete) + s.Run("DeleteReplicasUpdatedBefore", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := dbtime.Now().Add(time.Hour) + dbm.EXPECT().DeleteReplicasUpdatedBefore(gomock.Any(), t).Return(nil).AnyTimes() + check.Args(t).Asserts(rbac.ResourceSystem, policy.ActionDelete) })) - s.Run("GetReplicasUpdatedAfter", s.Subtest(func(db database.Store, check *expects) { - _, err := db.InsertReplica(context.Background(), database.InsertReplicaParams{ID: uuid.New(), UpdatedAt: time.Now()}) - require.NoError(s.T(), err) - check.Args(time.Now().Add(time.Hour*-1)).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetReplicasUpdatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := dbtime.Now().Add(-time.Hour) + dbm.EXPECT().GetReplicasUpdatedAfter(gomock.Any(), t).Return([]database.Replica{}, nil).AnyTimes() + check.Args(t).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetUserCount", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetUserCount", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetUserCount(gomock.Any(), false).Return(int64(0), nil).AnyTimes() check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0)) })) - s.Run("GetTemplates", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.Template(s.T(), db, database.Template{}) - check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("UpdateWorkspaceBuildCostByID", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{}) - o := b - o.DailyCost = 10 - check.Args(database.UpdateWorkspaceBuildCostByIDParams{ - ID: b.ID, - DailyCost: 10, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) - s.Run("UpdateWorkspaceBuildProvisionerStateByID", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) - check.Args(database.UpdateWorkspaceBuildProvisionerStateByIDParams{ - ID: build.ID, - ProvisionerState: []byte("testing"), - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) - s.Run("UpsertLastUpdateCheck", s.Subtest(func(db database.Store, check *expects) { - check.Args("value").Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) - s.Run("GetLastUpdateCheck", s.Subtest(func(db database.Store, check *expects) { - err := db.UpsertLastUpdateCheck(context.Background(), "value") - require.NoError(s.T(), err) + s.Run("GetTemplates", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetTemplates(gomock.Any()).Return([]database.Template{}, nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceBuildsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{CreatedAt: time.Now().Add(-time.Hour)}) - check.Args(time.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("GetWorkspaceAgentsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{CreatedAt: time.Now().Add(-time.Hour)}) - check.Args(time.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("UpdateWorkspaceBuildCostByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{}) + arg := database.UpdateWorkspaceBuildCostByIDParams{ID: b.ID, DailyCost: 10} + dbm.EXPECT().UpdateWorkspaceBuildCostByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetWorkspaceAppsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{CreatedAt: time.Now().Add(-time.Hour), OpenIn: database.WorkspaceAppOpenInSlimWindow}) - check.Args(time.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("UpdateWorkspaceBuildProvisionerStateByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{}) + arg := database.UpdateWorkspaceBuildProvisionerStateByIDParams{ID: b.ID, ProvisionerState: []byte("testing")} + dbm.EXPECT().UpdateWorkspaceBuildProvisionerStateByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetWorkspaceResourcesCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{CreatedAt: time.Now().Add(-time.Hour)}) - check.Args(time.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("UpsertLastUpdateCheck", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertLastUpdateCheck(gomock.Any(), "value").Return(nil).AnyTimes() + check.Args("value").Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetWorkspaceResourceMetadataCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - _ = dbgen.WorkspaceResourceMetadatums(s.T(), db, database.WorkspaceResourceMetadatum{}) - check.Args(time.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetLastUpdateCheck", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetLastUpdateCheck(gomock.Any()).Return("value", nil).AnyTimes() + check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("DeleteOldWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetWorkspaceBuildsCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetWorkspaceBuildsCreatedAfter(gomock.Any(), ts).Return([]database.WorkspaceBuild{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceAgentsCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetWorkspaceAgentsCreatedAfter(gomock.Any(), ts).Return([]database.WorkspaceAgent{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceAppsCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetWorkspaceAppsCreatedAfter(gomock.Any(), ts).Return([]database.WorkspaceApp{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceResourcesCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetWorkspaceResourcesCreatedAfter(gomock.Any(), ts).Return([]database.WorkspaceResource{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceResourceMetadataCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetWorkspaceResourceMetadataCreatedAfter(gomock.Any(), ts).Return([]database.WorkspaceResourceMetadatum{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("DeleteOldWorkspaceAgentStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().DeleteOldWorkspaceAgentStats(gomock.Any()).Return(nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionDelete) })) - s.Run("GetProvisionerJobsCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{CreatedAt: time.Now().Add(-time.Hour)}) - check.Args(time.Now()).Asserts(rbac.ResourceProvisionerJobs, policy.ActionRead) - })) - s.Run("GetTemplateVersionsByIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - t1 := dbgen.Template(s.T(), db, database.Template{}) - t2 := dbgen.Template(s.T(), db, database.Template{}) - tv1 := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}, - }) - tv2 := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, - }) - tv3 := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: t2.ID, Valid: true}, - }) - check.Args([]uuid.UUID{tv1.ID, tv2.ID, tv3.ID}). + s.Run("GetProvisionerJobsCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ts := dbtime.Now() + dbm.EXPECT().GetProvisionerJobsCreatedAfter(gomock.Any(), ts).Return([]database.ProvisionerJob{}, nil).AnyTimes() + check.Args(ts).Asserts(rbac.ResourceProvisionerJobs, policy.ActionRead) + })) + s.Run("GetTemplateVersionsByIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + tv1 := testutil.Fake(s.T(), faker, database.TemplateVersion{}) + tv2 := testutil.Fake(s.T(), faker, database.TemplateVersion{}) + tv3 := testutil.Fake(s.T(), faker, database.TemplateVersion{}) + ids := []uuid.UUID{tv1.ID, tv2.ID, tv3.ID} + dbm.EXPECT().GetTemplateVersionsByIDs(gomock.Any(), ids).Return([]database.TemplateVersion{tv1, tv2, tv3}, nil).AnyTimes() + check.Args(ids). Asserts(rbac.ResourceSystem, policy.ActionRead). Returns(slice.New(tv1, tv2, tv3)) })) - s.Run("GetParameterSchemasByJobID", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - tpl := dbgen.Template(s.T(), db, database.Template{}) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - }) - job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: tv.JobID}) - check.Args(job.ID). + s.Run("GetParameterSchemasByJobID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + tpl := testutil.Fake(s.T(), faker, database.Template{}) + v := testutil.Fake(s.T(), faker, database.TemplateVersion{TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}}) + jobID := v.JobID + dbm.EXPECT().GetTemplateVersionByJobID(gomock.Any(), jobID).Return(v, nil).AnyTimes() + dbm.EXPECT().GetTemplateByID(gomock.Any(), tpl.ID).Return(tpl, nil).AnyTimes() + dbm.EXPECT().GetParameterSchemasByJobID(gomock.Any(), jobID).Return([]database.ParameterSchema{}, nil).AnyTimes() + check.Args(jobID). Asserts(tpl, policy.ActionRead). ErrorsWithInMemDB(sql.ErrNoRows). Returns([]database.ParameterSchema{}) })) - s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - aWs := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - 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, OpenIn: database.WorkspaceAppOpenInSlimWindow}) - - bWs := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - 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, OpenIn: database.WorkspaceAppOpenInSlimWindow}) - - check.Args([]uuid.UUID{a.AgentID, b.AgentID}). + s.Run("GetWorkspaceAppsByAgentIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + a := testutil.Fake(s.T(), faker, database.WorkspaceApp{}) + b := testutil.Fake(s.T(), faker, database.WorkspaceApp{}) + ids := []uuid.UUID{a.AgentID, b.AgentID} + dbm.EXPECT().GetWorkspaceAppsByAgentIDs(gomock.Any(), ids).Return([]database.WorkspaceApp{a, b}, nil).AnyTimes() + check.Args(ids). Asserts(rbac.ResourceSystem, policy.ActionRead). Returns([]database.WorkspaceApp{a, b}) })) - s.Run("GetWorkspaceResourcesByJobIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - tpl := dbgen.Template(s.T(), db, database.Template{}) - v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, JobID: uuid.New()}) - tJob := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: v.JobID, Type: database.ProvisionerJobTypeTemplateVersionImport}) - - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) - wJob := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ID: build.JobID, Type: database.ProvisionerJobTypeWorkspaceBuild}) - check.Args([]uuid.UUID{tJob.ID, wJob.ID}). + s.Run("GetWorkspaceResourcesByJobIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New(), uuid.New()} + dbm.EXPECT().GetWorkspaceResourcesByJobIDs(gomock.Any(), ids).Return([]database.WorkspaceResource{}, nil).AnyTimes() + check.Args(ids). Asserts(rbac.ResourceSystem, policy.ActionRead). Returns([]database.WorkspaceResource{}) })) - s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) - _ = dbgen.ProvisionerJob(s.T(), db, nil, 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}) - check.Args([]uuid.UUID{a.ID, b.ID}). + s.Run("GetWorkspaceResourceMetadataByResourceIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New(), uuid.New()} + dbm.EXPECT().GetWorkspaceResourceMetadataByResourceIDs(gomock.Any(), ids).Return([]database.WorkspaceResourceMetadatum{}, nil).AnyTimes() + check.Args(ids). Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceAgentsByResourceIDs", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - 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}). + s.Run("GetWorkspaceAgentsByResourceIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + resID := uuid.New() + agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + dbm.EXPECT().GetWorkspaceAgentsByResourceIDs(gomock.Any(), []uuid.UUID{resID}).Return([]database.WorkspaceAgent{agt}, nil).AnyTimes() + check.Args([]uuid.UUID{resID}). Asserts(rbac.ResourceSystem, policy.ActionRead). Returns([]database.WorkspaceAgent{agt}) })) - s.Run("GetProvisionerJobsByIDs", s.Subtest(func(db database.Store, check *expects) { - o := dbgen.Organization(s.T(), db, database.Organization{}) - a := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{OrganizationID: o.ID}) - b := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{OrganizationID: o.ID}) - check.Args([]uuid.UUID{a.ID, b.ID}). - Asserts(rbac.ResourceProvisionerJobs.InOrg(o.ID), policy.ActionRead). + s.Run("GetProvisionerJobsByIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + org := testutil.Fake(s.T(), faker, database.Organization{}) + a := testutil.Fake(s.T(), faker, database.ProvisionerJob{OrganizationID: org.ID}) + b := testutil.Fake(s.T(), faker, database.ProvisionerJob{OrganizationID: org.ID}) + ids := []uuid.UUID{a.ID, b.ID} + dbm.EXPECT().GetProvisionerJobsByIDs(gomock.Any(), ids).Return([]database.ProvisionerJob{a, b}, nil).AnyTimes() + check.Args(ids). + Asserts(rbac.ResourceProvisionerJobs.InOrg(org.ID), policy.ActionRead). Returns(slice.New(a, b)) })) - s.Run("DeleteWorkspaceSubAgentByID", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.User(s.T(), db, database.User{}) - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) - tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID}) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID}) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - _ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID, ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID}}) + s.Run("DeleteWorkspaceSubAgentByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + ws := testutil.Fake(s.T(), faker, database.Workspace{}) + agent := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(ws, nil).AnyTimes() + dbm.EXPECT().DeleteWorkspaceSubAgentByID(gomock.Any(), agent.ID).Return(nil).AnyTimes() check.Args(agent.ID).Asserts(ws, policy.ActionDeleteAgent) })) - s.Run("GetWorkspaceAgentsByParentID", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.User(s.T(), db, database.User{}) - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) - tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID}) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID}) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - _ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID, ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID}}) - check.Args(agent.ID).Asserts(ws, policy.ActionRead) - })) - s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) - tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID}) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID}) - check.Args(database.InsertWorkspaceAgentParams{ - ID: uuid.New(), - ResourceID: res.ID, - Name: "dev", - APIKeyScope: database.AgentKeyScopeEnumAll, - }).Asserts(ws, policy.ActionCreateAgent) - })) - s.Run("UpsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) { - _ = dbgen.User(s.T(), db, database.User{}) - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) - tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID}) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID}) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID}) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - check.Args(database.UpsertWorkspaceAppParams{ - ID: uuid.New(), - AgentID: agent.ID, - Health: database.WorkspaceAppHealthDisabled, - SharingLevel: database.AppSharingLevelOwner, - OpenIn: database.WorkspaceAppOpenInSlimWindow, - }).Asserts(ws, policy.ActionUpdate) - })) - s.Run("InsertWorkspaceResourceMetadata", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceResourceMetadataParams{ - WorkspaceResourceID: uuid.New(), - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) - })) - s.Run("UpdateWorkspaceAgentConnectionByID", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID}) - agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - check.Args(database.UpdateWorkspaceAgentConnectionByIDParams{ - ID: agt.ID, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns() + s.Run("GetWorkspaceAgentsByParentID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + ws := testutil.Fake(s.T(), faker, database.Workspace{}) + parent := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + child := testutil.Fake(s.T(), faker, database.WorkspaceAgent{ParentID: uuid.NullUUID{Valid: true, UUID: parent.ID}}) + dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), parent.ID).Return(ws, nil).AnyTimes() + dbm.EXPECT().GetWorkspaceAgentsByParentID(gomock.Any(), parent.ID).Return([]database.WorkspaceAgent{child}, nil).AnyTimes() + check.Args(parent.ID).Asserts(ws, policy.ActionRead) })) - s.Run("AcquireProvisionerJob", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - StartedAt: sql.NullTime{Valid: false}, - UpdatedAt: time.Now(), - }) - check.Args(database.AcquireProvisionerJobParams{ - StartedAt: sql.NullTime{Valid: true, Time: time.Now()}, - OrganizationID: j.OrganizationID, - Types: []database.ProvisionerType{j.Provisioner}, - ProvisionerTags: must(json.Marshal(j.Tags)), - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpdateProvisionerJobWithCompleteByID", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.UpdateProvisionerJobWithCompleteByIDParams{ - ID: j.ID, - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpdateProvisionerJobWithCompleteWithStartedAtByID", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams{ - ID: j.ID, - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpdateProvisionerJobByID", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.UpdateProvisionerJobByIDParams{ - ID: j.ID, - UpdatedAt: time.Now(), - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpdateProvisionerJobLogsLength", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.UpdateProvisionerJobLogsLengthParams{ - ID: j.ID, - LogsLength: 100, - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpdateProvisionerJobLogsOverflowed", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.UpdateProvisionerJobLogsOverflowedParams{ - ID: j.ID, - LogsOverflowed: true, - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("InsertProvisionerJob", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertProvisionerJobParams{ + s.Run("InsertWorkspaceAgent", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + ws := testutil.Fake(s.T(), faker, database.Workspace{}) + res := testutil.Fake(s.T(), faker, database.WorkspaceResource{}) + arg := database.InsertWorkspaceAgentParams{ID: uuid.New(), ResourceID: res.ID, Name: "dev", APIKeyScope: database.AgentKeyScopeEnumAll} + dbm.EXPECT().GetWorkspaceByResourceID(gomock.Any(), res.ID).Return(ws, nil).AnyTimes() + dbm.EXPECT().InsertWorkspaceAgent(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.WorkspaceAgent{ResourceID: res.ID}), nil).AnyTimes() + check.Args(arg).Asserts(ws, policy.ActionCreateAgent) + })) + s.Run("UpsertWorkspaceApp", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + ws := testutil.Fake(s.T(), faker, database.Workspace{}) + agent := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + arg := database.UpsertWorkspaceAppParams{ID: uuid.New(), AgentID: agent.ID, Health: database.WorkspaceAppHealthDisabled, SharingLevel: database.AppSharingLevelOwner, OpenIn: database.WorkspaceAppOpenInSlimWindow} + dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(ws, nil).AnyTimes() + dbm.EXPECT().UpsertWorkspaceApp(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.WorkspaceApp{AgentID: agent.ID}), nil).AnyTimes() + check.Args(arg).Asserts(ws, policy.ActionUpdate) + })) + s.Run("InsertWorkspaceResourceMetadata", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceResourceMetadataParams{WorkspaceResourceID: uuid.New()} + dbm.EXPECT().InsertWorkspaceResourceMetadata(gomock.Any(), arg).Return([]database.WorkspaceResourceMetadatum{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) + s.Run("UpdateWorkspaceAgentConnectionByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + arg := database.UpdateWorkspaceAgentConnectionByIDParams{ID: agt.ID} + dbm.EXPECT().UpdateWorkspaceAgentConnectionByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns() + })) + s.Run("AcquireProvisionerJob", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + arg := database.AcquireProvisionerJobParams{StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, OrganizationID: uuid.New(), Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, ProvisionerTags: json.RawMessage("{}")} + dbm.EXPECT().AcquireProvisionerJob(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.ProvisionerJob{}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpdateProvisionerJobWithCompleteByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.UpdateProvisionerJobWithCompleteByIDParams{ID: j.ID} + dbm.EXPECT().UpdateProvisionerJobWithCompleteByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpdateProvisionerJobWithCompleteWithStartedAtByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams{ID: j.ID} + dbm.EXPECT().UpdateProvisionerJobWithCompleteWithStartedAtByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpdateProvisionerJobByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.UpdateProvisionerJobByIDParams{ID: j.ID, UpdatedAt: dbtime.Now()} + dbm.EXPECT().UpdateProvisionerJobByID(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpdateProvisionerJobLogsLength", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.UpdateProvisionerJobLogsLengthParams{ID: j.ID, LogsLength: 100} + dbm.EXPECT().UpdateProvisionerJobLogsLength(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpdateProvisionerJobLogsOverflowed", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.UpdateProvisionerJobLogsOverflowedParams{ID: j.ID, LogsOverflowed: true} + dbm.EXPECT().UpdateProvisionerJobLogsOverflowed(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("InsertProvisionerJob", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertProvisionerJobParams{ ID: uuid.New(), Provisioner: database.ProvisionerTypeEcho, StorageMethod: database.ProvisionerStorageMethodFile, Type: database.ProvisionerJobTypeWorkspaceBuild, Input: json.RawMessage("{}"), - }).Asserts( /* rbac.ResourceProvisionerJobs, policy.ActionCreate */ ) - })) - s.Run("InsertProvisionerJobLogs", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.InsertProvisionerJobLogsParams{ - JobID: j.ID, - }).Asserts( /* rbac.ResourceProvisionerJobs, policy.ActionUpdate */ ) - })) - s.Run("InsertProvisionerJobTimings", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - check.Args(database.InsertProvisionerJobTimingsParams{ - JobID: j.ID, - }).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) - })) - s.Run("UpsertProvisionerDaemon", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - org := dbgen.Organization(s.T(), db, database.Organization{}) + } + dbm.EXPECT().InsertProvisionerJob(gomock.Any(), arg).Return(testutil.Fake(s.T(), gofakeit.New(0), database.ProvisionerJob{}), nil).AnyTimes() + check.Args(arg).Asserts( /* rbac.ResourceProvisionerJobs, policy.ActionCreate */ ) + })) + s.Run("InsertProvisionerJobLogs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.InsertProvisionerJobLogsParams{JobID: j.ID} + dbm.EXPECT().InsertProvisionerJobLogs(gomock.Any(), arg).Return([]database.ProvisionerJobLog{}, nil).AnyTimes() + check.Args(arg).Asserts( /* rbac.ResourceProvisionerJobs, policy.ActionUpdate */ ) + })) + s.Run("InsertProvisionerJobTimings", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + arg := database.InsertProvisionerJobTimingsParams{JobID: j.ID} + dbm.EXPECT().InsertProvisionerJobTimings(gomock.Any(), arg).Return([]database.ProvisionerJobTiming{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionUpdate) + })) + s.Run("UpsertProvisionerDaemon", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + org := testutil.Fake(s.T(), faker, database.Organization{}) pd := rbac.ResourceProvisionerDaemon.InOrg(org.ID) - check.Args(database.UpsertProvisionerDaemonParams{ + argOrg := database.UpsertProvisionerDaemonParams{ OrganizationID: org.ID, Provisioners: []database.ProvisionerType{}, - Tags: database.StringMap(map[string]string{ - provisionersdk.TagScope: provisionersdk.ScopeOrganization, - }), - }).Asserts(pd, policy.ActionCreate) - check.Args(database.UpsertProvisionerDaemonParams{ + Tags: database.StringMap(map[string]string{provisionersdk.TagScope: provisionersdk.ScopeOrganization}), + } + dbm.EXPECT().UpsertProvisionerDaemon(gomock.Any(), argOrg).Return(testutil.Fake(s.T(), faker, database.ProvisionerDaemon{OrganizationID: org.ID}), nil).AnyTimes() + check.Args(argOrg).Asserts(pd, policy.ActionCreate) + + argUser := database.UpsertProvisionerDaemonParams{ OrganizationID: org.ID, Provisioners: []database.ProvisionerType{}, - Tags: database.StringMap(map[string]string{ - provisionersdk.TagScope: provisionersdk.ScopeUser, - provisionersdk.TagOwner: "11111111-1111-1111-1111-111111111111", - }), - }).Asserts(pd.WithOwner("11111111-1111-1111-1111-111111111111"), policy.ActionCreate) + Tags: database.StringMap(map[string]string{provisionersdk.TagScope: provisionersdk.ScopeUser, provisionersdk.TagOwner: "11111111-1111-1111-1111-111111111111"}), + } + dbm.EXPECT().UpsertProvisionerDaemon(gomock.Any(), argUser).Return(testutil.Fake(s.T(), faker, database.ProvisionerDaemon{OrganizationID: org.ID}), nil).AnyTimes() + check.Args(argUser).Asserts(pd.WithOwner("11111111-1111-1111-1111-111111111111"), policy.ActionCreate) })) - s.Run("InsertTemplateVersionParameter", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{}) - check.Args(database.InsertTemplateVersionParameterParams{ - TemplateVersionID: v.ID, - Options: json.RawMessage("{}"), - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertTemplateVersionParameter", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + v := testutil.Fake(s.T(), faker, database.TemplateVersion{}) + arg := database.InsertTemplateVersionParameterParams{TemplateVersionID: v.ID, Options: json.RawMessage("{}")} + dbm.EXPECT().InsertTemplateVersionParameter(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.TemplateVersionParameter{TemplateVersionID: v.ID}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertWorkspaceAppStatus", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertWorkspaceAppStatusParams{ - ID: uuid.New(), - State: "working", - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceAppStatus", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAppStatusParams{ID: uuid.New(), State: "working"} + dbm.EXPECT().InsertWorkspaceAppStatus(gomock.Any(), arg).Return(testutil.Fake(s.T(), gofakeit.New(0), database.WorkspaceAppStatus{ID: arg.ID, State: arg.State}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertWorkspaceResource", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertWorkspaceResourceParams{ - ID: uuid.New(), - Transition: database.WorkspaceTransitionStart, - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceResource", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceResourceParams{ID: uuid.New(), Transition: database.WorkspaceTransitionStart} + dbm.EXPECT().InsertWorkspaceResource(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.WorkspaceResource{ID: arg.ID}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("DeleteOldWorkspaceAgentLogs", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts(rbac.ResourceSystem, policy.ActionDelete) + s.Run("DeleteOldWorkspaceAgentLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().DeleteOldWorkspaceAgentLogs(gomock.Any(), t).Return(nil).AnyTimes() + check.Args(t).Asserts(rbac.ResourceSystem, policy.ActionDelete) })) - s.Run("InsertWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceAgentStatsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) + s.Run("InsertWorkspaceAgentStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentStatsParams{} + dbm.EXPECT().InsertWorkspaceAgentStats(gomock.Any(), arg).Return(xerrors.New("any error")).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) })) - s.Run("InsertWorkspaceAppStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceAppStatsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceAppStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAppStatsParams{} + dbm.EXPECT().InsertWorkspaceAppStats(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("UpsertWorkspaceAppAuditSession", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: pj.ID}) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agent.ID}) - check.Args(database.UpsertWorkspaceAppAuditSessionParams{ - AgentID: agent.ID, - AppID: app.ID, - UserID: u.ID, - Ip: "127.0.0.1", - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) - s.Run("InsertWorkspaceAgentScriptTimings", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertWorkspaceAgentScriptTimingsParams{ - ScriptID: uuid.New(), - Stage: database.WorkspaceAgentScriptTimingStageStart, - Status: database.WorkspaceAgentScriptTimingStatusOk, - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("UpsertWorkspaceAppAuditSession", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + u := testutil.Fake(s.T(), faker, database.User{}) + agent := testutil.Fake(s.T(), faker, database.WorkspaceAgent{}) + app := testutil.Fake(s.T(), faker, database.WorkspaceApp{}) + arg := database.UpsertWorkspaceAppAuditSessionParams{AgentID: agent.ID, AppID: app.ID, UserID: u.ID, Ip: "127.0.0.1"} + dbm.EXPECT().UpsertWorkspaceAppAuditSession(gomock.Any(), arg).Return(true, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("InsertWorkspaceAgentScripts", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceAgentScriptsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceAgentScriptTimings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentScriptTimingsParams{ScriptID: uuid.New(), Stage: database.WorkspaceAgentScriptTimingStageStart, Status: database.WorkspaceAgentScriptTimingStatusOk} + dbm.EXPECT().InsertWorkspaceAgentScriptTimings(gomock.Any(), arg).Return(testutil.Fake(s.T(), gofakeit.New(0), database.WorkspaceAgentScriptTiming{ScriptID: arg.ScriptID}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertWorkspaceAgentMetadata", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertWorkspaceAgentMetadataParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceAgentScripts", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentScriptsParams{} + dbm.EXPECT().InsertWorkspaceAgentScripts(gomock.Any(), arg).Return([]database.WorkspaceAgentScript{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertWorkspaceAgentLogs", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceAgentLogsParams{}).Asserts() + s.Run("InsertWorkspaceAgentMetadata", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentMetadataParams{} + dbm.EXPECT().InsertWorkspaceAgentMetadata(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertWorkspaceAgentLogSources", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertWorkspaceAgentLogSourcesParams{}).Asserts() + s.Run("InsertWorkspaceAgentLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentLogsParams{} + dbm.EXPECT().InsertWorkspaceAgentLogs(gomock.Any(), arg).Return([]database.WorkspaceAgentLog{}, nil).AnyTimes() + check.Args(arg).Asserts() })) - s.Run("GetTemplateDAUs", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateDAUsParams{}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("InsertWorkspaceAgentLogSources", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertWorkspaceAgentLogSourcesParams{} + dbm.EXPECT().InsertWorkspaceAgentLogSources(gomock.Any(), arg).Return([]database.WorkspaceAgentLogSource{}, nil).AnyTimes() + check.Args(arg).Asserts() })) - s.Run("GetActiveWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()). - Asserts(rbac.ResourceSystem, policy.ActionRead). - ErrorsWithInMemDB(sql.ErrNoRows). - Returns([]database.WorkspaceBuild{}) + s.Run("GetTemplateDAUs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.GetTemplateDAUsParams{} + dbm.EXPECT().GetTemplateDAUs(gomock.Any(), arg).Return([]database.GetTemplateDAUsRow{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetActiveWorkspaceBuildsByTemplateID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().GetActiveWorkspaceBuildsByTemplateID(gomock.Any(), id).Return([]database.WorkspaceBuild{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.WorkspaceBuild{}) })) - s.Run("GetDeploymentDAUs", s.Subtest(func(db database.Store, check *expects) { - check.Args(int32(0)).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetDeploymentDAUs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + tz := int32(0) + dbm.EXPECT().GetDeploymentDAUs(gomock.Any(), tz).Return([]database.GetDeploymentDAUsRow{}, nil).AnyTimes() + check.Args(tz).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetAppSecurityKey", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetAppSecurityKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetAppSecurityKey(gomock.Any()).Return("", sql.ErrNoRows).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead).ErrorsWithPG(sql.ErrNoRows) })) - s.Run("UpsertAppSecurityKey", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertAppSecurityKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertAppSecurityKey(gomock.Any(), "foo").Return(nil).AnyTimes() check.Args("foo").Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetApplicationName", s.Subtest(func(db database.Store, check *expects) { - db.UpsertApplicationName(context.Background(), "foo") + s.Run("GetApplicationName", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetApplicationName(gomock.Any()).Return("foo", nil).AnyTimes() check.Args().Asserts() })) - s.Run("UpsertApplicationName", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertApplicationName", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertApplicationName(gomock.Any(), "").Return(nil).AnyTimes() check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("GetHealthSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetHealthSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetHealthSettings(gomock.Any()).Return("{}", nil).AnyTimes() check.Args().Asserts() })) - s.Run("UpsertHealthSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertHealthSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertHealthSettings(gomock.Any(), "foo").Return(nil).AnyTimes() check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("GetNotificationsSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetNotificationsSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetNotificationsSettings(gomock.Any()).Return("{}", nil).AnyTimes() check.Args().Asserts() })) - s.Run("UpsertNotificationsSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertNotificationsSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertNotificationsSettings(gomock.Any(), "foo").Return(nil).AnyTimes() check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("GetDeploymentWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() + s.Run("GetDeploymentWorkspaceAgentStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetDeploymentWorkspaceAgentStats(gomock.Any(), t).Return(database.GetDeploymentWorkspaceAgentStatsRow{}, nil).AnyTimes() + check.Args(t).Asserts() })) - s.Run("GetDeploymentWorkspaceAgentUsageStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() + s.Run("GetDeploymentWorkspaceAgentUsageStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetDeploymentWorkspaceAgentUsageStats(gomock.Any(), t).Return(database.GetDeploymentWorkspaceAgentUsageStatsRow{}, nil).AnyTimes() + check.Args(t).Asserts() })) - s.Run("GetDeploymentWorkspaceStats", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetDeploymentWorkspaceStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetDeploymentWorkspaceStats(gomock.Any()).Return(database.GetDeploymentWorkspaceStatsRow{}, nil).AnyTimes() check.Args().Asserts() })) - s.Run("GetFileTemplates", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetFileTemplates", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().GetFileTemplates(gomock.Any(), id).Return([]database.GetFileTemplatesRow{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetProvisionerJobsToBeReaped", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetProvisionerJobsToBeReapedParams{}).Asserts(rbac.ResourceProvisionerJobs, policy.ActionRead) + s.Run("GetProvisionerJobsToBeReaped", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.GetProvisionerJobsToBeReapedParams{} + dbm.EXPECT().GetProvisionerJobsToBeReaped(gomock.Any(), arg).Return([]database.ProvisionerJob{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceProvisionerJobs, policy.ActionRead) })) - s.Run("UpsertOAuthSigningKey", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertOAuthSigningKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertOAuthSigningKey(gomock.Any(), "foo").Return(nil).AnyTimes() check.Args("foo").Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetOAuthSigningKey", s.Subtest(func(db database.Store, check *expects) { - db.UpsertOAuthSigningKey(context.Background(), "foo") + s.Run("GetOAuthSigningKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetOAuthSigningKey(gomock.Any()).Return("foo", nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("UpsertCoordinatorResumeTokenSigningKey", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertCoordinatorResumeTokenSigningKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertCoordinatorResumeTokenSigningKey(gomock.Any(), "foo").Return(nil).AnyTimes() check.Args("foo").Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetCoordinatorResumeTokenSigningKey", s.Subtest(func(db database.Store, check *expects) { - db.UpsertCoordinatorResumeTokenSigningKey(context.Background(), "foo") + s.Run("GetCoordinatorResumeTokenSigningKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetCoordinatorResumeTokenSigningKey(gomock.Any()).Return("foo", nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("InsertMissingGroups", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertMissingGroupsParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) - })) - s.Run("UpdateUserLoginType", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - check.Args(database.UpdateUserLoginTypeParams{ - NewLoginType: database.LoginTypePassword, - UserID: u.ID, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) - s.Run("GetWorkspaceAgentStatsAndLabels", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() - })) - s.Run("GetWorkspaceAgentUsageStatsAndLabels", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() - })) - s.Run("GetWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() - })) - s.Run("GetWorkspaceAgentUsageStats", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() - })) - s.Run("GetWorkspaceProxyByHostname", s.Subtest(func(db database.Store, check *expects) { - p, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{ - WildcardHostname: "*.example.com", - }) - check.Args(database.GetWorkspaceProxyByHostnameParams{ - Hostname: "foo.example.com", - AllowWildcardHostname: true, - }).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(p) - })) - s.Run("GetTemplateAverageBuildTime", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetTemplateAverageBuildTimeParams{}).Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("GetWorkspacesByTemplateID", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.Nil).Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("GetWorkspacesEligibleForTransition", s.Subtest(func(db database.Store, check *expects) { - check.Args(time.Time{}).Asserts() + s.Run("InsertMissingGroups", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertMissingGroupsParams{} + dbm.EXPECT().InsertMissingGroups(gomock.Any(), arg).Return([]database.Group{}, xerrors.New("any error")).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate).Errors(errMatchAny) })) - s.Run("InsertTemplateVersionVariable", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertTemplateVersionVariableParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("UpdateUserLoginType", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + u := testutil.Fake(s.T(), faker, database.User{}) + arg := database.UpdateUserLoginTypeParams{NewLoginType: database.LoginTypePassword, UserID: u.ID} + dbm.EXPECT().UpdateUserLoginType(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.User{}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) + s.Run("GetWorkspaceAgentStatsAndLabels", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetWorkspaceAgentStatsAndLabels(gomock.Any(), t).Return([]database.GetWorkspaceAgentStatsAndLabelsRow{}, nil).AnyTimes() + check.Args(t).Asserts() + })) + s.Run("GetWorkspaceAgentUsageStatsAndLabels", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetWorkspaceAgentUsageStatsAndLabels(gomock.Any(), t).Return([]database.GetWorkspaceAgentUsageStatsAndLabelsRow{}, nil).AnyTimes() + check.Args(t).Asserts() + })) + s.Run("GetWorkspaceAgentStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetWorkspaceAgentStats(gomock.Any(), t).Return([]database.GetWorkspaceAgentStatsRow{}, nil).AnyTimes() + check.Args(t).Asserts() + })) + s.Run("GetWorkspaceAgentUsageStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetWorkspaceAgentUsageStats(gomock.Any(), t).Return([]database.GetWorkspaceAgentUsageStatsRow{}, nil).AnyTimes() + check.Args(t).Asserts() + })) + s.Run("GetWorkspaceProxyByHostname", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + p := testutil.Fake(s.T(), faker, database.WorkspaceProxy{WildcardHostname: "*.example.com"}) + arg := database.GetWorkspaceProxyByHostnameParams{Hostname: "foo.example.com", AllowWildcardHostname: true} + dbm.EXPECT().GetWorkspaceProxyByHostname(gomock.Any(), arg).Return(p, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(p) + })) + s.Run("GetTemplateAverageBuildTime", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.GetTemplateAverageBuildTimeParams{} + dbm.EXPECT().GetTemplateAverageBuildTime(gomock.Any(), arg).Return(database.GetTemplateAverageBuildTimeRow{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspacesByTemplateID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.Nil + dbm.EXPECT().GetWorkspacesByTemplateID(gomock.Any(), id).Return([]database.WorkspaceTable{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspacesEligibleForTransition", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + t := time.Time{} + dbm.EXPECT().GetWorkspacesEligibleForTransition(gomock.Any(), t).Return([]database.GetWorkspacesEligibleForTransitionRow{}, nil).AnyTimes() + check.Args(t).Asserts() + })) + s.Run("InsertTemplateVersionVariable", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertTemplateVersionVariableParams{} + dbm.EXPECT().InsertTemplateVersionVariable(gomock.Any(), arg).Return(testutil.Fake(s.T(), gofakeit.New(0), database.TemplateVersionVariable{}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("InsertTemplateVersionWorkspaceTag", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertTemplateVersionWorkspaceTagParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertTemplateVersionWorkspaceTag", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertTemplateVersionWorkspaceTagParams{} + dbm.EXPECT().InsertTemplateVersionWorkspaceTag(gomock.Any(), arg).Return(testutil.Fake(s.T(), gofakeit.New(0), database.TemplateVersionWorkspaceTag{}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("UpdateInactiveUsersToDormant", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpdateInactiveUsersToDormantParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate). - ErrorsWithInMemDB(sql.ErrNoRows). - Returns([]database.UpdateInactiveUsersToDormantRow{}) + s.Run("UpdateInactiveUsersToDormant", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpdateInactiveUsersToDormantParams{} + dbm.EXPECT().UpdateInactiveUsersToDormant(gomock.Any(), arg).Return([]database.UpdateInactiveUsersToDormantRow{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate).Returns([]database.UpdateInactiveUsersToDormantRow{}) })) - s.Run("GetWorkspaceUniqueOwnerCountByTemplateIDs", s.Subtest(func(db database.Store, check *expects) { - check.Args([]uuid.UUID{uuid.New()}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceUniqueOwnerCountByTemplateIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New()} + dbm.EXPECT().GetWorkspaceUniqueOwnerCountByTemplateIDs(gomock.Any(), ids).Return([]database.GetWorkspaceUniqueOwnerCountByTemplateIDsRow{}, nil).AnyTimes() + check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceAgentScriptsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { - check.Args([]uuid.UUID{uuid.New()}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceAgentScriptsByAgentIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New()} + dbm.EXPECT().GetWorkspaceAgentScriptsByAgentIDs(gomock.Any(), ids).Return([]database.WorkspaceAgentScript{}, nil).AnyTimes() + check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceAgentLogSourcesByAgentIDs", s.Subtest(func(db database.Store, check *expects) { - check.Args([]uuid.UUID{uuid.New()}).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceAgentLogSourcesByAgentIDs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + ids := []uuid.UUID{uuid.New()} + dbm.EXPECT().GetWorkspaceAgentLogSourcesByAgentIDs(gomock.Any(), ids).Return([]database.WorkspaceAgentLogSource{}, nil).AnyTimes() + check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetProvisionerJobsByIDsWithQueuePosition", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetProvisionerJobsByIDsWithQueuePositionParams{}).Asserts() + s.Run("GetProvisionerJobsByIDsWithQueuePosition", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.GetProvisionerJobsByIDsWithQueuePositionParams{} + dbm.EXPECT().GetProvisionerJobsByIDsWithQueuePosition(gomock.Any(), arg).Return([]database.GetProvisionerJobsByIDsWithQueuePositionRow{}, nil).AnyTimes() + check.Args(arg).Asserts() })) - s.Run("GetReplicaByID", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) + s.Run("GetReplicaByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().GetReplicaByID(gomock.Any(), id).Return(database.Replica{}, sql.ErrNoRows).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) })) - s.Run("GetWorkspaceAgentAndLatestBuildByAuthToken", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) + s.Run("GetWorkspaceAgentAndLatestBuildByAuthToken", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + tok := uuid.New() + dbm.EXPECT().GetWorkspaceAgentAndLatestBuildByAuthToken(gomock.Any(), tok).Return(database.GetWorkspaceAgentAndLatestBuildByAuthTokenRow{}, sql.ErrNoRows).AnyTimes() + check.Args(tok).Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) })) - s.Run("GetUserLinksByUserID", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetUserLinksByUserID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().GetUserLinksByUserID(gomock.Any(), id).Return([]database.UserLink{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("DeleteRuntimeConfig", s.Subtest(func(db database.Store, check *expects) { + s.Run("DeleteRuntimeConfig", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().DeleteRuntimeConfig(gomock.Any(), "test").Return(nil).AnyTimes() check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionDelete) })) - s.Run("GetRuntimeConfig", s.Subtest(func(db database.Store, check *expects) { - _ = db.UpsertRuntimeConfig(context.Background(), database.UpsertRuntimeConfigParams{ - Key: "test", - Value: "value", - }) + s.Run("GetRuntimeConfig", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetRuntimeConfig(gomock.Any(), "test").Return("value", nil).AnyTimes() check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("UpsertRuntimeConfig", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertRuntimeConfigParams{ - Key: "test", - Value: "value", - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) - })) - s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.GetFailedWorkspaceBuildsByTemplateIDParams{ - TemplateID: uuid.New(), - Since: dbtime.Now(), - }).Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("GetNotificationReportGeneratorLogByTemplate", s.Subtest(func(db database.Store, check *expects) { - _ = db.UpsertNotificationReportGeneratorLog(context.Background(), database.UpsertNotificationReportGeneratorLogParams{ - NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, - LastGeneratedAt: dbtime.Now(), - }) - check.Args(notifications.TemplateWorkspaceBuildsFailedReport).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("UpsertRuntimeConfig", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertRuntimeConfigParams{Key: "test", Value: "value"} + dbm.EXPECT().UpsertRuntimeConfig(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("GetWorkspaceBuildStatsByTemplates", s.Subtest(func(db database.Store, check *expects) { - check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetFailedWorkspaceBuildsByTemplateID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.GetFailedWorkspaceBuildsByTemplateIDParams{TemplateID: uuid.New(), Since: dbtime.Now()} + dbm.EXPECT().GetFailedWorkspaceBuildsByTemplateID(gomock.Any(), arg).Return([]database.GetFailedWorkspaceBuildsByTemplateIDRow{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("UpsertNotificationReportGeneratorLog", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertNotificationReportGeneratorLogParams{ - NotificationTemplateID: uuid.New(), - LastGeneratedAt: dbtime.Now(), - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("GetNotificationReportGeneratorLogByTemplate", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetNotificationReportGeneratorLogByTemplate(gomock.Any(), notifications.TemplateWorkspaceBuildsFailedReport).Return(database.NotificationReportGeneratorLog{}, nil).AnyTimes() + check.Args(notifications.TemplateWorkspaceBuildsFailedReport).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetProvisionerJobTimingsByJobID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: u.ID, - }) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - OrganizationID: org.ID, - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - CreatedBy: u.ID, - }) - w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: u.ID, - OrganizationID: org.ID, - TemplateID: tpl.ID, - }) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - }) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: j.ID, WorkspaceID: w.ID, TemplateVersionID: tv.ID}) - t := dbgen.ProvisionerJobTimings(s.T(), db, b, 2) - check.Args(j.ID).Asserts(w, policy.ActionRead).Returns(t) + s.Run("GetWorkspaceBuildStatsByTemplates", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + at := dbtime.Now() + dbm.EXPECT().GetWorkspaceBuildStatsByTemplates(gomock.Any(), at).Return([]database.GetWorkspaceBuildStatsByTemplatesRow{}, nil).AnyTimes() + check.Args(at).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceAgentScriptTimingsByBuildID", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) - job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - }) - build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{JobID: job.ID, WorkspaceID: workspace.ID}) - resource := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{ - JobID: build.JobID, - }) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ - ResourceID: resource.ID, - }) - script := dbgen.WorkspaceAgentScript(s.T(), db, database.WorkspaceAgentScript{ - WorkspaceAgentID: agent.ID, - }) - timing := dbgen.WorkspaceAgentScriptTiming(s.T(), db, database.WorkspaceAgentScriptTiming{ - ScriptID: script.ID, - }) - rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ - { - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Stage: timing.Stage, - ScriptID: timing.ScriptID, - ExitCode: timing.ExitCode, - Status: timing.Status, - DisplayName: script.DisplayName, - WorkspaceAgentID: agent.ID, - WorkspaceAgentName: agent.Name, - }, - } - check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) + s.Run("UpsertNotificationReportGeneratorLog", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertNotificationReportGeneratorLogParams{NotificationTemplateID: uuid.New(), LastGeneratedAt: dbtime.Now()} + dbm.EXPECT().UpsertNotificationReportGeneratorLog(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("DisableForeignKeysAndTriggers", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetProvisionerJobTimingsByJobID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) + b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{JobID: j.ID}) + ws := testutil.Fake(s.T(), faker, database.Workspace{ID: b.WorkspaceID}) + dbm.EXPECT().GetProvisionerJobByID(gomock.Any(), j.ID).Return(j, nil).AnyTimes() + dbm.EXPECT().GetWorkspaceBuildByJobID(gomock.Any(), j.ID).Return(b, nil).AnyTimes() + dbm.EXPECT().GetWorkspaceByID(gomock.Any(), b.WorkspaceID).Return(ws, nil).AnyTimes() + dbm.EXPECT().GetProvisionerJobTimingsByJobID(gomock.Any(), j.ID).Return([]database.ProvisionerJobTiming{}, nil).AnyTimes() + check.Args(j.ID).Asserts(ws, policy.ActionRead) + })) + s.Run("GetWorkspaceAgentScriptTimingsByBuildID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + build := testutil.Fake(s.T(), faker, database.WorkspaceBuild{}) + dbm.EXPECT().GetWorkspaceAgentScriptTimingsByBuildID(gomock.Any(), build.ID).Return([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow{}, nil).AnyTimes() + check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.GetWorkspaceAgentScriptTimingsByBuildIDRow{}) + })) + s.Run("DisableForeignKeysAndTriggers", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().DisableForeignKeysAndTriggers(gomock.Any()).Return(nil).AnyTimes() check.Args().Asserts() })) - s.Run("InsertWorkspaceModule", s.Subtest(func(db database.Store, check *expects) { - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - }) - check.Args(database.InsertWorkspaceModuleParams{ - JobID: j.ID, - Transition: database.WorkspaceTransitionStart, - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertWorkspaceModule", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) + arg := database.InsertWorkspaceModuleParams{JobID: j.ID, Transition: database.WorkspaceTransitionStart} + dbm.EXPECT().InsertWorkspaceModule(gomock.Any(), arg).Return(testutil.Fake(s.T(), faker, database.WorkspaceModule{JobID: j.ID}), nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("GetWorkspaceModulesByJobID", s.Subtest(func(db database.Store, check *expects) { - check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceModulesByJobID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + id := uuid.New() + dbm.EXPECT().GetWorkspaceModulesByJobID(gomock.Any(), id).Return([]database.WorkspaceModule{}, nil).AnyTimes() + check.Args(id).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetWorkspaceModulesCreatedAfter", s.Subtest(func(db database.Store, check *expects) { - check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) + s.Run("GetWorkspaceModulesCreatedAfter", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + at := dbtime.Now() + dbm.EXPECT().GetWorkspaceModulesCreatedAfter(gomock.Any(), at).Return([]database.WorkspaceModule{}, nil).AnyTimes() + check.Args(at).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetTelemetryItem", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetTelemetryItem", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetTelemetryItem(gomock.Any(), "test").Return(database.TelemetryItem{}, sql.ErrNoRows).AnyTimes() check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) })) - s.Run("GetTelemetryItems", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetTelemetryItems", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetTelemetryItems(gomock.Any()).Return([]database.TelemetryItem{}, nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("InsertTelemetryItemIfNotExists", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.InsertTelemetryItemIfNotExistsParams{ - Key: "test", - Value: "value", - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + s.Run("InsertTelemetryItemIfNotExists", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.InsertTelemetryItemIfNotExistsParams{Key: "test", Value: "value"} + dbm.EXPECT().InsertTelemetryItemIfNotExists(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) - s.Run("UpsertTelemetryItem", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertTelemetryItemParams{ - Key: "test", - Value: "value", - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + s.Run("UpsertTelemetryItem", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertTelemetryItemParams{Key: "test", Value: "value"} + dbm.EXPECT().UpsertTelemetryItem(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate) })) - s.Run("GetOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetOAuth2GithubDefaultEligible", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetOAuth2GithubDefaultEligible(gomock.Any()).Return(false, sql.ErrNoRows).AnyTimes() check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Errors(sql.ErrNoRows) })) - s.Run("UpsertOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertOAuth2GithubDefaultEligible", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().UpsertOAuth2GithubDefaultEligible(gomock.Any(), true).Return(nil).AnyTimes() check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("GetWebpushVAPIDKeys", s.Subtest(func(db database.Store, check *expects) { - require.NoError(s.T(), db.UpsertWebpushVAPIDKeys(context.Background(), database.UpsertWebpushVAPIDKeysParams{ - VapidPublicKey: "test", - VapidPrivateKey: "test", - })) - check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(database.GetWebpushVAPIDKeysRow{ - VapidPublicKey: "test", - VapidPrivateKey: "test", - }) + s.Run("GetWebpushVAPIDKeys", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetWebpushVAPIDKeys(gomock.Any()).Return(database.GetWebpushVAPIDKeysRow{VapidPublicKey: "test", VapidPrivateKey: "test"}, nil).AnyTimes() + check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(database.GetWebpushVAPIDKeysRow{VapidPublicKey: "test", VapidPrivateKey: "test"}) })) - s.Run("UpsertWebpushVAPIDKeys", s.Subtest(func(db database.Store, check *expects) { - check.Args(database.UpsertWebpushVAPIDKeysParams{ - VapidPublicKey: "test", - VapidPrivateKey: "test", - }).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) + s.Run("UpsertWebpushVAPIDKeys", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertWebpushVAPIDKeysParams{VapidPublicKey: "test", VapidPrivateKey: "test"} + dbm.EXPECT().UpsertWebpushVAPIDKeys(gomock.Any(), arg).Return(nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) - s.Run("Build/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: u.ID, - OrganizationID: o.ID, - TemplateID: tpl.ID, - }) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - }) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - _ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ - JobID: j.ID, - WorkspaceID: w.ID, - TemplateVersionID: tv.ID, - }) + s.Run("Build/GetProvisionerJobByIDForUpdate", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild}) + dbm.EXPECT().GetProvisionerJobByIDForUpdate(gomock.Any(), j.ID).Return(j, nil).AnyTimes() + // Minimal assertion check argument + b := testutil.Fake(s.T(), faker, database.WorkspaceBuild{JobID: j.ID}) + w := testutil.Fake(s.T(), faker, database.Workspace{ID: b.WorkspaceID}) + dbm.EXPECT().GetWorkspaceBuildByJobID(gomock.Any(), j.ID).Return(b, nil).AnyTimes() + dbm.EXPECT().GetWorkspaceByID(gomock.Any(), b.WorkspaceID).Return(w, nil).AnyTimes() check.Args(j.ID).Asserts(w, policy.ActionRead).Returns(j) })) - s.Run("TemplateVersion/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeTemplateVersionImport, - }) - tpl := dbgen.Template(s.T(), db, database.Template{}) - v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - JobID: j.ID, - }) - check.Args(j.ID).Asserts(v.RBACObject(tpl), policy.ActionRead).Returns(j) + s.Run("TemplateVersion/GetProvisionerJobByIDForUpdate", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{Type: database.ProvisionerJobTypeTemplateVersionImport}) + tpl := testutil.Fake(s.T(), faker, database.Template{}) + tv := testutil.Fake(s.T(), faker, database.TemplateVersion{TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}}) + dbm.EXPECT().GetProvisionerJobByIDForUpdate(gomock.Any(), j.ID).Return(j, nil).AnyTimes() + dbm.EXPECT().GetTemplateVersionByJobID(gomock.Any(), j.ID).Return(tv, nil).AnyTimes() + dbm.EXPECT().GetTemplateByID(gomock.Any(), tpl.ID).Return(tpl, nil).AnyTimes() + check.Args(j.ID).Asserts(tv.RBACObject(tpl), policy.ActionRead).Returns(j) })) - s.Run("TemplateVersionDryRun/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - tpl := dbgen.Template(s.T(), db, database.Template{}) - v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - }) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeTemplateVersionDryRun, - Input: must(json.Marshal(struct { - TemplateVersionID uuid.UUID `json:"template_version_id"` - }{TemplateVersionID: v.ID})), - }) - check.Args(j.ID).Asserts(v.RBACObject(tpl), policy.ActionRead).Returns(j) + s.Run("TemplateVersionDryRun/GetProvisionerJobByIDForUpdate", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + tpl := testutil.Fake(s.T(), faker, database.Template{}) + tv := testutil.Fake(s.T(), faker, database.TemplateVersion{TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}}) + j := testutil.Fake(s.T(), faker, database.ProvisionerJob{}) + j.Type = database.ProvisionerJobTypeTemplateVersionDryRun + j.Input = must(json.Marshal(struct { + TemplateVersionID uuid.UUID `json:"template_version_id"` + }{TemplateVersionID: tv.ID})) + dbm.EXPECT().GetProvisionerJobByIDForUpdate(gomock.Any(), j.ID).Return(j, nil).AnyTimes() + dbm.EXPECT().GetTemplateVersionByID(gomock.Any(), tv.ID).Return(tv, nil).AnyTimes() + dbm.EXPECT().GetTemplateByID(gomock.Any(), tpl.ID).Return(tpl, nil).AnyTimes() + check.Args(j.ID).Asserts(tv.RBACObject(tpl), policy.ActionRead).Returns(j) })) } diff --git a/coderd/httpapi/httperror/responserror.go b/coderd/httpapi/httperror/responserror.go index be219f538bcf7..000089b6d0bd5 100644 --- a/coderd/httpapi/httperror/responserror.go +++ b/coderd/httpapi/httperror/responserror.go @@ -1,8 +1,12 @@ package httperror import ( + "context" "errors" + "fmt" + "net/http" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" ) @@ -17,3 +21,48 @@ func IsResponder(err error) (Responder, bool) { } return nil, false } + +func NewResponseError(status int, resp codersdk.Response) error { + return &responseError{ + status: status, + response: resp, + } +} + +func WriteResponseError(ctx context.Context, rw http.ResponseWriter, err error) { + if responseErr, ok := IsResponder(err); ok { + code, resp := responseErr.Response() + + httpapi.Write(ctx, rw, code, resp) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal server error", + Detail: err.Error(), + }) +} + +type responseError struct { + status int + response codersdk.Response +} + +var ( + _ error = (*responseError)(nil) + _ Responder = (*responseError)(nil) +) + +func (e *responseError) Error() string { + return fmt.Sprintf("%s: %s", e.response.Message, e.response.Detail) +} + +func (e *responseError) Status() int { + return e.status +} + +func (e *responseError) Response() (int, codersdk.Response) { + return e.status, e.response +} + +var ErrResourceNotFound = NewResponseError(http.StatusNotFound, httpapi.ResourceNotFoundResponse) diff --git a/coderd/provisionerdserver/metrics.go b/coderd/provisionerdserver/metrics.go index 67bd997055e1a..b1afc10670f22 100644 --- a/coderd/provisionerdserver/metrics.go +++ b/coderd/provisionerdserver/metrics.go @@ -100,25 +100,14 @@ func (m *Metrics) Register(reg prometheus.Registerer) error { return reg.Register(m.workspaceClaimTimings) } -func (f WorkspaceTimingFlags) count() int { - count := 0 - if f.IsPrebuild { - count++ - } - if f.IsClaim { - count++ - } - if f.IsFirstBuild { - count++ - } - return count -} - -// getWorkspaceTimingType returns the type of the workspace build: -// - isPrebuild: if the workspace build corresponds to the creation of a prebuilt workspace -// - isClaim: if the workspace build corresponds to the claim of a prebuilt workspace -// - isWorkspaceFirstBuild: if the workspace build corresponds to the creation of a regular workspace -// (not created from the prebuild pool) +// getWorkspaceTimingType classifies a workspace build: +// - PrebuildCreation: creation of a prebuilt workspace +// - PrebuildClaim: claim of an existing prebuilt workspace +// - WorkspaceCreation: first build of a regular (non-prebuilt) workspace +// +// Note: order matters. Creating a prebuilt workspace is also a first build +// (IsPrebuild && IsFirstBuild). We check IsPrebuild before IsFirstBuild so +// prebuilds take precedence. This is the only case where two flags can be true. func getWorkspaceTimingType(flags WorkspaceTimingFlags) WorkspaceTimingType { switch { case flags.IsPrebuild: @@ -149,14 +138,6 @@ func (m *Metrics) UpdateWorkspaceTimingsMetrics( "isClaim", flags.IsClaim, "isWorkspaceFirstBuild", flags.IsFirstBuild) - if flags.count() > 1 { - m.logger.Warn(ctx, "invalid workspace timing flags", - "isPrebuild", flags.IsPrebuild, - "isClaim", flags.IsClaim, - "isWorkspaceFirstBuild", flags.IsFirstBuild) - return - } - workspaceTimingType := getWorkspaceTimingType(flags) switch workspaceTimingType { case WorkspaceCreation: diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index e54f75ef5cba6..2fdb40a1e4661 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -329,13 +329,44 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) - workspace := httpmw.WorkspaceParam(r) var createBuild codersdk.CreateWorkspaceBuildRequest if !httpapi.Read(ctx, rw, r, &createBuild) { return } + apiBuild, err := api.postWorkspaceBuildsInternal( + ctx, + apiKey, + workspace, + createBuild, + func(action policy.Action, object rbac.Objecter) bool { + return api.Authorize(r, action, object) + }, + audit.WorkspaceBuildBaggageFromRequest(r), + ) + if err != nil { + httperror.WriteWorkspaceBuildError(ctx, rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) +} + +// postWorkspaceBuildsInternal handles the internal logic for creating +// workspace builds, can be called by other handlers and must not +// reference httpmw. +func (api *API) postWorkspaceBuildsInternal( + ctx context.Context, + apiKey database.APIKey, + workspace database.Workspace, + createBuild codersdk.CreateWorkspaceBuildRequest, + authorize func(action policy.Action, object rbac.Objecter) bool, + workspaceBuildBaggage audit.WorkspaceBuildBaggage, +) ( + codersdk.WorkspaceBuild, + error, +) { transition := database.WorkspaceTransition(createBuild.Transition) builder := wsbuilder.New(workspace, transition, *api.BuildUsageChecker.Load()). Initiator(apiKey.UserID). @@ -362,11 +393,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { previousWorkspaceBuild, err = tx.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err)) - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching previous workspace build", Detail: err.Error(), }) - return nil } if createBuild.TemplateVersionID != uuid.Nil { @@ -375,16 +405,14 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { if createBuild.Orphan { if createBuild.Transition != codersdk.WorkspaceTransitionDelete { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Orphan is only permitted when deleting a workspace.", }) - return nil } if len(createBuild.ProvisionerState) > 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.", }) - return nil } builder = builder.Orphan() } @@ -397,24 +425,23 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { tx, api.FileCache, func(action policy.Action, object rbac.Objecter) bool { - if auth := api.Authorize(r, action, object); auth { + if auth := authorize(action, object); auth { return true } // Special handling for prebuilt workspace deletion if action == policy.ActionDelete { if workspaceObj, ok := object.(database.PrebuiltWorkspaceResource); ok && workspaceObj.IsPrebuild() { - return api.Authorize(r, action, workspaceObj.AsPrebuild()) + return authorize(action, workspaceObj.AsPrebuild()) } } return false }, - audit.WorkspaceBuildBaggageFromRequest(r), + workspaceBuildBaggage, ) return err }, nil) if err != nil { - httperror.WriteWorkspaceBuildError(ctx, rw, err) - return + return codersdk.WorkspaceBuild{}, err } var queuePos database.GetProvisionerJobsByIDsWithQueuePositionRow @@ -478,11 +505,13 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { provisionerDaemons, ) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error converting workspace build.", - Detail: err.Error(), - }) - return + return codersdk.WorkspaceBuild{}, httperror.NewResponseError( + http.StatusInternalServerError, + codersdk.Response{ + Message: "Internal error converting workspace build.", + Detail: err.Error(), + }, + ) } // If this workspace build has a different template version ID to the previous build @@ -509,7 +538,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { WorkspaceID: workspace.ID, }) - httpapi.Write(ctx, rw, http.StatusCreated, apiBuild) + return apiBuild, nil } func (api *API) notifyWorkspaceUpdated( diff --git a/coderd/workspaces.go b/coderd/workspaces.go index bcda1dd022733..3b8e35c003682 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -388,7 +388,13 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req AvatarURL: member.AvatarURL, } - createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r) + w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, r) + if err != nil { + httperror.WriteResponseError(ctx, rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, w) } // Create a new workspace for the currently authenticated user. @@ -442,8 +448,9 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) { // This can be optimized. It exists as it is now for code simplicity. // The most common case is to create a workspace for 'Me'. Which does // not enter this code branch. - template, ok := requestTemplate(ctx, rw, req, api.Database) - if !ok { + template, err := requestTemplate(ctx, req, api.Database) + if err != nil { + httperror.WriteResponseError(ctx, rw, err) return } @@ -476,7 +483,14 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) { }) defer commitAudit() - createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r) + + w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, r) + if err != nil { + httperror.WriteResponseError(ctx, rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, w) } type workspaceOwner struct { @@ -492,12 +506,11 @@ func createWorkspace( api *API, owner workspaceOwner, req codersdk.CreateWorkspaceRequest, - rw http.ResponseWriter, r *http.Request, -) { - template, ok := requestTemplate(ctx, rw, req, api.Database) - if !ok { - return +) (codersdk.Workspace, error) { + template, err := requestTemplate(ctx, req, api.Database) + if err != nil { + return codersdk.Workspace{}, err } // This is a premature auth check to avoid doing unnecessary work if the user @@ -506,14 +519,12 @@ func createWorkspace( rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) { // If this check fails, return a proper unauthorized error to the user to indicate // what is going on. - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusForbidden, codersdk.Response{ Message: "Unauthorized to create workspace.", Detail: "You are unable to create a workspace in this organization. " + "It is possible to have access to the template, but not be able to create a workspace. " + "Please contact an administrator about your permissions if you feel this is an error.", - Validations: nil, }) - return } // Update audit log's organization @@ -523,49 +534,42 @@ func createWorkspace( // would be wasted. if !api.Authorize(r, policy.ActionCreate, rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) { - httpapi.ResourceNotFound(rw) - return + return codersdk.Workspace{}, httperror.ErrResourceNotFound } // The user also needs permission to use the template. At this point they have // read perms, but not necessarily "use". This is also checked in `db.InsertWorkspace`. // Doing this up front can save some work below if the user doesn't have permission. if !api.Authorize(r, policy.ActionUse, template) { - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusForbidden, codersdk.Response{ Message: fmt.Sprintf("Unauthorized access to use the template %q.", template.Name), Detail: "Although you are able to view the template, you are unable to create a workspace using it. " + "Please contact an administrator about your permissions if you feel this is an error.", - Validations: nil, }) - return } templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template) if templateAccessControl.IsDeprecated() { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Template %q has been deprecated, and cannot be used to create a new workspace.", template.Name), // Pass the deprecated message to the user. - Detail: templateAccessControl.Deprecated, - Validations: nil, + Detail: templateAccessControl.Deprecated, }) - return } dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Invalid Autostart Schedule.", Validations: []codersdk.ValidationError{{Field: "schedule", Detail: err.Error()}}, }) - return } templateSchedule, err := (*api.TemplateScheduleStore.Load()).Get(ctx, api.Database, template.ID) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching template schedule.", Detail: err.Error(), }) - return } nextStartAt := sql.NullTime{} @@ -578,11 +582,10 @@ func createWorkspace( dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, templateSchedule.DefaultTTL) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Invalid Workspace Time to Shutdown.", Validations: []codersdk.ValidationError{{Field: "ttl_ms", Detail: err.Error()}}, }) - return } // back-compatibility: default to "never" if not included. @@ -590,11 +593,10 @@ func createWorkspace( if req.AutomaticUpdates != "" { dbAU, err = validWorkspaceAutomaticUpdates(req.AutomaticUpdates) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Invalid Workspace Automatic Updates setting.", Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: err.Error()}}, }) - return } } @@ -607,20 +609,18 @@ func createWorkspace( }) if err == nil { // If the workspace already exists, don't allow creation. - httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusConflict, codersdk.Response{ Message: fmt.Sprintf("Workspace %q already exists.", req.Name), Validations: []codersdk.ValidationError{{ Field: "name", Detail: "This value is already in use and should be unique.", }}, }) - return } else if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name), Detail: err.Error(), }) - return } var ( @@ -759,8 +759,7 @@ func createWorkspace( return err }, nil) if err != nil { - httperror.WriteWorkspaceBuildError(ctx, rw, err) - return + return codersdk.Workspace{}, err } err = provisionerjobs.PostJob(api.Pubsub, *provisionerJob) @@ -809,11 +808,10 @@ func createWorkspace( provisionerDaemons, ) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting workspace build.", Detail: err.Error(), }) - return } w, err := convertWorkspace( @@ -825,40 +823,38 @@ func createWorkspace( codersdk.WorkspaceAppStatus{}, ) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting workspace.", Detail: err.Error(), }) - return } - httpapi.Write(ctx, rw, http.StatusCreated, w) + + return w, nil } -func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template, bool) { +func requestTemplate(ctx context.Context, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template, error) { // If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it. templateID := req.TemplateID if templateID == uuid.Nil { templateVersion, err := db.GetTemplateVersionByID(ctx, req.TemplateVersionID) if httpapi.Is404Error(err) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Template version %q doesn't exist.", req.TemplateVersionID), Validations: []codersdk.ValidationError{{ Field: "template_version_id", Detail: "template not found", }}, }) - return database.Template{}, false } if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching template version.", Detail: err.Error(), }) - return database.Template{}, false } if templateVersion.Archived { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Archived template versions cannot be used to make a workspace.", Validations: []codersdk.ValidationError{ { @@ -867,7 +863,6 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C }, }, }) - return database.Template{}, false } templateID = templateVersion.TemplateID.UUID @@ -875,29 +870,26 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C template, err := db.GetTemplateByID(ctx, templateID) if httpapi.Is404Error(err) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Template %q doesn't exist.", templateID), Validations: []codersdk.ValidationError{{ Field: "template_id", Detail: "template not found", }}, }) - return database.Template{}, false } if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching template.", Detail: err.Error(), }) - return database.Template{}, false } if template.Deleted { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + return database.Template{}, httperror.NewResponseError(http.StatusNotFound, codersdk.Response{ Message: fmt.Sprintf("Template %q has been deleted!", template.Name), }) - return database.Template{}, false } - return template, true + return template, nil } func claimPrebuild( diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 753471e34b565..764fd26ae7996 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -190,3 +190,18 @@ func (c *ExperimentalClient) TaskByID(ctx context.Context, id uuid.UUID) (Task, return task, nil } + +// DeleteTask deletes a task by its ID. +// +// Experimental: This method is experimental and may change in the future. +func (c *ExperimentalClient) DeleteTask(ctx context.Context, user string, id uuid.UUID) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/tasks/%s/%s", user, id.String()), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusAccepted { + return ReadBodyAsError(res) + } + return nil +} diff --git a/docs/_redirects b/docs/_redirects new file mode 100644 index 0000000000000..fdfc401f098f9 --- /dev/null +++ b/docs/_redirects @@ -0,0 +1,6 @@ +# Redirect old offline deployments URL to new airgap URL +/install/offline /install/airgap 301 + +# Redirect old offline anchor fragments to new airgap anchors +/install/offline#offline-docs /install/airgap#airgap-docs 301 +/install/offline#offline-container-images /install/airgap#airgap-container-images 301 diff --git a/docs/admin/integrations/index.md b/docs/admin/integrations/index.md index 900925bd2dfd0..3a1a11f2448df 100644 --- a/docs/admin/integrations/index.md +++ b/docs/admin/integrations/index.md @@ -13,6 +13,6 @@ our [installation guides](../../install/index.md). The following resources may help as you're deploying Coder. - [Coder packages: one-click install on cloud providers](https://github.com/coder/packages) -- [Deploy Coder offline](../../install/offline.md) +- [Deploy Coder Air-gapped](../../install/airgap.md) - [Supported resources (Terraform registry)](https://registry.terraform.io) - [Writing custom templates](../templates/index.md) diff --git a/docs/admin/integrations/jfrog-artifactory.md b/docs/admin/integrations/jfrog-artifactory.md index 702bce2599266..06f0bc670fad8 100644 --- a/docs/admin/integrations/jfrog-artifactory.md +++ b/docs/admin/integrations/jfrog-artifactory.md @@ -129,9 +129,9 @@ To set this up, follow these steps: If you don't want to use the official modules, you can read through the [example template](https://github.com/coder/coder/tree/main/examples/jfrog/docker), which uses Docker as the underlying compute. The same concepts apply to all compute types. -## Offline Deployments +## Air-Gapped Deployments -See the [offline deployments](../templates/extending-templates/modules.md#offline-installations) section for instructions on how to use Coder modules in an offline environment with Artifactory. +See the [air-gapped deployments](../templates/extending-templates/modules.md#offline-installations) section for instructions on how to use Coder modules in an offline environment with Artifactory. ## Next Steps diff --git a/docs/admin/networking/index.md b/docs/admin/networking/index.md index 4ab3352b2c19f..87cbcd7775c93 100644 --- a/docs/admin/networking/index.md +++ b/docs/admin/networking/index.md @@ -116,12 +116,12 @@ If a direct connection is not available (e.g. client or server is behind NAT), Coder will use a relayed connection. By default, [Coder uses Google's public STUN server](../../reference/cli/server.md#--derp-server-stun-addresses), but this can be disabled or changed for -[offline deployments](../../install/offline.md). +[Air-gapped deployments](../../install/airgap.md). ### Relayed connections By default, your Coder server also runs a built-in DERP relay which can be used -for both public and [offline deployments](../../install/offline.md). +for both public and [Air-gapped deployments](../../install/airgap.md). However, our Wireguard integration through Tailscale has graciously allowed us to use @@ -135,7 +135,7 @@ coder server --derp-config-url https://controlplane.tailscale.com/derpmap/defaul #### Custom Relays If you want lower latency than what Tailscale offers or want additional DERP -relays for offline deployments, you may run custom DERP servers. Refer to +relays for air-gapped deployments, you may run custom DERP servers. Refer to [Tailscale's documentation](https://tailscale.com/kb/1118/custom-derp-servers/#why-run-your-own-derp-server) to learn how to set them up. diff --git a/docs/changelogs/v0.27.0.md b/docs/changelogs/v0.27.0.md index a37997f942f23..5e06e5a028c3c 100644 --- a/docs/changelogs/v0.27.0.md +++ b/docs/changelogs/v0.27.0.md @@ -25,7 +25,7 @@ Agent logs can be pushed after a workspace has started (#8528) - Template version messages (#8435) 252772262-087f1338-f1e2-49fb-81f2-358070a46484 - TTL and max TTL validation increased to 30 days (#8258) -- [Self-hosted docs](https://coder.com/docs/install/offline#offline-docs): +- [Self-hosted docs](https://coder.com/docs/install/airgap#airgap-docs): Host your own copy of Coder's documentation in your own environment (#8527) (#8601) - Add custom coder bin path for `config-ssh` (#8425) diff --git a/docs/changelogs/v2.8.0.md b/docs/changelogs/v2.8.0.md index e7804ab57b3db..1b17ba3a7343f 100644 --- a/docs/changelogs/v2.8.0.md +++ b/docs/changelogs/v2.8.0.md @@ -83,7 +83,7 @@ ### Documentation -- Using coder modules in offline deployments (#11788) (@matifali) +- Using coder modules in air-gapped deployments (#11788) (@matifali) - Simplify JFrog integration docs (#11787) (@matifali) - Add guide for azure federation (#11864) (@ericpaulsen) - Fix example template README 404s and semantics (#11903) (@ericpaulsen) diff --git a/docs/install/offline.md b/docs/install/airgap.md similarity index 97% rename from docs/install/offline.md rename to docs/install/airgap.md index 289780526f76a..cb2f2340a63cd 100644 --- a/docs/install/offline.md +++ b/docs/install/airgap.md @@ -1,12 +1,10 @@ -# Offline Deployments - -All Coder features are supported in offline / behind firewalls / in air-gapped -environments. However, some changes to your configuration are necessary. +# Air-gapped Deployments +All Coder features are supported in air-gapped / behind firewalls / disconnected / offline. This is a general comparison. Keep reading for a full tutorial running Coder -offline with Kubernetes or Docker. +air-gapped with Kubernetes or Docker. -| | Public deployments | Offline deployments | +| | Public deployments | Air-gapped deployments | |--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Terraform binary | By default, Coder downloads Terraform binary from [releases.hashicorp.com](https://releases.hashicorp.com) | Terraform binary must be included in `PATH` for the VM or container image. [Supported versions](https://github.com/coder/coder/blob/main/provisioner/terraform/install.go#L23-L24) | | Terraform registry | Coder templates will attempt to download providers from [registry.terraform.io](https://registry.terraform.io) or [custom source addresses](https://developer.hashicorp.com/terraform/language/providers/requirements#source-addresses) specified in each template | [Custom source addresses](https://developer.hashicorp.com/terraform/language/providers/requirements#source-addresses) can be specified in each Coder template, or a custom registry/mirror can be used. More details below | @@ -16,7 +14,7 @@ offline with Kubernetes or Docker. | Telemetry | Telemetry is on by default, and [can be disabled](../reference/cli/server.md#--telemetry) | Telemetry [can be disabled](../reference/cli/server.md#--telemetry) | | Update check | By default, Coder checks for updates from [GitHub releases](https://github.com/coder/coder/releases) | Update checks [can be disabled](../reference/cli/server.md#--update-check) | -## Offline container images +## Air-gapped container images The following instructions walk you through how to build a custom Coder server image for Docker or Kubernetes @@ -214,9 +212,9 @@ coder: -## Offline docs +## Air-gapped docs -Coder also provides offline documentation in case you want to host it on your +Coder also provides air-gapped documentation in case you want to host it on your own server. The docs are exported as static files that you can host on any web server, as demonstrated in the example below: diff --git a/docs/manifest.json b/docs/manifest.json index d2cd11ace699b..9359fb6f1da33 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -154,9 +154,9 @@ ] }, { - "title": "Offline Deployments", - "description": "Run Coder in offline / air-gapped environments", - "path": "./install/offline.md", + "title": "Air-gapped Deployments", + "description": "Run Coder in air-gapped / disconnected / offline environments", + "path": "./install/airgap.md", "icon_path": "./images/icons/lan.svg" }, { diff --git a/docs/tutorials/faqs.md b/docs/tutorials/faqs.md index bd386f81288a8..a2f350b45a734 100644 --- a/docs/tutorials/faqs.md +++ b/docs/tutorials/faqs.md @@ -332,7 +332,7 @@ References: ## Can I run Coder in an air-gapped or offline mode? (no Internet)? Yes, Coder can be deployed in -[air-gapped or offline mode](../install/offline.md). +[air-gapped or offline mode](../install/airgap.md). Our product bundles with the Terraform binary so assume access to terraform.io during installation. The docs outline rebuilding the Coder container with diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index c9986c97580e0..ce9050992eb92 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "crypto/rand" "crypto/tls" + "database/sql" "io" "net/http" "os/exec" @@ -23,10 +24,13 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/drpcsdk" "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/license" + entprebuilds "github.com/coder/coder/v2/enterprise/coderd/prebuilds" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisioner/terraform" @@ -446,3 +450,98 @@ func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui return closer } + +func GetRunningPrebuilds( + ctx context.Context, + t *testing.T, + db database.Store, + desiredPrebuilds int, +) []database.GetRunningPrebuiltWorkspacesRow { + t.Helper() + + var runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow + testutil.Eventually(ctx, t, func(context.Context) bool { + prebuiltWorkspaces, err := db.GetRunningPrebuiltWorkspaces(ctx) + assert.NoError(t, err, "failed to get running prebuilds") + + for _, prebuild := range prebuiltWorkspaces { + runningPrebuilds = append(runningPrebuilds, prebuild) + + agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, prebuild.ID) + assert.NoError(t, err, "failed to get agents") + + // Manually mark all agents as ready since tests don't have real agent processes + // that would normally report their lifecycle state. Prebuilt workspaces are only + // eligible for claiming when their agents reach the "ready" state. + for _, agent := range agents { + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true}, + ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true}, + }) + assert.NoError(t, err, "failed to update agent") + } + } + + t.Logf("found %d running prebuilds so far, want %d", len(runningPrebuilds), desiredPrebuilds) + return len(runningPrebuilds) == desiredPrebuilds + }, testutil.IntervalSlow, "found %d running prebuilds, expected %d", len(runningPrebuilds), desiredPrebuilds) + + return runningPrebuilds +} + +func MustRunReconciliationLoopForPreset( + ctx context.Context, + t *testing.T, + db database.Store, + reconciler *entprebuilds.StoreReconciler, + preset codersdk.Preset, +) []*prebuilds.ReconciliationActions { + t.Helper() + + state, err := reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + ps, err := state.FilterByPreset(preset.ID) + require.NoError(t, err) + require.NotNil(t, ps) + actions, err := reconciler.CalculateActions(ctx, *ps) + require.NoError(t, err) + require.NotNil(t, actions) + require.NoError(t, reconciler.ReconcilePreset(ctx, *ps)) + + return actions +} + +func MustClaimPrebuild( + ctx context.Context, + t *testing.T, + client *codersdk.Client, + userClient *codersdk.Client, + username string, + version codersdk.TemplateVersion, + presetID uuid.UUID, + autostartSchedule ...string, +) codersdk.Workspace { + t.Helper() + + var startSchedule string + if len(autostartSchedule) > 0 { + startSchedule = autostartSchedule[0] + } + + workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + userWorkspace, err := userClient.CreateUserWorkspace(ctx, username, codersdk.CreateWorkspaceRequest{ + TemplateVersionID: version.ID, + Name: workspaceName, + TemplateVersionPresetID: presetID, + AutostartSchedule: ptr.Ref(startSchedule), + }) + require.NoError(t, err) + build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID) + require.Equal(t, build.Job.Status, codersdk.ProvisionerJobSucceeded) + workspace := coderdtest.MustWorkspace(t, client, userWorkspace.ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) + + return workspace +} diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index 504c9a04caea0..d2913f7e0e229 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "math" + "sort" "time" "github.com/golang-jwt/jwt/v4" @@ -192,6 +193,13 @@ func LicensesEntitlements( }) } + // nextLicenseValidityPeriod holds the current or next contiguous period + // where there will be at least one active license. This is used for + // generating license expiry warnings. Previously we would generate licenses + // expiry warnings for each license, but it means that the warning will show + // even if you've loaded up a new license that doesn't have any gap. + nextLicenseValidityPeriod := &licenseValidityPeriod{} + // TODO: License specific warnings and errors should be tied to the license, not the // 'Entitlements' group as a whole. for _, license := range licenses { @@ -201,6 +209,17 @@ func LicensesEntitlements( // The license isn't valid yet. We don't consider any entitlements contained in it, but // it's also not an error. Just skip it silently. This can happen if an administrator // uploads a license for a new term that hasn't started yet. + // + // We still want to factor this into our validity period, though. + // This ensures we can suppress license expiry warnings for expiring + // licenses while a new license is ready to take its place. + // + // claims is nil, so reparse the claims with the IgnoreNbf function. + claims, err = ParseClaimsIgnoreNbf(license.JWT, keys) + if err != nil { + continue + } + nextLicenseValidityPeriod.ApplyClaims(claims) continue } if err != nil { @@ -209,6 +228,10 @@ func LicensesEntitlements( continue } + // Obviously, valid licenses should be considered for the license + // validity period. + nextLicenseValidityPeriod.ApplyClaims(claims) + usagePeriodStart := claims.NotBefore.Time // checked not-nil when validating claims usagePeriodEnd := claims.ExpiresAt.Time // checked not-nil when validating claims if usagePeriodStart.After(usagePeriodEnd) { @@ -237,10 +260,6 @@ func LicensesEntitlements( entitlement = codersdk.EntitlementGracePeriod } - // Will add a warning if the license is expiring soon. - // This warning can be raised multiple times if there is more than 1 license. - licenseExpirationWarning(&entitlements, now, claims) - // 'claims.AllFeature' is the legacy way to set 'claims.FeatureSet = codersdk.FeatureSetEnterprise' // If both are set, ignore the legacy 'claims.AllFeature' if claims.AllFeatures && claims.FeatureSet == "" { @@ -405,6 +424,10 @@ func LicensesEntitlements( // Now the license specific warnings and errors are added to the entitlements. + // Add a single warning if we are currently in the license validity period + // and it's expiring soon. + nextLicenseValidityPeriod.LicenseExpirationWarning(&entitlements, now) + // If HA is enabled, ensure the feature is entitled. if featureArguments.ReplicaCount > 1 { feature := entitlements.Features[codersdk.FeatureHighAvailability] @@ -742,10 +765,85 @@ func keyFunc(keys map[string]ed25519.PublicKey) func(*jwt.Token) (interface{}, e } } -// licenseExpirationWarning adds a warning message if the license is expiring soon. -func licenseExpirationWarning(entitlements *codersdk.Entitlements, now time.Time, claims *Claims) { - // Add warning if license is expiring soon - daysToExpire := int(math.Ceil(claims.LicenseExpires.Sub(now).Hours() / 24)) +// licenseValidityPeriod keeps track of all license validity periods, and +// generates warnings over contiguous periods across multiple licenses. +// +// Note: this does not track the actual entitlements of each license to ensure +// newer licenses cover the same features as older licenses before merging. It +// is assumed that all licenses cover the same features. +type licenseValidityPeriod struct { + // parts contains all tracked license periods prior to merging. + parts [][2]time.Time +} + +// ApplyClaims tracks a license validity period. This should only be called with +// valid (including not-yet-valid), unexpired licenses. +func (p *licenseValidityPeriod) ApplyClaims(claims *Claims) { + if claims == nil || claims.NotBefore == nil || claims.LicenseExpires == nil { + // Bad data + return + } + p.Apply(claims.NotBefore.Time, claims.LicenseExpires.Time) +} + +// Apply adds a license validity period. +func (p *licenseValidityPeriod) Apply(start, end time.Time) { + if end.Before(start) { + // Bad data + return + } + p.parts = append(p.parts, [2]time.Time{start, end}) +} + +// merged merges the license validity periods into contiguous blocks, and sorts +// the merged blocks. +func (p *licenseValidityPeriod) merged() [][2]time.Time { + if len(p.parts) == 0 { + return nil + } + + // Sort the input periods by start time. + sorted := make([][2]time.Time, len(p.parts)) + copy(sorted, p.parts) + sort.Slice(sorted, func(i, j int) bool { + return sorted[i][0].Before(sorted[j][0]) + }) + + out := make([][2]time.Time, 0, len(sorted)) + cur := sorted[0] + for i := 1; i < len(sorted); i++ { + next := sorted[i] + + // If the current period's end time is before or equal to the next + // period's start time, they should be merged. + if !next[0].After(cur[1]) { + // Pick the maximum end time. + if next[1].After(cur[1]) { + cur[1] = next[1] + } + continue + } + + // They don't overlap, so commit the current period and start a new one. + out = append(out, cur) + cur = next + } + // Commit the final period. + out = append(out, cur) + return out +} + +// LicenseExpirationWarning adds a warning message if we are currently in the +// license validity period and it's expiring soon. +func (p *licenseValidityPeriod) LicenseExpirationWarning(entitlements *codersdk.Entitlements, now time.Time) { + merged := p.merged() + if len(merged) == 0 { + // No licenses + return + } + end := merged[0][1] + + daysToExpire := int(math.Ceil(end.Sub(now).Hours() / 24)) showWarningDays := 30 isTrial := entitlements.Trial if isTrial { diff --git a/enterprise/coderd/license/license_internal_test.go b/enterprise/coderd/license/license_internal_test.go new file mode 100644 index 0000000000000..616f0b5b989b9 --- /dev/null +++ b/enterprise/coderd/license/license_internal_test.go @@ -0,0 +1,140 @@ +package license + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNextLicenseValidityPeriod(t *testing.T) { + t.Parallel() + + t.Run("Apply", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + + licensePeriods [][2]time.Time + expectedPeriods [][2]time.Time + }{ + { + name: "None", + licensePeriods: [][2]time.Time{}, + expectedPeriods: [][2]time.Time{}, + }, + { + name: "One", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "TwoOverlapping", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "TwoNonOverlapping", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "ThreeOverlapping", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "ThreeNonOverlapping", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "PeriodContainsAnotherPeriod", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 8, 0, 0, 0, 0, time.UTC)}, + {time.Date(2025, 1, 3, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: [][2]time.Time{ + {time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 8, 0, 0, 0, 0, time.UTC)}, + }, + }, + { + name: "EndBeforeStart", + licensePeriods: [][2]time.Time{ + {time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)}, + }, + expectedPeriods: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Test with all possible permutations of the periods to ensure + // consistency regardless of the order. + ps := permutations(tc.licensePeriods) + for _, p := range ps { + t.Logf("permutation: %v", p) + period := &licenseValidityPeriod{} + for _, times := range p { + t.Logf("applying %v", times) + period.Apply(times[0], times[1]) + } + assert.Equal(t, tc.expectedPeriods, period.merged(), "merged") + } + }) + } + }) +} + +func permutations[T any](arr []T) [][]T { + var res [][]T + var helper func([]T, int) + helper = func(a []T, i int) { + if i == len(a)-1 { + // make a copy before appending + tmp := make([]T, len(a)) + copy(tmp, a) + res = append(res, tmp) + return + } + for j := i; j < len(a); j++ { + a[i], a[j] = a[j], a[i] + helper(a, i+1) + a[i], a[j] = a[j], a[i] // backtrack + } + } + helper(arr, 0) + return res +} diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 0ca7d2287ad63..c457b7f076922 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -180,6 +180,121 @@ func TestEntitlements(t *testing.T) { ) }) + t.Run("Expiration warning suppressed if new license covers gap", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + // Insert the expiring license + graceDate := dbtime.Now().AddDate(0, 0, 1) + _, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{ + JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, + + FeatureSet: codersdk.FeatureSetPremium, + GraceAt: graceDate, + ExpiresAt: dbtime.Now().AddDate(0, 0, 5), + }), + Exp: time.Now().AddDate(0, 0, 5), + }) + require.NoError(t, err) + + // Warning should be generated. + entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all) + require.NoError(t, err) + require.True(t, entitlements.HasLicense) + require.False(t, entitlements.Trial) + require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + require.Len(t, entitlements.Warnings, 1) + require.Contains(t, entitlements.Warnings, "Your license expires in 1 day.") + + // Insert the new, not-yet-valid license that starts BEFORE the expiring + // license expires. + _, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{ + JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, + + FeatureSet: codersdk.FeatureSetPremium, + NotBefore: graceDate.Add(-time.Hour), // contiguous, and also in the future + GraceAt: dbtime.Now().AddDate(1, 0, 0), + ExpiresAt: dbtime.Now().AddDate(1, 0, 5), + }), + Exp: dbtime.Now().AddDate(1, 0, 5), + }) + require.NoError(t, err) + + // Warning should be suppressed. + entitlements, err = license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all) + require.NoError(t, err) + require.True(t, entitlements.HasLicense) + require.False(t, entitlements.Trial) + require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + require.Len(t, entitlements.Warnings, 0) // suppressed + }) + + t.Run("Expiration warning not suppressed if new license has gap", func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + // Insert the expiring license + graceDate := dbtime.Now().AddDate(0, 0, 1) + _, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{ + JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, + + FeatureSet: codersdk.FeatureSetPremium, + GraceAt: graceDate, + ExpiresAt: dbtime.Now().AddDate(0, 0, 5), + }), + Exp: time.Now().AddDate(0, 0, 5), + }) + require.NoError(t, err) + + // Should generate a warning. + entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all) + require.NoError(t, err) + require.True(t, entitlements.HasLicense) + require.False(t, entitlements.Trial) + require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + require.Len(t, entitlements.Warnings, 1) + require.Contains(t, entitlements.Warnings, "Your license expires in 1 day.") + + // Insert the new, not-yet-valid license that starts AFTER the expiring + // license expires (e.g. there's a gap) + _, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{ + JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, + + FeatureSet: codersdk.FeatureSetPremium, + NotBefore: graceDate.Add(time.Minute), // gap of 1 second! + GraceAt: dbtime.Now().AddDate(1, 0, 0), + ExpiresAt: dbtime.Now().AddDate(1, 0, 5), + }), + Exp: dbtime.Now().AddDate(1, 0, 5), + }) + require.NoError(t, err) + + // Warning should still be generated. + entitlements, err = license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all) + require.NoError(t, err) + require.True(t, entitlements.HasLicense) + require.False(t, entitlements.Trial) + require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + require.Len(t, entitlements.Warnings, 1) + require.Contains(t, entitlements.Warnings, "Your license expires in 1 day.") + }) + t.Run("Expiration warning for trials", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 555806b62371d..0943fd9077868 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -2879,105 +2879,114 @@ func TestWorkspaceProvisionerdServerMetrics(t *testing.T) { t.Parallel() // Setup - log := testutil.Logger(t) + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) + logger := testutil.Logger(t) reg := prometheus.NewRegistry() - provisionerdserverMetrics := provisionerdserver.NewMetrics(log) + provisionerdserverMetrics := provisionerdserver.NewMetrics(logger) err := provisionerdserverMetrics.Register(reg) require.NoError(t, err) - client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ Options: &coderdtest.Options{ + Database: db, + Pubsub: pb, IncludeProvisionerDaemon: true, + Clock: clock, ProvisionerdServerMetrics: provisionerdserverMetrics, }, - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureWorkspacePrebuilds: 1, - }, - }, }) - // Given: a template and a template version with a preset without prebuild instances - presetNoPrebuildID := uuid.New() - versionNoPrebuild := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionNoPrebuild.ID) - templateNoPrebuild := coderdtest.CreateTemplate(t, client, owner.OrganizationID, versionNoPrebuild.ID) - presetNoPrebuild := dbgen.Preset(t, db, database.InsertPresetParams{ - ID: presetNoPrebuildID, - TemplateVersionID: versionNoPrebuild.ID, - }) + // Setup Prebuild reconciler + cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{}) + reconciler := prebuilds.NewStoreReconciler( + db, pb, cache, + codersdk.PrebuildsConfig{}, + logger, + clock, + prometheus.NewRegistry(), + notifications.NewNoopEnqueuer(), + api.AGPL.BuildUsageChecker, + ) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + organizationName, err := client.Organization(ctx, owner.OrganizationID) + require.NoError(t, err) + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) - // Given: a template and a template version with a preset with a prebuild instance - presetPrebuildID := uuid.New() - versionPrebuild := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionPrebuild.ID) + // Setup template and template version with a preset with 1 prebuild instance + versionPrebuild := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(1)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionPrebuild.ID) templatePrebuild := coderdtest.CreateTemplate(t, client, owner.OrganizationID, versionPrebuild.ID) - presetPrebuild := dbgen.Preset(t, db, database.InsertPresetParams{ - ID: presetPrebuildID, - TemplateVersionID: versionPrebuild.ID, - DesiredInstances: sql.NullInt32{Int32: 1, Valid: true}, - }) - // Given: a prebuild workspace - wb := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OwnerID: database.PrebuildsSystemUserID, - TemplateID: templatePrebuild.ID, - }).Seed(database.WorkspaceBuild{ - TemplateVersionID: versionPrebuild.ID, - TemplateVersionPresetID: uuid.NullUUID{ - UUID: presetPrebuildID, - Valid: true, - }, - }).WithAgent(func(agent []*proto.Agent) []*proto.Agent { - return agent - }).Do() + presetsPrebuild, err := client.TemplateVersionPresets(ctx, versionPrebuild.ID) + require.NoError(t, err) + require.Len(t, presetsPrebuild, 1) - // Mark the prebuilt workspace's agent as ready so the prebuild can be claimed - // nolint:gocritic - ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong)) - agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(wb.AgentToken)) + // Setup template and template version with a preset without prebuild instances + versionNoPrebuild := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(0)) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionNoPrebuild.ID) + templateNoPrebuild := coderdtest.CreateTemplate(t, client, owner.OrganizationID, versionNoPrebuild.ID) + presetsNoPrebuild, err := client.TemplateVersionPresets(ctx, versionNoPrebuild.ID) require.NoError(t, err) - err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ - ID: agent.WorkspaceAgent.ID, - LifecycleState: database.WorkspaceAgentLifecycleStateReady, + require.Len(t, presetsNoPrebuild, 1) + + // Given: no histogram value for prebuilt workspaces creation + prebuildCreationMetric := promhelp.MetricValue(t, reg, "coderd_workspace_creation_duration_seconds", prometheus.Labels{ + "organization_name": organizationName.Name, + "template_name": templatePrebuild.Name, + "preset_name": presetsPrebuild[0].Name, + "type": "prebuild", }) - require.NoError(t, err) + require.Nil(t, prebuildCreationMetric) - organizationName, err := client.Organization(ctx, owner.OrganizationID) - require.NoError(t, err) - user, err := client.User(ctx, "testUser") - require.NoError(t, err) + // Given: reconciliation loop runs and starts prebuilt workspace + coderdenttest.MustRunReconciliationLoopForPreset(ctx, t, db, reconciler, presetsPrebuild[0]) + runningPrebuilds := coderdenttest.GetRunningPrebuilds(ctx, t, db, 1) + require.Len(t, runningPrebuilds, 1) + + // Then: the histogram value for prebuilt workspace creation should be updated + prebuildCreationHistogram := promhelp.HistogramValue(t, reg, "coderd_workspace_creation_duration_seconds", prometheus.Labels{ + "organization_name": organizationName.Name, + "template_name": templatePrebuild.Name, + "preset_name": presetsPrebuild[0].Name, + "type": "prebuild", + }) + require.NotNil(t, prebuildCreationHistogram) + require.Equal(t, uint64(1), prebuildCreationHistogram.GetSampleCount()) + + // Given: a running prebuilt workspace, ready to be claimed + prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition) + require.Nil(t, prebuild.DormantAt) + require.Nil(t, prebuild.DeletingAt) // Given: no histogram value for prebuilt workspaces claim - prebuiltWorkspaceHistogramMetric := promhelp.MetricValue(t, reg, "coderd_prebuilt_workspace_claim_duration_seconds", prometheus.Labels{ + prebuildClaimMetric := promhelp.MetricValue(t, reg, "coderd_prebuilt_workspace_claim_duration_seconds", prometheus.Labels{ "organization_name": organizationName.Name, "template_name": templatePrebuild.Name, - "preset_name": presetPrebuild.Name, + "preset_name": presetsPrebuild[0].Name, }) - require.Nil(t, prebuiltWorkspaceHistogramMetric) + require.Nil(t, prebuildClaimMetric) // Given: the prebuilt workspace is claimed by a user - claimedWorkspace, err := client.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{ - TemplateVersionID: versionPrebuild.ID, - TemplateVersionPresetID: presetPrebuildID, - Name: coderdtest.RandomUsername(t), - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, claimedWorkspace.LatestBuild.ID) - require.Equal(t, wb.Workspace.ID, claimedWorkspace.ID) + workspace := coderdenttest.MustClaimPrebuild(ctx, t, client, userClient, user.Username, versionPrebuild, presetsPrebuild[0].ID) + require.Equal(t, prebuild.ID, workspace.ID) // Then: the histogram value for prebuilt workspace claim should be updated - prebuiltWorkspaceHistogram := promhelp.HistogramValue(t, reg, "coderd_prebuilt_workspace_claim_duration_seconds", prometheus.Labels{ + prebuildClaimHistogram := promhelp.HistogramValue(t, reg, "coderd_prebuilt_workspace_claim_duration_seconds", prometheus.Labels{ "organization_name": organizationName.Name, "template_name": templatePrebuild.Name, - "preset_name": presetPrebuild.Name, + "preset_name": presetsPrebuild[0].Name, }) - require.NotNil(t, prebuiltWorkspaceHistogram) - require.Equal(t, uint64(1), prebuiltWorkspaceHistogram.GetSampleCount()) + require.NotNil(t, prebuildClaimHistogram) + require.Equal(t, uint64(1), prebuildClaimHistogram.GetSampleCount()) // Given: no histogram value for regular workspaces creation regularWorkspaceHistogramMetric := promhelp.MetricValue(t, reg, "coderd_workspace_creation_duration_seconds", prometheus.Labels{ "organization_name": organizationName.Name, "template_name": templateNoPrebuild.Name, - "preset_name": presetNoPrebuild.Name, + "preset_name": presetsNoPrebuild[0].Name, "type": "regular", }) require.Nil(t, regularWorkspaceHistogramMetric) @@ -2985,7 +2994,7 @@ func TestWorkspaceProvisionerdServerMetrics(t *testing.T) { // Given: a user creates a regular workspace (without prebuild pool) regularWorkspace, err := client.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{ TemplateVersionID: versionNoPrebuild.ID, - TemplateVersionPresetID: presetNoPrebuildID, + TemplateVersionPresetID: presetsNoPrebuild[0].ID, Name: coderdtest.RandomUsername(t), }) require.NoError(t, err) @@ -2995,7 +3004,7 @@ func TestWorkspaceProvisionerdServerMetrics(t *testing.T) { regularWorkspaceHistogram := promhelp.HistogramValue(t, reg, "coderd_workspace_creation_duration_seconds", prometheus.Labels{ "organization_name": organizationName.Name, "template_name": templateNoPrebuild.Name, - "preset_name": presetNoPrebuild.Name, + "preset_name": presetsNoPrebuild[0].Name, "type": "regular", }) require.NotNil(t, regularWorkspaceHistogram)