diff --git a/Makefile b/Makefile index c8f71c589b7b5..d198e2a29c16c 100644 --- a/Makefile +++ b/Makefile @@ -486,7 +486,7 @@ coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/dat go run ./coderd/database/gen/dump/main.go # Generates Go code for querying the database. -coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go +coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go coderd/database/gen/fake/main.go ./coderd/database/generate.sh diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 11172959cd637..e3a3aecebb0ff 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -215,14 +215,6 @@ func (*fakeQuerier) Ping(_ context.Context) (time.Duration, error) { return 0, nil } -func (*fakeQuerier) AcquireLock(_ context.Context, _ int64) error { - return xerrors.New("AcquireLock must only be called within a transaction") -} - -func (*fakeQuerier) TryAcquireLock(_ context.Context, _ int64) (bool, error) { - return false, xerrors.New("TryAcquireLock must only be called within a transaction") -} - func (tx *fakeTx) AcquireLock(_ context.Context, id int64) error { if _, ok := tx.fakeQuerier.locks[id]; ok { return xerrors.Errorf("cannot acquire lock %d: already held", id) @@ -261,1915 +253,1714 @@ func (q *fakeQuerier) InTx(fn func(database.Store) error, _ *sql.TxOptions) erro return fn(tx) } -func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.AcquireProvisionerJobParams) (database.ProvisionerJob, error) { - if err := validateDatabaseType(arg); err != nil { - return database.ProvisionerJob{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for index, provisionerJob := range q.provisionerJobs { - if provisionerJob.StartedAt.Valid { - continue - } - found := false - for _, provisionerType := range arg.Types { - if provisionerJob.Provisioner != provisionerType { - continue - } - found = true - break - } - if !found { - continue - } - tags := map[string]string{} - if arg.Tags != nil { - err := json.Unmarshal(arg.Tags, &tags) - if err != nil { - return provisionerJob, xerrors.Errorf("unmarshal: %w", err) - } - } - - missing := false - for key, value := range provisionerJob.Tags { - provided, found := tags[key] - if !found { - missing = true - break - } - if provided != value { - missing = true - break - } - } - if missing { - continue +// getUserByIDNoLock is used by other functions in the database fake. +func (q *fakeQuerier) getUserByIDNoLock(id uuid.UUID) (database.User, error) { + for _, user := range q.users { + if user.ID == id { + return user, nil } - provisionerJob.StartedAt = arg.StartedAt - provisionerJob.UpdatedAt = arg.StartedAt.Time - provisionerJob.WorkerID = arg.WorkerID - q.provisionerJobs[index] = provisionerJob - return provisionerJob, nil } - return database.ProvisionerJob{}, sql.ErrNoRows + return database.User{}, sql.ErrNoRows } -func (*fakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { - // no-op - return nil -} +func (q *fakeQuerier) GetAuthorizedUserCount(ctx context.Context, params database.GetFilteredUserCountParams, prepared rbac.PreparedAuthorized) (int64, error) { + if err := validateDatabaseType(params); err != nil { + return 0, err + } -func (q *fakeQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() - // Get latest build for workspace. - workspaceBuild, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) - if err != nil { - return nil, xerrors.Errorf("get latest workspace build: %w", err) + // Call this to match the same function calls as the SQL implementation. + if prepared != nil { + _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL()) + if err != nil { + return -1, err + } } - // Get resources for build. - resources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, workspaceBuild.JobID) - if err != nil { - return nil, xerrors.Errorf("get workspace resources: %w", err) - } - if len(resources) == 0 { - return []database.WorkspaceAgent{}, nil - } + users := make([]database.User, 0, len(q.users)) - resourceIDs := make([]uuid.UUID, len(resources)) - for i, resource := range resources { - resourceIDs[i] = resource.ID - } + for _, user := range q.users { + // If the filter exists, ensure the object is authorized. + if prepared != nil && prepared.Authorize(ctx, user.RBACObject()) != nil { + continue + } - agents, err := q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) - if err != nil { - return nil, xerrors.Errorf("get workspace agents: %w", err) + users = append(users, user) } - return agents, nil -} - -func (q *fakeQuerier) GetDeploymentWorkspaceAgentStats(_ context.Context, createdAfter time.Time) (database.GetDeploymentWorkspaceAgentStatsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) - for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { - agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) + // Filter out deleted since they should never be returned.. + tmp := make([]database.User, 0, len(users)) + for _, user := range users { + if !user.Deleted { + tmp = append(tmp, user) } } + users = tmp - latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} - for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { - latestAgentStats[agentStat.AgentID] = agentStat + if params.Search != "" { + tmp := make([]database.User, 0, len(users)) + for i, user := range users { + if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) { + tmp = append(tmp, users[i]) + } else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) { + tmp = append(tmp, users[i]) + } } + users = tmp } - stat := database.GetDeploymentWorkspaceAgentStatsRow{} - for _, agentStat := range latestAgentStats { - stat.SessionCountVSCode += agentStat.SessionCountVSCode - stat.SessionCountJetBrains += agentStat.SessionCountJetBrains - stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY - stat.SessionCountSSH += agentStat.SessionCountSSH - } - - latencies := make([]float64, 0) - for _, agentStat := range agentStatsCreatedAfter { - if agentStat.ConnectionMedianLatencyMS <= 0 { - continue + if len(params.Status) > 0 { + usersFilteredByStatus := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool { + return strings.EqualFold(string(a), string(b)) + }) { + usersFilteredByStatus = append(usersFilteredByStatus, users[i]) + } } - stat.WorkspaceRxBytes += agentStat.RxBytes - stat.WorkspaceTxBytes += agentStat.TxBytes - latencies = append(latencies, agentStat.ConnectionMedianLatencyMS) + users = usersFilteredByStatus } - tryPercentile := func(fs []float64, p float64) float64 { - if len(fs) == 0 { - return -1 + if len(params.RbacRole) > 0 && !slice.Contains(params.RbacRole, rbac.RoleMember()) { + usersFilteredByRole := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) { + usersFilteredByRole = append(usersFilteredByRole, users[i]) + } } - sort.Float64s(fs) - return fs[int(float64(len(fs))*p/100)] - } - stat.WorkspaceConnectionLatency50 = tryPercentile(latencies, 50) - stat.WorkspaceConnectionLatency95 = tryPercentile(latencies, 95) + users = usersFilteredByRole + } - return stat, nil + return int64(len(users)), nil } -func (q *fakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) { - if err := validateDatabaseType(p); err != nil { - return database.WorkspaceAgentStat{}, err +func convertUsers(users []database.User, count int64) []database.GetUsersRow { + rows := make([]database.GetUsersRow, len(users)) + for i, u := range users { + rows[i] = database.GetUsersRow{ + ID: u.ID, + Email: u.Email, + Username: u.Username, + HashedPassword: u.HashedPassword, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + Status: u.Status, + RBACRoles: u.RBACRoles, + LoginType: u.LoginType, + AvatarURL: u.AvatarURL, + Deleted: u.Deleted, + LastSeenAt: u.LastSeenAt, + Count: count, + } } - q.mutex.Lock() - defer q.mutex.Unlock() + return rows +} - stat := database.WorkspaceAgentStat{ - ID: p.ID, - CreatedAt: p.CreatedAt, - WorkspaceID: p.WorkspaceID, - AgentID: p.AgentID, - UserID: p.UserID, - ConnectionsByProto: p.ConnectionsByProto, - ConnectionCount: p.ConnectionCount, - RxPackets: p.RxPackets, - RxBytes: p.RxBytes, - TxPackets: p.TxPackets, - TxBytes: p.TxBytes, - TemplateID: p.TemplateID, - SessionCountVSCode: p.SessionCountVSCode, - SessionCountJetBrains: p.SessionCountJetBrains, - SessionCountReconnectingPTY: p.SessionCountReconnectingPTY, - SessionCountSSH: p.SessionCountSSH, - ConnectionMedianLatencyMS: p.ConnectionMedianLatencyMS, +//nolint:gocyclo +func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err } - q.workspaceAgentStats = append(q.workspaceAgentStats, stat) - return stat, nil -} -func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplateDAUsParams) ([]database.GetTemplateDAUsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - seens := make(map[time.Time]map[uuid.UUID]struct{}) - - for _, as := range q.workspaceAgentStats { - if as.TemplateID != arg.TemplateID { - continue + if prepared != nil { + // Call this to match the same function calls as the SQL implementation. + _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL()) + if err != nil { + return nil, err } - if as.ConnectionCount == 0 { + } + + workspaces := make([]database.Workspace, 0) + for _, workspace := range q.workspaces { + if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { continue } - date := as.CreatedAt.UTC().Add(time.Duration(arg.TzOffset) * time.Hour * -1).Truncate(time.Hour * 24) - - dateEntry := seens[date] - if dateEntry == nil { - dateEntry = make(map[uuid.UUID]struct{}) + if arg.OwnerUsername != "" { + owner, err := q.getUserByIDNoLock(workspace.OwnerID) + if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { + continue + } } - dateEntry[as.UserID] = struct{}{} - seens[date] = dateEntry - } - - seenKeys := maps.Keys(seens) - sort.Slice(seenKeys, func(i, j int) bool { - return seenKeys[i].Before(seenKeys[j]) - }) - var rs []database.GetTemplateDAUsRow - for _, key := range seenKeys { - ids := seens[key] - for id := range ids { - rs = append(rs, database.GetTemplateDAUsRow{ - Date: key, - UserID: id, - }) + if arg.TemplateName != "" { + template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) + if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { + continue + } } - } - return rs, nil -} - -func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context, tzOffset int32) ([]database.GetDeploymentDAUsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - seens := make(map[time.Time]map[uuid.UUID]struct{}) - - for _, as := range q.workspaceAgentStats { - if as.ConnectionCount == 0 { + if !arg.Deleted && workspace.Deleted { continue } - date := as.CreatedAt.UTC().Add(time.Duration(tzOffset) * -1 * time.Hour).Truncate(time.Hour * 24) - - dateEntry := seens[date] - if dateEntry == nil { - dateEntry = make(map[uuid.UUID]struct{}) - } - dateEntry[as.UserID] = struct{}{} - seens[date] = dateEntry - } - - seenKeys := maps.Keys(seens) - sort.Slice(seenKeys, func(i, j int) bool { - return seenKeys[i].Before(seenKeys[j]) - }) - - var rs []database.GetDeploymentDAUsRow - for _, key := range seenKeys { - ids := seens[key] - for id := range ids { - rs = append(rs, database.GetDeploymentDAUsRow{ - Date: key, - UserID: id, - }) - } - } - - return rs, nil -} -func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) { - if err := validateDatabaseType(arg); err != nil { - return database.GetTemplateAverageBuildTimeRow{}, err - } - - var emptyRow database.GetTemplateAverageBuildTimeRow - var ( - startTimes []float64 - stopTimes []float64 - deleteTimes []float64 - ) - q.mutex.RLock() - defer q.mutex.RUnlock() - for _, wb := range q.workspaceBuilds { - version, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID) - if err != nil { - return emptyRow, err - } - if version.TemplateID != arg.TemplateID { + if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { continue } - job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID) - if err != nil { - return emptyRow, err - } - if job.CompletedAt.Valid { - took := job.CompletedAt.Time.Sub(job.StartedAt.Time).Seconds() - switch wb.Transition { - case database.WorkspaceTransitionStart: - startTimes = append(startTimes, took) - case database.WorkspaceTransitionStop: - stopTimes = append(stopTimes, took) - case database.WorkspaceTransitionDelete: - deleteTimes = append(deleteTimes, took) + if arg.Status != "" { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) } - } - } - tryPercentile := func(fs []float64, p float64) float64 { - if len(fs) == 0 { - return -1 - } - sort.Float64s(fs) - return fs[int(float64(len(fs))*p/100)] - } - - var row database.GetTemplateAverageBuildTimeRow - row.Delete50, row.Delete95 = tryPercentile(deleteTimes, 50), tryPercentile(deleteTimes, 95) - row.Stop50, row.Stop95 = tryPercentile(stopTimes, 50), tryPercentile(stopTimes, 95) - row.Start50, row.Start95 = tryPercentile(startTimes, 50), tryPercentile(startTimes, 95) - return row, nil -} - -func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, apiKey := range q.apiKeys { - if apiKey.ID == id { - return apiKey, nil - } - } - return database.APIKey{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetAPIKeyByName(_ context.Context, params database.GetAPIKeyByNameParams) (database.APIKey, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - if params.TokenName == "" { - return database.APIKey{}, sql.ErrNoRows - } - for _, apiKey := range q.apiKeys { - if params.UserID == apiKey.UserID && params.TokenName == apiKey.TokenName { - return apiKey, nil - } - } - return database.APIKey{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time) ([]database.APIKey, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - apiKeys := make([]database.APIKey, 0) - for _, key := range q.apiKeys { - if key.LastUsed.After(after) { - apiKeys = append(apiKeys, key) - } - } - return apiKeys, nil -} - -func (q *fakeQuerier) GetAPIKeysByLoginType(_ context.Context, t database.LoginType) ([]database.APIKey, error) { - if err := validateDatabaseType(t); err != nil { - return nil, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - apiKeys := make([]database.APIKey, 0) - for _, key := range q.apiKeys { - if key.LoginType == t { - apiKeys = append(apiKeys, key) - } - } - return apiKeys, nil -} + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job: %w", err) + } -func (q *fakeQuerier) GetAPIKeysByUserID(_ context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + // This logic should match the logic in the workspace.sql file. + var statusMatch bool + switch database.WorkspaceStatus(arg.Status) { + case database.WorkspaceStatusPending: + statusMatch = isNull(job.StartedAt) + case database.WorkspaceStatusStarting: + statusMatch = isNotNull(job.StartedAt) && + isNull(job.CanceledAt) && + isNull(job.CompletedAt) && + time.Since(job.UpdatedAt) < 30*time.Second && + build.Transition == database.WorkspaceTransitionStart - apiKeys := make([]database.APIKey, 0) - for _, key := range q.apiKeys { - if key.UserID == params.UserID && key.LoginType == params.LoginType { - apiKeys = append(apiKeys, key) - } - } - return apiKeys, nil -} + case database.WorkspaceStatusRunning: + statusMatch = isNotNull(job.CompletedAt) && + isNull(job.CanceledAt) && + isNull(job.Error) && + build.Transition == database.WorkspaceTransitionStart -func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error { - q.mutex.Lock() - defer q.mutex.Unlock() + case database.WorkspaceStatusStopping: + statusMatch = isNotNull(job.StartedAt) && + isNull(job.CanceledAt) && + isNull(job.CompletedAt) && + time.Since(job.UpdatedAt) < 30*time.Second && + build.Transition == database.WorkspaceTransitionStop - for index, apiKey := range q.apiKeys { - if apiKey.ID != id { - continue - } - q.apiKeys[index] = q.apiKeys[len(q.apiKeys)-1] - q.apiKeys = q.apiKeys[:len(q.apiKeys)-1] - return nil - } - return sql.ErrNoRows -} + case database.WorkspaceStatusStopped: + statusMatch = isNotNull(job.CompletedAt) && + isNull(job.CanceledAt) && + isNull(job.Error) && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusFailed: + statusMatch = (isNotNull(job.CanceledAt) && isNotNull(job.Error)) || + (isNotNull(job.CompletedAt) && isNotNull(job.Error)) -func (q *fakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, userID uuid.UUID) error { - q.mutex.Lock() - defer q.mutex.Unlock() + case database.WorkspaceStatusCanceling: + statusMatch = isNotNull(job.CanceledAt) && + isNull(job.CompletedAt) - for i := len(q.apiKeys) - 1; i >= 0; i-- { - if q.apiKeys[i].UserID == userID && q.apiKeys[i].Scope == database.APIKeyScopeApplicationConnect { - q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...) - } - } + case database.WorkspaceStatusCanceled: + statusMatch = isNotNull(job.CanceledAt) && + isNotNull(job.CompletedAt) - return nil -} + case database.WorkspaceStatusDeleted: + statusMatch = isNotNull(job.StartedAt) && + isNull(job.CanceledAt) && + isNotNull(job.CompletedAt) && + time.Since(job.UpdatedAt) < 30*time.Second && + build.Transition == database.WorkspaceTransitionDelete && + isNull(job.Error) -func (q *fakeQuerier) DeleteAPIKeysByUserID(_ context.Context, userID uuid.UUID) error { - q.mutex.Lock() - defer q.mutex.Unlock() + case database.WorkspaceStatusDeleting: + statusMatch = isNull(job.CompletedAt) && + isNull(job.CanceledAt) && + isNull(job.Error) && + build.Transition == database.WorkspaceTransitionDelete - for i := len(q.apiKeys) - 1; i >= 0; i-- { - if q.apiKeys[i].UserID == userID { - q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...) + default: + return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) + } + if !statusMatch { + continue + } } - } - - return nil -} - -func (q *fakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { - if err := validateDatabaseType(arg); err != nil { - return database.File{}, err - } - q.mutex.RLock() - defer q.mutex.RUnlock() + if arg.HasAgent != "" { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) + } - for _, file := range q.files { - if file.Hash == arg.Hash && file.CreatedBy == arg.CreatedBy { - return file, nil - } - } - return database.File{}, sql.ErrNoRows -} + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job: %w", err) + } -func (q *fakeQuerier) GetFileByID(_ context.Context, id uuid.UUID) (database.File, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + workspaceResources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, job.ID) + if err != nil { + return nil, xerrors.Errorf("get workspace resources: %w", err) + } - for _, file := range q.files { - if file.ID == id { - return file, nil - } - } - return database.File{}, sql.ErrNoRows -} + var workspaceResourceIDs []uuid.UUID + for _, wr := range workspaceResources { + workspaceResourceIDs = append(workspaceResourceIDs, wr.ID) + } -func (q *fakeQuerier) GetFileTemplates(_ context.Context, id uuid.UUID) ([]database.GetFileTemplatesRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + workspaceAgents, err := q.getWorkspaceAgentsByResourceIDsNoLock(ctx, workspaceResourceIDs) + if err != nil { + return nil, xerrors.Errorf("get workspace agents: %w", err) + } - rows := make([]database.GetFileTemplatesRow, 0) - var file database.File - for _, f := range q.files { - if f.ID == id { - file = f - break - } - } - if file.Hash == "" { - return rows, nil - } + var hasAgentMatched bool + for _, wa := range workspaceAgents { + if mapAgentStatus(wa, arg.AgentInactiveDisconnectTimeoutSeconds) == arg.HasAgent { + hasAgentMatched = true + } + } - for _, job := range q.provisionerJobs { - if job.FileID == id { - for _, version := range q.templateVersions { - if version.JobID == job.ID { - for _, template := range q.templates { - if template.ID == version.TemplateID.UUID { - rows = append(rows, database.GetFileTemplatesRow{ - FileID: file.ID, - FileCreatedBy: file.CreatedBy, - TemplateID: template.ID, - TemplateOrganizationID: template.OrganizationID, - TemplateCreatedBy: template.CreatedBy, - UserACL: template.UserACL, - GroupACL: template.GroupACL, - }) - } - } + if !hasAgentMatched { + continue + } + } + + if len(arg.TemplateIds) > 0 { + match := false + for _, id := range arg.TemplateIds { + if workspace.TemplateID == id { + match = true + break } } + if !match { + continue + } } - } - return rows, nil -} + // If the filter exists, ensure the object is authorized. + if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { + continue + } + workspaces = append(workspaces, workspace) + } -func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { - if err := validateDatabaseType(arg); err != nil { - return database.User{}, err + // Sort workspaces (ORDER BY) + isRunning := func(build database.WorkspaceBuild, job database.ProvisionerJob) bool { + return job.CompletedAt.Valid && !job.CanceledAt.Valid && !job.Error.Valid && build.Transition == database.WorkspaceTransitionStart } - q.mutex.RLock() - defer q.mutex.RUnlock() + preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} + preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} + preloadedUsers := map[uuid.UUID]database.User{} - for _, user := range q.users { - if !user.Deleted && (strings.EqualFold(user.Email, arg.Email) || strings.EqualFold(user.Username, arg.Username)) { - return user, nil + for _, w := range workspaces { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, w.ID) + if err == nil { + preloadedWorkspaceBuilds[w.ID] = build + } else if !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get latest build: %w", err) } - } - return database.User{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetUserByID(_ context.Context, id uuid.UUID) (database.User, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - return q.getUserByIDNoLock(id) -} + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err == nil { + preloadedProvisionerJobs[w.ID] = job + } else if !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get provisioner job: %w", err) + } -// getUserByIDNoLock is used by other functions in the database fake. -func (q *fakeQuerier) getUserByIDNoLock(id uuid.UUID) (database.User, error) { - for _, user := range q.users { - if user.ID == id { - return user, nil + user, err := q.getUserByIDNoLock(w.OwnerID) + if err == nil { + preloadedUsers[w.ID] = user + } else if !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get user: %w", err) } } - return database.User{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + sort.Slice(workspaces, func(i, j int) bool { + w1 := workspaces[i] + w2 := workspaces[j] - existing := int64(0) - for _, u := range q.users { - if !u.Deleted { - existing++ - } - } - return existing, nil -} + // Order by: running first + w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) + w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) -func (q *fakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + if w1IsRunning && !w2IsRunning { + return true + } - active := int64(0) - for _, u := range q.users { - if u.Status == database.UserStatusActive && !u.Deleted { - active++ + if !w1IsRunning && w2IsRunning { + return false } - } - return active, nil -} -func (q *fakeQuerier) GetFilteredUserCount(ctx context.Context, arg database.GetFilteredUserCountParams) (int64, error) { - if err := validateDatabaseType(arg); err != nil { - return 0, err - } - count, err := q.GetAuthorizedUserCount(ctx, arg, nil) - return count, err -} + // Order by: usernames + if w1.ID != w2.ID { + return sort.StringsAreSorted([]string{preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username}) + } -func (q *fakeQuerier) GetAuthorizedUserCount(ctx context.Context, params database.GetFilteredUserCountParams, prepared rbac.PreparedAuthorized) (int64, error) { - if err := validateDatabaseType(params); err != nil { - return 0, err - } + // Order by: workspace names + return sort.StringsAreSorted([]string{w1.Name, w2.Name}) + }) - q.mutex.RLock() - defer q.mutex.RUnlock() + beforePageCount := len(workspaces) - // Call this to match the same function calls as the SQL implementation. - if prepared != nil { - _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL()) - if err != nil { - return -1, err + if arg.Offset > 0 { + if int(arg.Offset) > len(workspaces) { + return []database.GetWorkspacesRow{}, nil } + workspaces = workspaces[arg.Offset:] + } + if arg.Limit > 0 { + if int(arg.Limit) > len(workspaces) { + return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil + } + workspaces = workspaces[:arg.Limit] } - users := make([]database.User, 0, len(q.users)) + return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil +} - for _, user := range q.users { - // If the filter exists, ensure the object is authorized. - if prepared != nil && prepared.Authorize(ctx, user.RBACObject()) != nil { - continue +// mapAgentStatus determines the agent status based on different timestamps like created_at, last_connected_at, disconnected_at, etc. +// The function must be in sync with: coderd/workspaceagents.go:convertWorkspaceAgent. +func mapAgentStatus(dbAgent database.WorkspaceAgent, agentInactiveDisconnectTimeoutSeconds int64) string { + var status string + connectionTimeout := time.Duration(dbAgent.ConnectionTimeoutSeconds) * time.Second + switch { + case !dbAgent.FirstConnectedAt.Valid: + switch { + case connectionTimeout > 0 && database.Now().Sub(dbAgent.CreatedAt) > connectionTimeout: + // If the agent took too long to connect the first time, + // mark it as timed out. + status = "timeout" + default: + // If the agent never connected, it's waiting for the compute + // to start up. + status = "connecting" } + case dbAgent.DisconnectedAt.Time.After(dbAgent.LastConnectedAt.Time): + // If we've disconnected after our last connection, we know the + // agent is no longer connected. + status = "disconnected" + case database.Now().Sub(dbAgent.LastConnectedAt.Time) > time.Duration(agentInactiveDisconnectTimeoutSeconds)*time.Second: + // The connection died without updating the last connected. + status = "disconnected" + case dbAgent.LastConnectedAt.Valid: + // The agent should be assumed connected if it's under inactivity timeouts + // and last connected at has been properly set. + status = "connected" + default: + panic("unknown agent status: " + status) + } + return status +} - users = append(users, user) +func convertToWorkspaceRows(workspaces []database.Workspace, count int64) []database.GetWorkspacesRow { + rows := make([]database.GetWorkspacesRow, len(workspaces)) + for i, w := range workspaces { + rows[i] = database.GetWorkspacesRow{ + ID: w.ID, + CreatedAt: w.CreatedAt, + UpdatedAt: w.UpdatedAt, + OwnerID: w.OwnerID, + OrganizationID: w.OrganizationID, + TemplateID: w.TemplateID, + Deleted: w.Deleted, + Name: w.Name, + AutostartSchedule: w.AutostartSchedule, + Ttl: w.Ttl, + LastUsedAt: w.LastUsedAt, + Count: count, + } } + return rows +} - // Filter out deleted since they should never be returned.. - tmp := make([]database.User, 0, len(users)) - for _, user := range users { - if !user.Deleted { - tmp = append(tmp, user) +func (q *fakeQuerier) getWorkspaceByIDNoLock(_ context.Context, id uuid.UUID) (database.Workspace, error) { + for _, workspace := range q.workspaces { + if workspace.ID == id { + return workspace, nil } } - users = tmp + return database.Workspace{}, sql.ErrNoRows +} - if params.Search != "" { - tmp := make([]database.User, 0, len(users)) - for i, user := range users { - if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) { - tmp = append(tmp, users[i]) - } else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) { - tmp = append(tmp, users[i]) - } +func (q *fakeQuerier) getWorkspaceByAgentIDNoLock(_ context.Context, agentID uuid.UUID) (database.Workspace, error) { + var agent database.WorkspaceAgent + for _, _agent := range q.workspaceAgents { + if _agent.ID == agentID { + agent = _agent + break } - users = tmp + } + if agent.ID == uuid.Nil { + return database.Workspace{}, sql.ErrNoRows } - if len(params.Status) > 0 { - usersFilteredByStatus := make([]database.User, 0, len(users)) - for i, user := range users { - if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool { - return strings.EqualFold(string(a), string(b)) - }) { - usersFilteredByStatus = append(usersFilteredByStatus, users[i]) - } + var resource database.WorkspaceResource + for _, _resource := range q.workspaceResources { + if _resource.ID == agent.ResourceID { + resource = _resource + break } - users = usersFilteredByStatus + } + if resource.ID == uuid.Nil { + return database.Workspace{}, sql.ErrNoRows } - if len(params.RbacRole) > 0 && !slice.Contains(params.RbacRole, rbac.RoleMember()) { - usersFilteredByRole := make([]database.User, 0, len(users)) - for i, user := range users { - if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) { - usersFilteredByRole = append(usersFilteredByRole, users[i]) - } + var build database.WorkspaceBuild + for _, _build := range q.workspaceBuilds { + if _build.JobID == resource.JobID { + build = _build + break } + } + if build.ID == uuid.Nil { + return database.Workspace{}, sql.ErrNoRows + } - users = usersFilteredByRole + for _, workspace := range q.workspaces { + if workspace.ID == build.WorkspaceID { + return workspace, nil + } } - return int64(len(users)), nil + return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error { - if err := validateDatabaseType(params); err != nil { - return err +func (q *fakeQuerier) getWorkspaceBuildByIDNoLock(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { + for _, history := range q.workspaceBuilds { + if history.ID == id { + return history, nil + } } + return database.WorkspaceBuild{}, sql.ErrNoRows +} - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { + var row database.WorkspaceBuild + var buildNum int32 = -1 + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { + row = workspaceBuild + buildNum = workspaceBuild.BuildNumber + } + } + if buildNum == -1 { + return database.WorkspaceBuild{}, sql.ErrNoRows + } + return row, nil +} - for i, u := range q.users { - if u.ID == params.ID { - u.Deleted = params.Deleted - q.users[i] = u - // NOTE: In the real world, this is done by a trigger. - for i, k := range q.apiKeys { - if k.UserID == u.ID { - q.apiKeys[i] = q.apiKeys[len(q.apiKeys)-1] - q.apiKeys = q.apiKeys[:len(q.apiKeys)-1] - } - } - return nil +func (q *fakeQuerier) getTemplateByIDNoLock(_ context.Context, id uuid.UUID) (database.Template, error) { + for _, template := range q.templates { + if template.ID == id { + return template.DeepCopy(), nil } } - return sql.ErrNoRows + return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.GetUsersRow, error) { - if err := validateDatabaseType(params); err != nil { +func (q *fakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) { + if err := validateDatabaseType(arg); err != nil { return nil, err } q.mutex.RLock() defer q.mutex.RUnlock() - // Avoid side-effect of sorting. - users := make([]database.User, len(q.users)) - copy(users, q.users) - - // Database orders by username - slices.SortFunc(users, func(a, b database.User) bool { - return strings.ToLower(a.Username) < strings.ToLower(b.Username) - }) - - // Filter out deleted since they should never be returned.. - tmp := make([]database.User, 0, len(users)) - for _, user := range users { - if !user.Deleted { - tmp = append(tmp, user) + // Call this to match the same function calls as the SQL implementation. + if prepared != nil { + _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithACL()) + if err != nil { + return nil, err } } - users = tmp - if params.AfterID != uuid.Nil { - found := false - for i, v := range users { - if v.ID == params.AfterID { - // We want to return all users after index i. - users = users[i+1:] - found = true - break - } + var templates []database.Template + for _, template := range q.templates { + if prepared != nil && prepared.Authorize(ctx, template.RBACObject()) != nil { + continue } - // If no users after the time, then we return an empty list. - if !found { - return []database.GetUsersRow{}, nil + if template.Deleted != arg.Deleted { + continue } - } - - if params.Search != "" { - tmp := make([]database.User, 0, len(users)) - for i, user := range users { - if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) { - tmp = append(tmp, users[i]) - } else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) { - tmp = append(tmp, users[i]) - } + if arg.OrganizationID != uuid.Nil && template.OrganizationID != arg.OrganizationID { + continue } - users = tmp - } - if len(params.Status) > 0 { - usersFilteredByStatus := make([]database.User, 0, len(users)) - for i, user := range users { - if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool { - return strings.EqualFold(string(a), string(b)) - }) { - usersFilteredByStatus = append(usersFilteredByStatus, users[i]) - } + if arg.ExactName != "" && !strings.EqualFold(template.Name, arg.ExactName) { + continue } - users = usersFilteredByStatus - } - if len(params.RbacRole) > 0 && !slice.Contains(params.RbacRole, rbac.RoleMember()) { - usersFilteredByRole := make([]database.User, 0, len(users)) - for i, user := range users { - if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) { - usersFilteredByRole = append(usersFilteredByRole, users[i]) + if len(arg.IDs) > 0 { + match := false + for _, id := range arg.IDs { + if template.ID == id { + match = true + break + } + } + if !match { + continue } } - users = usersFilteredByRole - } - - beforePageCount := len(users) - - if params.OffsetOpt > 0 { - if int(params.OffsetOpt) > len(users)-1 { - return []database.GetUsersRow{}, nil - } - users = users[params.OffsetOpt:] - } - - if params.LimitOpt > 0 { - if int(params.LimitOpt) > len(users) { - params.LimitOpt = int32(len(users)) - } - users = users[:params.LimitOpt] + templates = append(templates, template.DeepCopy()) } - - return convertUsers(users, int64(beforePageCount)), nil -} - -func convertUsers(users []database.User, count int64) []database.GetUsersRow { - rows := make([]database.GetUsersRow, len(users)) - for i, u := range users { - rows[i] = database.GetUsersRow{ - ID: u.ID, - Email: u.Email, - Username: u.Username, - HashedPassword: u.HashedPassword, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - Status: u.Status, - RBACRoles: u.RBACRoles, - LoginType: u.LoginType, - AvatarURL: u.AvatarURL, - Deleted: u.Deleted, - LastSeenAt: u.LastSeenAt, - Count: count, - } + if len(templates) > 0 { + slices.SortFunc(templates, func(i, j database.Template) bool { + if i.Name != j.Name { + return i.Name < j.Name + } + return i.ID.String() < j.ID.String() + }) + return templates, nil } - return rows + return nil, sql.ErrNoRows } -func (q *fakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]database.User, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - users := make([]database.User, 0) - for _, user := range q.users { - for _, id := range ids { - if user.ID != id { - continue - } - users = append(users, user) +func (q *fakeQuerier) getTemplateVersionByIDNoLock(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { + for _, templateVersion := range q.templateVersions { + if templateVersion.ID != templateVersionID { + continue } + return templateVersion, nil } - return users, nil + return database.TemplateVersion{}, sql.ErrNoRows } -func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.UUID) (database.GetAuthorizationUserRolesRow, error) { +func (q *fakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]database.TemplateUser, error) { q.mutex.RLock() defer q.mutex.RUnlock() - var user *database.User - roles := make([]string, 0) - for _, u := range q.users { - if u.ID == userID { - u := u - roles = append(roles, u.RBACRoles...) - roles = append(roles, "member") - user = &u + var template database.Template + for _, t := range q.templates { + if t.ID == id { + template = t break } } - for _, mem := range q.organizationMembers { - if mem.UserID == userID { - roles = append(roles, mem.Roles...) - roles = append(roles, "organization-member:"+mem.OrganizationID.String()) - } + if template.ID == uuid.Nil { + return nil, sql.ErrNoRows } - var groups []string - for _, member := range q.groupMembers { - if member.UserID == userID { - groups = append(groups, member.GroupID.String()) + users := make([]database.TemplateUser, 0, len(template.UserACL)) + for k, v := range template.UserACL { + user, err := q.getUserByIDNoLock(uuid.MustParse(k)) + if err != nil && xerrors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get user by ID: %w", err) + } + // We don't delete users from the map if they + // get deleted so just skip. + if xerrors.Is(err, sql.ErrNoRows) { + continue } - } - - if user == nil { - return database.GetAuthorizationUserRolesRow{}, sql.ErrNoRows - } - - return database.GetAuthorizationUserRolesRow{ - ID: userID, - Username: user.Username, - Status: user.Status, - Roles: roles, - Groups: groups, - }, nil -} -func (q *fakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err + if user.Deleted || user.Status == database.UserStatusSuspended { + continue + } + + users = append(users, database.TemplateUser{ + User: user, + Actions: v, + }) } - // A nil auth filter means no auth filter. - workspaceRows, err := q.GetAuthorizedWorkspaces(ctx, arg, nil) - return workspaceRows, err + return users, nil } -//nolint:gocyclo -func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err - } - +func (q *fakeQuerier) GetTemplateGroupRoles(_ context.Context, id uuid.UUID) ([]database.TemplateGroup, error) { q.mutex.RLock() defer q.mutex.RUnlock() - if prepared != nil { - // Call this to match the same function calls as the SQL implementation. - _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL()) - if err != nil { - return nil, err + var template database.Template + for _, t := range q.templates { + if t.ID == id { + template = t + break } } - workspaces := make([]database.Workspace, 0) - for _, workspace := range q.workspaces { - if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { + if template.ID == uuid.Nil { + return nil, sql.ErrNoRows + } + + groups := make([]database.TemplateGroup, 0, len(template.GroupACL)) + for k, v := range template.GroupACL { + group, err := q.getGroupByIDNoLock(context.Background(), uuid.MustParse(k)) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get group by ID: %w", err) + } + // We don't delete groups from the map if they + // get deleted so just skip. + if xerrors.Is(err, sql.ErrNoRows) { continue } - if arg.OwnerUsername != "" { - owner, err := q.getUserByIDNoLock(workspace.OwnerID) - if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { - continue - } + groups = append(groups, database.TemplateGroup{ + Group: group, + Actions: v, + }) + } + + return groups, nil +} + +func (q *fakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { + // The schema sorts this by created at, so we iterate the array backwards. + for i := len(q.workspaceAgents) - 1; i >= 0; i-- { + agent := q.workspaceAgents[i] + if agent.ID == id { + return agent, nil } + } + return database.WorkspaceAgent{}, sql.ErrNoRows +} - if arg.TemplateName != "" { - template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) - if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { +func (q *fakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { + workspaceAgents := make([]database.WorkspaceAgent, 0) + for _, agent := range q.workspaceAgents { + for _, resourceID := range resourceIDs { + if agent.ResourceID != resourceID { continue } + workspaceAgents = append(workspaceAgents, agent) } + } + return workspaceAgents, nil +} - if !arg.Deleted && workspace.Deleted { +func (q *fakeQuerier) getProvisionerJobByIDNoLock(_ context.Context, id uuid.UUID) (database.ProvisionerJob, error) { + for _, provisionerJob := range q.provisionerJobs { + if provisionerJob.ID != id { continue } + return provisionerJob, nil + } + return database.ProvisionerJob{}, sql.ErrNoRows +} - if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { +func (q *fakeQuerier) getWorkspaceResourcesByJobIDNoLock(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { + resources := make([]database.WorkspaceResource, 0) + for _, resource := range q.workspaceResources { + if resource.JobID != jobID { continue } + resources = append(resources, resource) + } + return resources, nil +} - if arg.Status != "" { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return nil, xerrors.Errorf("get latest build: %w", err) - } - - job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) - if err != nil { - return nil, xerrors.Errorf("get provisioner job: %w", err) - } - - // This logic should match the logic in the workspace.sql file. - var statusMatch bool - switch database.WorkspaceStatus(arg.Status) { - case database.WorkspaceStatusPending: - statusMatch = isNull(job.StartedAt) - case database.WorkspaceStatusStarting: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && - build.Transition == database.WorkspaceTransitionStart - - case database.WorkspaceStatusRunning: - statusMatch = isNotNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && - build.Transition == database.WorkspaceTransitionStart +func (q *fakeQuerier) getGroupByIDNoLock(_ context.Context, id uuid.UUID) (database.Group, error) { + for _, group := range q.groups { + if group.ID == id { + return group, nil + } + } - case database.WorkspaceStatusStopping: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && - build.Transition == database.WorkspaceTransitionStop + return database.Group{}, sql.ErrNoRows +} - case database.WorkspaceStatusStopped: - statusMatch = isNotNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusFailed: - statusMatch = (isNotNull(job.CanceledAt) && isNotNull(job.Error)) || - (isNotNull(job.CompletedAt) && isNotNull(job.Error)) +// isNull is only used in dbfake, so reflect is ok. Use this to make the logic +// look more similar to the postgres. +func isNull(v interface{}) bool { + return !isNotNull(v) +} - case database.WorkspaceStatusCanceling: - statusMatch = isNotNull(job.CanceledAt) && - isNull(job.CompletedAt) +func isNotNull(v interface{}) bool { + return reflect.ValueOf(v).FieldByName("Valid").Bool() +} - case database.WorkspaceStatusCanceled: - statusMatch = isNotNull(job.CanceledAt) && - isNotNull(job.CompletedAt) +func (*fakeQuerier) AcquireLock(_ context.Context, _ int64) error { + return xerrors.New("AcquireLock must only be called within a transaction") +} - case database.WorkspaceStatusDeleted: - statusMatch = isNotNull(job.StartedAt) && - isNull(job.CanceledAt) && - isNotNull(job.CompletedAt) && - time.Since(job.UpdatedAt) < 30*time.Second && - build.Transition == database.WorkspaceTransitionDelete && - isNull(job.Error) +func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.AcquireProvisionerJobParams) (database.ProvisionerJob, error) { + if err := validateDatabaseType(arg); err != nil { + return database.ProvisionerJob{}, err + } - case database.WorkspaceStatusDeleting: - statusMatch = isNull(job.CompletedAt) && - isNull(job.CanceledAt) && - isNull(job.Error) && - build.Transition == database.WorkspaceTransitionDelete + q.mutex.Lock() + defer q.mutex.Unlock() - default: - return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) - } - if !statusMatch { + for index, provisionerJob := range q.provisionerJobs { + if provisionerJob.StartedAt.Valid { + continue + } + found := false + for _, provisionerType := range arg.Types { + if provisionerJob.Provisioner != provisionerType { continue } + found = true + break } - - if arg.HasAgent != "" { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return nil, xerrors.Errorf("get latest build: %w", err) - } - - job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) - if err != nil { - return nil, xerrors.Errorf("get provisioner job: %w", err) - } - - workspaceResources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, job.ID) - if err != nil { - return nil, xerrors.Errorf("get workspace resources: %w", err) - } - - var workspaceResourceIDs []uuid.UUID - for _, wr := range workspaceResources { - workspaceResourceIDs = append(workspaceResourceIDs, wr.ID) - } - - workspaceAgents, err := q.getWorkspaceAgentsByResourceIDsNoLock(ctx, workspaceResourceIDs) + if !found { + continue + } + tags := map[string]string{} + if arg.Tags != nil { + err := json.Unmarshal(arg.Tags, &tags) if err != nil { - return nil, xerrors.Errorf("get workspace agents: %w", err) - } - - var hasAgentMatched bool - for _, wa := range workspaceAgents { - if mapAgentStatus(wa, arg.AgentInactiveDisconnectTimeoutSeconds) == arg.HasAgent { - hasAgentMatched = true - } - } - - if !hasAgentMatched { - continue + return provisionerJob, xerrors.Errorf("unmarshal: %w", err) } } - if len(arg.TemplateIds) > 0 { - match := false - for _, id := range arg.TemplateIds { - if workspace.TemplateID == id { - match = true - break - } + missing := false + for key, value := range provisionerJob.Tags { + provided, found := tags[key] + if !found { + missing = true + break } - if !match { - continue + if provided != value { + missing = true + break } } + if missing { + continue + } + provisionerJob.StartedAt = arg.StartedAt + provisionerJob.UpdatedAt = arg.StartedAt.Time + provisionerJob.WorkerID = arg.WorkerID + q.provisionerJobs[index] = provisionerJob + return provisionerJob, nil + } + return database.ProvisionerJob{}, sql.ErrNoRows +} - // If the filter exists, ensure the object is authorized. - if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { +func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, apiKey := range q.apiKeys { + if apiKey.ID != id { continue } - workspaces = append(workspaces, workspace) + q.apiKeys[index] = q.apiKeys[len(q.apiKeys)-1] + q.apiKeys = q.apiKeys[:len(q.apiKeys)-1] + return nil } + return sql.ErrNoRows +} - // Sort workspaces (ORDER BY) - isRunning := func(build database.WorkspaceBuild, job database.ProvisionerJob) bool { - return job.CompletedAt.Valid && !job.CanceledAt.Valid && !job.Error.Valid && build.Transition == database.WorkspaceTransitionStart +func (q *fakeQuerier) DeleteAPIKeysByUserID(_ context.Context, userID uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i := len(q.apiKeys) - 1; i >= 0; i-- { + if q.apiKeys[i].UserID == userID { + q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...) + } } - preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} - preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} - preloadedUsers := map[uuid.UUID]database.User{} + return nil +} - for _, w := range workspaces { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, w.ID) - if err == nil { - preloadedWorkspaceBuilds[w.ID] = build - } else if !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get latest build: %w", err) +func (q *fakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, userID uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i := len(q.apiKeys) - 1; i >= 0; i-- { + if q.apiKeys[i].UserID == userID && q.apiKeys[i].Scope == database.APIKeyScopeApplicationConnect { + q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...) } + } - job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) - if err == nil { - preloadedProvisionerJobs[w.ID] = job - } else if !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get provisioner job: %w", err) + return nil +} + +func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, key := range q.gitSSHKey { + if key.UserID != userID { + continue } + q.gitSSHKey[index] = q.gitSSHKey[len(q.gitSSHKey)-1] + q.gitSSHKey = q.gitSSHKey[:len(q.gitSSHKey)-1] + return nil + } + return sql.ErrNoRows +} - user, err := q.getUserByIDNoLock(w.OwnerID) - if err == nil { - preloadedUsers[w.ID] = user - } else if !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get user: %w", err) +func (q *fakeQuerier) DeleteGroupByID(_ context.Context, id uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, group := range q.groups { + if group.ID == id { + q.groups = append(q.groups[:i], q.groups[i+1:]...) + return nil } } - sort.Slice(workspaces, func(i, j int) bool { - w1 := workspaces[i] - w2 := workspaces[j] + return sql.ErrNoRows +} - // Order by: running first - w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) - w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) +func (q *fakeQuerier) DeleteGroupMemberFromGroup(_ context.Context, arg database.DeleteGroupMemberFromGroupParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() - if w1IsRunning && !w2IsRunning { - return true + for i, member := range q.groupMembers { + if member.UserID == arg.UserID && member.GroupID == arg.GroupID { + q.groupMembers = append(q.groupMembers[:i], q.groupMembers[i+1:]...) } + } + return nil +} - if !w1IsRunning && w2IsRunning { - return false - } +func (q *fakeQuerier) DeleteGroupMembersByOrgAndUser(_ context.Context, arg database.DeleteGroupMembersByOrgAndUserParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() - // Order by: usernames - if w1.ID != w2.ID { - return sort.StringsAreSorted([]string{preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username}) + newMembers := q.groupMembers[:0] + for _, member := range q.groupMembers { + if member.UserID != arg.UserID { + // Do not delete the other members + newMembers = append(newMembers, member) + } else if member.UserID == arg.UserID { + // We only want to delete from groups in the organization in the args. + for _, group := range q.groups { + // Find the group that the member is apartof. + if group.ID == member.GroupID { + // Only add back the member if the organization ID does not match + // the arg organization ID. Since the arg is saying which + // org to delete. + if group.OrganizationID != arg.OrganizationID { + newMembers = append(newMembers, member) + } + break + } + } } + } + q.groupMembers = newMembers - // Order by: workspace names - return sort.StringsAreSorted([]string{w1.Name, w2.Name}) - }) + return nil +} - beforePageCount := len(workspaces) +func (q *fakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error) { + q.mutex.Lock() + defer q.mutex.Unlock() - if arg.Offset > 0 { - if int(arg.Offset) > len(workspaces) { - return []database.GetWorkspacesRow{}, nil + for index, l := range q.licenses { + if l.ID == id { + q.licenses[index] = q.licenses[len(q.licenses)-1] + q.licenses = q.licenses[:len(q.licenses)-1] + return id, nil } - workspaces = workspaces[arg.Offset:] } - if arg.Limit > 0 { - if int(arg.Limit) > len(workspaces) { - return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil + return 0, sql.ErrNoRows +} + +func (*fakeQuerier) DeleteOldWorkspaceAgentStartupLogs(_ context.Context) error { + // noop + return nil +} + +func (*fakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { + // no-op + return nil +} + +func (q *fakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, replica := range q.replicas { + if replica.UpdatedAt.Before(before) { + q.replicas = append(q.replicas[:i], q.replicas[i+1:]...) } - workspaces = workspaces[:arg.Limit] } - return convertToWorkspaceRows(workspaces, int64(beforePageCount)), nil + return nil } -// mapAgentStatus determines the agent status based on different timestamps like created_at, last_connected_at, disconnected_at, etc. -// The function must be in sync with: coderd/workspaceagents.go:convertWorkspaceAgent. -func mapAgentStatus(dbAgent database.WorkspaceAgent, agentInactiveDisconnectTimeoutSeconds int64) string { - var status string - connectionTimeout := time.Duration(dbAgent.ConnectionTimeoutSeconds) * time.Second - switch { - case !dbAgent.FirstConnectedAt.Valid: - switch { - case connectionTimeout > 0 && database.Now().Sub(dbAgent.CreatedAt) > connectionTimeout: - // If the agent took too long to connect the first time, - // mark it as timed out. - status = "timeout" - default: - // If the agent never connected, it's waiting for the compute - // to start up. - status = "connecting" +func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, apiKey := range q.apiKeys { + if apiKey.ID == id { + return apiKey, nil } - case dbAgent.DisconnectedAt.Time.After(dbAgent.LastConnectedAt.Time): - // If we've disconnected after our last connection, we know the - // agent is no longer connected. - status = "disconnected" - case database.Now().Sub(dbAgent.LastConnectedAt.Time) > time.Duration(agentInactiveDisconnectTimeoutSeconds)*time.Second: - // The connection died without updating the last connected. - status = "disconnected" - case dbAgent.LastConnectedAt.Valid: - // The agent should be assumed connected if it's under inactivity timeouts - // and last connected at has been properly set. - status = "connected" - default: - panic("unknown agent status: " + status) } - return status + return database.APIKey{}, sql.ErrNoRows } -func convertToWorkspaceRows(workspaces []database.Workspace, count int64) []database.GetWorkspacesRow { - rows := make([]database.GetWorkspacesRow, len(workspaces)) - for i, w := range workspaces { - rows[i] = database.GetWorkspacesRow{ - ID: w.ID, - CreatedAt: w.CreatedAt, - UpdatedAt: w.UpdatedAt, - OwnerID: w.OwnerID, - OrganizationID: w.OrganizationID, - TemplateID: w.TemplateID, - Deleted: w.Deleted, - Name: w.Name, - AutostartSchedule: w.AutostartSchedule, - Ttl: w.Ttl, - LastUsedAt: w.LastUsedAt, - Count: count, +func (q *fakeQuerier) GetAPIKeyByName(_ context.Context, params database.GetAPIKeyByNameParams) (database.APIKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if params.TokenName == "" { + return database.APIKey{}, sql.ErrNoRows + } + for _, apiKey := range q.apiKeys { + if params.UserID == apiKey.UserID && params.TokenName == apiKey.TokenName { + return apiKey, nil + } + } + return database.APIKey{}, sql.ErrNoRows +} + +func (q *fakeQuerier) GetAPIKeysByLoginType(_ context.Context, t database.LoginType) ([]database.APIKey, error) { + if err := validateDatabaseType(t); err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + apiKeys := make([]database.APIKey, 0) + for _, key := range q.apiKeys { + if key.LoginType == t { + apiKeys = append(apiKeys, key) } } - return rows + return apiKeys, nil } -func (q *fakeQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (database.Workspace, error) { +func (q *fakeQuerier) GetAPIKeysByUserID(_ context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getWorkspaceByIDNoLock(ctx, id) -} - -func (q *fakeQuerier) getWorkspaceByIDNoLock(_ context.Context, id uuid.UUID) (database.Workspace, error) { - for _, workspace := range q.workspaces { - if workspace.ID == id { - return workspace, nil + apiKeys := make([]database.APIKey, 0) + for _, key := range q.apiKeys { + if key.UserID == params.UserID && key.LoginType == params.LoginType { + apiKeys = append(apiKeys, key) } } - return database.Workspace{}, sql.ErrNoRows + return apiKeys, nil } -func (q *fakeQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) { +func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time) ([]database.APIKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getWorkspaceByAgentIDNoLock(ctx, agentID) -} - -func (q *fakeQuerier) getWorkspaceByAgentIDNoLock(_ context.Context, agentID uuid.UUID) (database.Workspace, error) { - var agent database.WorkspaceAgent - for _, _agent := range q.workspaceAgents { - if _agent.ID == agentID { - agent = _agent - break + apiKeys := make([]database.APIKey, 0) + for _, key := range q.apiKeys { + if key.LastUsed.After(after) { + apiKeys = append(apiKeys, key) } } - if agent.ID == uuid.Nil { - return database.Workspace{}, sql.ErrNoRows - } + return apiKeys, nil +} - var resource database.WorkspaceResource - for _, _resource := range q.workspaceResources { - if _resource.ID == agent.ResourceID { - resource = _resource - break - } - } - if resource.ID == uuid.Nil { - return database.Workspace{}, sql.ErrNoRows - } +func (q *fakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - var build database.WorkspaceBuild - for _, _build := range q.workspaceBuilds { - if _build.JobID == resource.JobID { - build = _build - break + active := int64(0) + for _, u := range q.users { + if u.Status == database.UserStatusActive && !u.Deleted { + active++ } } - if build.ID == uuid.Nil { - return database.Workspace{}, sql.ErrNoRows - } + return active, nil +} - for _, workspace := range q.workspaces { - if workspace.ID == build.WorkspaceID { - return workspace, nil - } - } +func (q *fakeQuerier) GetAppSecurityKey(_ context.Context) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - return database.Workspace{}, sql.ErrNoRows + return q.appSecurityKey, nil } -func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg database.GetWorkspaceByOwnerIDAndNameParams) (database.Workspace, error) { +func (q *fakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { if err := validateDatabaseType(arg); err != nil { - return database.Workspace{}, err + return nil, err } q.mutex.RLock() defer q.mutex.RUnlock() - var found *database.Workspace - for _, workspace := range q.workspaces { - workspace := workspace - if workspace.OwnerID != arg.OwnerID { + logs := make([]database.GetAuditLogsOffsetRow, 0, arg.Limit) + + // q.auditLogs are already sorted by time DESC, so no need to sort after the fact. + for _, alog := range q.auditLogs { + if arg.Offset > 0 { + arg.Offset-- continue } - if !strings.EqualFold(workspace.Name, arg.Name) { + if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } - if workspace.Deleted != arg.Deleted { + if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { continue } + if arg.ResourceID != uuid.Nil && alog.ResourceID != arg.ResourceID { + continue + } + if arg.Username != "" { + user, err := q.getUserByIDNoLock(alog.UserID) + if err == nil && !strings.EqualFold(arg.Username, user.Username) { + continue + } + } + if arg.Email != "" { + user, err := q.getUserByIDNoLock(alog.UserID) + if err == nil && !strings.EqualFold(arg.Email, user.Email) { + continue + } + } + if !arg.DateFrom.IsZero() { + if alog.Time.Before(arg.DateFrom) { + continue + } + } + if !arg.DateTo.IsZero() { + if alog.Time.After(arg.DateTo) { + continue + } + } + if arg.BuildReason != "" { + workspaceBuild, err := q.getWorkspaceBuildByIDNoLock(context.Background(), alog.ResourceID) + if err == nil && !strings.EqualFold(arg.BuildReason, string(workspaceBuild.Reason)) { + continue + } + } - // Return the most recent workspace with the given name - if found == nil || workspace.CreatedAt.After(found.CreatedAt) { - found = &workspace + user, err := q.getUserByIDNoLock(alog.UserID) + userValid := err == nil + + logs = append(logs, database.GetAuditLogsOffsetRow{ + ID: alog.ID, + RequestID: alog.RequestID, + OrganizationID: alog.OrganizationID, + Ip: alog.Ip, + UserAgent: alog.UserAgent, + ResourceType: alog.ResourceType, + ResourceID: alog.ResourceID, + ResourceTarget: alog.ResourceTarget, + ResourceIcon: alog.ResourceIcon, + Action: alog.Action, + Diff: alog.Diff, + StatusCode: alog.StatusCode, + AdditionalFields: alog.AdditionalFields, + UserID: alog.UserID, + UserUsername: sql.NullString{String: user.Username, Valid: userValid}, + UserEmail: sql.NullString{String: user.Email, Valid: userValid}, + UserCreatedAt: sql.NullTime{Time: user.CreatedAt, Valid: userValid}, + UserStatus: database.NullUserStatus{UserStatus: user.Status, Valid: userValid}, + UserRoles: user.RBACRoles, + Count: 0, + }) + + if len(logs) >= int(arg.Limit) { + break } } - if found != nil { - return *found, nil - } - return database.Workspace{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetWorkspaceByWorkspaceAppID(_ context.Context, workspaceAppID uuid.UUID) (database.Workspace, error) { - if err := validateDatabaseType(workspaceAppID); err != nil { - return database.Workspace{}, err + count := int64(len(logs)) + for i := range logs { + logs[i].Count = count } + return logs, nil +} + +func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.UUID) (database.GetAuthorizationUserRolesRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, workspaceApp := range q.workspaceApps { - workspaceApp := workspaceApp - if workspaceApp.ID == workspaceAppID { - return q.getWorkspaceByAgentIDNoLock(context.Background(), workspaceApp.AgentID) + var user *database.User + roles := make([]string, 0) + for _, u := range q.users { + if u.ID == userID { + u := u + roles = append(roles, u.RBACRoles...) + roles = append(roles, "member") + user = &u + break } } - return database.Workspace{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + for _, mem := range q.organizationMembers { + if mem.UserID == userID { + roles = append(roles, mem.Roles...) + roles = append(roles, "organization-member:"+mem.OrganizationID.String()) + } + } - apps := make([]database.WorkspaceApp, 0) - for _, app := range q.workspaceApps { - if app.AgentID == id { - apps = append(apps, app) + var groups []string + for _, member := range q.groupMembers { + if member.UserID == userID { + groups = append(groups, member.GroupID.String()) } } - if len(apps) == 0 { - return nil, sql.ErrNoRows + + if user == nil { + return database.GetAuthorizationUserRolesRow{}, sql.ErrNoRows } - return apps, nil + + return database.GetAuthorizationUserRolesRow{ + ID: userID, + Username: user.Username, + Status: user.Status, + Roles: roles, + Groups: groups, + }, nil } -func (q *fakeQuerier) GetWorkspaceAppsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceApp, error) { +func (q *fakeQuerier) GetDERPMeshKey(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() - apps := make([]database.WorkspaceApp, 0) - for _, app := range q.workspaceApps { - if app.CreatedAt.After(after) { - apps = append(apps, app) - } - } - return apps, nil + return q.derpMeshKey, nil } -func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { +func (q *fakeQuerier) GetDefaultProxyConfig(_ context.Context) (database.GetDefaultProxyConfigRow, error) { + return database.GetDefaultProxyConfigRow{ + DisplayName: q.defaultProxyDisplayName, + IconUrl: q.defaultProxyIconURL, + }, nil +} + +func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context, tzOffset int32) ([]database.GetDeploymentDAUsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - apps := make([]database.WorkspaceApp, 0) - for _, app := range q.workspaceApps { - for _, id := range ids { - if app.AgentID == id { - apps = append(apps, app) - break - } + seens := make(map[time.Time]map[uuid.UUID]struct{}) + + for _, as := range q.workspaceAgentStats { + if as.ConnectionCount == 0 { + continue + } + date := as.CreatedAt.UTC().Add(time.Duration(tzOffset) * -1 * time.Hour).Truncate(time.Hour * 24) + + dateEntry := seens[date] + if dateEntry == nil { + dateEntry = make(map[uuid.UUID]struct{}) } + dateEntry[as.UserID] = struct{}{} + seens[date] = dateEntry } - return apps, nil -} - -func (q *fakeQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - return q.getWorkspaceBuildByIDNoLock(ctx, id) -} + seenKeys := maps.Keys(seens) + sort.Slice(seenKeys, func(i, j int) bool { + return seenKeys[i].Before(seenKeys[j]) + }) -func (q *fakeQuerier) getWorkspaceBuildByIDNoLock(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { - for _, history := range q.workspaceBuilds { - if history.ID == id { - return history, nil + var rs []database.GetDeploymentDAUsRow + for _, key := range seenKeys { + ids := seens[key] + for id := range ids { + rs = append(rs, database.GetDeploymentDAUsRow{ + Date: key, + UserID: id, + }) } } - return database.WorkspaceBuild{}, sql.ErrNoRows + + return rs, nil } -func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetDeploymentID(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, build := range q.workspaceBuilds { - if build.JobID == jobID { - return build, nil - } - } - return database.WorkspaceBuild{}, sql.ErrNoRows + return q.deploymentID, nil } -func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetDeploymentWorkspaceAgentStats(_ context.Context, createdAfter time.Time) (database.GetDeploymentWorkspaceAgentStatsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) -} - -func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { - var row database.WorkspaceBuild - var buildNum int32 = -1 - for _, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { - row = workspaceBuild - buildNum = workspaceBuild.BuildNumber + agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) } } - if buildNum == -1 { - return database.WorkspaceBuild{}, sql.ErrNoRows + + latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + latestAgentStats[agentStat.AgentID] = agentStat + } } - return row, nil -} -func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuild, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + stat := database.GetDeploymentWorkspaceAgentStatsRow{} + for _, agentStat := range latestAgentStats { + stat.SessionCountVSCode += agentStat.SessionCountVSCode + stat.SessionCountJetBrains += agentStat.SessionCountJetBrains + stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY + stat.SessionCountSSH += agentStat.SessionCountSSH + } - builds := make(map[uuid.UUID]database.WorkspaceBuild) - buildNumbers := make(map[uuid.UUID]int32) - for _, workspaceBuild := range q.workspaceBuilds { - id := workspaceBuild.WorkspaceID - if workspaceBuild.BuildNumber > buildNumbers[id] { - builds[id] = workspaceBuild - buildNumbers[id] = workspaceBuild.BuildNumber + latencies := make([]float64, 0) + for _, agentStat := range agentStatsCreatedAfter { + if agentStat.ConnectionMedianLatencyMS <= 0 { + continue } + stat.WorkspaceRxBytes += agentStat.RxBytes + stat.WorkspaceTxBytes += agentStat.TxBytes + latencies = append(latencies, agentStat.ConnectionMedianLatencyMS) } - var returnBuilds []database.WorkspaceBuild - for i, n := range buildNumbers { - if n > 0 { - b := builds[i] - returnBuilds = append(returnBuilds, b) + + tryPercentile := func(fs []float64, p float64) float64 { + if len(fs) == 0 { + return -1 } + sort.Float64s(fs) + return fs[int(float64(len(fs))*p/100)] } - if len(returnBuilds) == 0 { - return nil, sql.ErrNoRows - } - return returnBuilds, nil + + stat.WorkspaceConnectionLatency50 = tryPercentile(latencies, 50) + stat.WorkspaceConnectionLatency95 = tryPercentile(latencies, 95) + + return stat, nil } -func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database.GetDeploymentWorkspaceStatsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - builds := make(map[uuid.UUID]database.WorkspaceBuild) - buildNumbers := make(map[uuid.UUID]int32) - for _, workspaceBuild := range q.workspaceBuilds { - for _, id := range ids { - if id == workspaceBuild.WorkspaceID && workspaceBuild.BuildNumber > buildNumbers[id] { - builds[id] = workspaceBuild - buildNumbers[id] = workspaceBuild.BuildNumber + stat := database.GetDeploymentWorkspaceStatsRow{} + for _, workspace := range q.workspaces { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return stat, err + } + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err != nil { + return stat, err + } + if !job.StartedAt.Valid { + stat.PendingWorkspaces++ + continue + } + if job.StartedAt.Valid && + !job.CanceledAt.Valid && + time.Since(job.UpdatedAt) <= 30*time.Second && + !job.CompletedAt.Valid { + stat.BuildingWorkspaces++ + continue + } + if job.CompletedAt.Valid && + !job.CanceledAt.Valid && + !job.Error.Valid { + if build.Transition == database.WorkspaceTransitionStart { + stat.RunningWorkspaces++ + } + if build.Transition == database.WorkspaceTransitionStop { + stat.StoppedWorkspaces++ } + continue } - } - var returnBuilds []database.WorkspaceBuild - for i, n := range buildNumbers { - if n > 0 { - b := builds[i] - returnBuilds = append(returnBuilds, b) + if job.CanceledAt.Valid || job.Error.Valid { + stat.FailedWorkspaces++ + continue } } - if len(returnBuilds) == 0 { - return nil, sql.ErrNoRows - } - return returnBuilds, nil + return stat, nil } -func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, - params database.GetWorkspaceBuildsByWorkspaceIDParams, -) ([]database.WorkspaceBuild, error) { - if err := validateDatabaseType(params); err != nil { - return nil, err +func (q *fakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) { + if err := validateDatabaseType(arg); err != nil { + return database.File{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - history := make([]database.WorkspaceBuild, 0) - for _, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.CreatedAt.Before(params.Since) { - continue - } - if workspaceBuild.WorkspaceID == params.WorkspaceID { - history = append(history, workspaceBuild) + for _, file := range q.files { + if file.Hash == arg.Hash && file.CreatedBy == arg.CreatedBy { + return file, nil } } + return database.File{}, sql.ErrNoRows +} - // Order by build_number - slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { - // use greater than since we want descending order - return a.BuildNumber > b.BuildNumber - }) - - if params.AfterID != uuid.Nil { - found := false - for i, v := range history { - if v.ID == params.AfterID { - // We want to return all builds after index i. - history = history[i+1:] - found = true - break - } - } +func (q *fakeQuerier) GetFileByID(_ context.Context, id uuid.UUID) (database.File, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - // If no builds after the time, then we return an empty list. - if !found { - return nil, sql.ErrNoRows + for _, file := range q.files { + if file.ID == id { + return file, nil } } + return database.File{}, sql.ErrNoRows +} - if params.OffsetOpt > 0 { - if int(params.OffsetOpt) > len(history)-1 { - return nil, sql.ErrNoRows +func (q *fakeQuerier) GetFileTemplates(_ context.Context, id uuid.UUID) ([]database.GetFileTemplatesRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + rows := make([]database.GetFileTemplatesRow, 0) + var file database.File + for _, f := range q.files { + if f.ID == id { + file = f + break } - history = history[params.OffsetOpt:] + } + if file.Hash == "" { + return rows, nil } - if params.LimitOpt > 0 { - if int(params.LimitOpt) > len(history) { - params.LimitOpt = int32(len(history)) + for _, job := range q.provisionerJobs { + if job.FileID == id { + for _, version := range q.templateVersions { + if version.JobID == job.ID { + for _, template := range q.templates { + if template.ID == version.TemplateID.UUID { + rows = append(rows, database.GetFileTemplatesRow{ + FileID: file.ID, + FileCreatedBy: file.CreatedBy, + TemplateID: template.ID, + TemplateOrganizationID: template.OrganizationID, + TemplateCreatedBy: template.CreatedBy, + UserACL: template.UserACL, + GroupACL: template.GroupACL, + }) + } + } + } + } } - history = history[:params.LimitOpt] } - if len(history) == 0 { - return nil, sql.ErrNoRows + return rows, nil +} + +func (q *fakeQuerier) GetFilteredUserCount(ctx context.Context, arg database.GetFilteredUserCountParams) (int64, error) { + if err := validateDatabaseType(arg); err != nil { + return 0, err } - return history, nil + count, err := q.GetAuthorizedUserCount(ctx, arg, nil) + return count, err } -func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetGitAuthLink(_ context.Context, arg database.GetGitAuthLinkParams) (database.GitAuthLink, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.GitAuthLink{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - - for _, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.WorkspaceID != arg.WorkspaceID { + for _, gitAuthLink := range q.gitAuthLinks { + if arg.UserID != gitAuthLink.UserID { continue } - if workspaceBuild.BuildNumber != arg.BuildNumber { + if arg.ProviderID != gitAuthLink.ProviderID { continue } - return workspaceBuild, nil + return gitAuthLink, nil } - return database.WorkspaceBuild{}, sql.ErrNoRows + return database.GitAuthLink{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { +func (q *fakeQuerier) GetGitSSHKey(_ context.Context, userID uuid.UUID) (database.GitSSHKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() - params := make([]database.WorkspaceBuildParameter, 0) - for _, param := range q.workspaceBuildParameters { - if param.WorkspaceBuildID != workspaceBuildID { - continue + for _, key := range q.gitSSHKey { + if key.UserID == userID { + return key, nil } - params = append(params, param) } - return params, nil + return database.GitSSHKey{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) { q.mutex.RLock() defer q.mutex.RUnlock() - workspaceBuilds := make([]database.WorkspaceBuild, 0) - for _, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.CreatedAt.After(after) { - workspaceBuilds = append(workspaceBuilds, workspaceBuild) - } - } - return workspaceBuilds, nil + return q.getGroupByIDNoLock(ctx, id) } -func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - if len(q.organizations) == 0 { - return nil, sql.ErrNoRows +func (q *fakeQuerier) GetGroupByOrgAndName(_ context.Context, arg database.GetGroupByOrgAndNameParams) (database.Group, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Group{}, err } - return q.organizations, nil -} -func (q *fakeQuerier) GetOrganizationByID(_ context.Context, id uuid.UUID) (database.Organization, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, organization := range q.organizations { - if organization.ID == id { - return organization, nil + for _, group := range q.groups { + if group.OrganizationID == arg.OrganizationID && + group.Name == arg.Name { + return group, nil } } - return database.Organization{}, sql.ErrNoRows + + return database.Group{}, sql.ErrNoRows } -func (q *fakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) { +func (q *fakeQuerier) GetGroupMembers(_ context.Context, groupID uuid.UUID) ([]database.User, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, organization := range q.organizations { - if organization.Name == name { - return organization, nil + var members []database.GroupMember + for _, member := range q.groupMembers { + if member.GroupID == groupID { + members = append(members, member) } } - return database.Organization{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetOrganizationsByUserID(_ context.Context, userID uuid.UUID) ([]database.Organization, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + users := make([]database.User, 0, len(members)) - organizations := make([]database.Organization, 0) - for _, organizationMember := range q.organizationMembers { - if organizationMember.UserID != userID { - continue - } - for _, organization := range q.organizations { - if organization.ID != organizationMember.OrganizationID { - continue + for _, member := range members { + for _, user := range q.users { + if user.ID == member.UserID && user.Status == database.UserStatusActive && !user.Deleted { + users = append(users, user) + break } - organizations = append(organizations, organization) } } - if len(organizations) == 0 { - return nil, sql.ErrNoRows - } - return organizations, nil + + return users, nil } -func (q *fakeQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (database.Template, error) { +func (q *fakeQuerier) GetGroupsByOrganizationID(_ context.Context, organizationID uuid.UUID) ([]database.Group, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getTemplateByIDNoLock(ctx, id) -} - -func (q *fakeQuerier) getTemplateByIDNoLock(_ context.Context, id uuid.UUID) (database.Template, error) { - for _, template := range q.templates { - if template.ID == id { - return template.DeepCopy(), nil + var groups []database.Group + for _, group := range q.groups { + // Omit the allUsers group. + if group.OrganizationID == organizationID && group.ID != organizationID { + groups = append(groups, group) } } - return database.Template{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg database.GetTemplateByOrganizationAndNameParams) (database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err - } + return groups, nil +} +func (q *fakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, template := range q.templates { - if template.OrganizationID != arg.OrganizationID { - continue - } - if !strings.EqualFold(template.Name, arg.Name) { - continue - } - if template.Deleted != arg.Deleted { - continue - } - return template.DeepCopy(), nil + if q.lastUpdateCheck == nil { + return "", sql.ErrNoRows } - return database.Template{}, sql.ErrNoRows + return string(q.lastUpdateCheck), nil } -func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for idx, tpl := range q.templates { - if tpl.ID != arg.ID { - continue - } - tpl.UpdatedAt = database.Now() - tpl.Name = arg.Name - tpl.DisplayName = arg.DisplayName - tpl.Description = arg.Description - tpl.Icon = arg.Icon - q.templates[idx] = tpl - return tpl.DeepCopy(), nil - } +func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - return database.Template{}, sql.ErrNoRows + return q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) } -func (q *fakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetLatestWorkspaceBuilds(_ context.Context) ([]database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - for idx, tpl := range q.templates { - if tpl.ID != arg.ID { - continue + builds := make(map[uuid.UUID]database.WorkspaceBuild) + buildNumbers := make(map[uuid.UUID]int32) + for _, workspaceBuild := range q.workspaceBuilds { + id := workspaceBuild.WorkspaceID + if workspaceBuild.BuildNumber > buildNumbers[id] { + builds[id] = workspaceBuild + buildNumbers[id] = workspaceBuild.BuildNumber } - tpl.AllowUserAutostart = arg.AllowUserAutostart - tpl.AllowUserAutostop = arg.AllowUserAutostop - tpl.UpdatedAt = database.Now() - tpl.DefaultTTL = arg.DefaultTTL - tpl.MaxTTL = arg.MaxTTL - tpl.FailureTTL = arg.FailureTTL - tpl.InactivityTTL = arg.InactivityTTL - q.templates[idx] = tpl - return tpl.DeepCopy(), nil } - - return database.Template{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetTemplatesWithFilter(ctx context.Context, arg database.GetTemplatesWithFilterParams) ([]database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err + var returnBuilds []database.WorkspaceBuild + for i, n := range buildNumbers { + if n > 0 { + b := builds[i] + returnBuilds = append(returnBuilds, b) + } } - - return q.GetAuthorizedTemplates(ctx, arg, nil) -} - -func (q *fakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err + if len(returnBuilds) == 0 { + return nil, sql.ErrNoRows } + return returnBuilds, nil +} +func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() - // Call this to match the same function calls as the SQL implementation. - if prepared != nil { - _, err := prepared.CompileToSQL(ctx, rbac.ConfigWithACL()) - if err != nil { - return nil, err + builds := make(map[uuid.UUID]database.WorkspaceBuild) + buildNumbers := make(map[uuid.UUID]int32) + for _, workspaceBuild := range q.workspaceBuilds { + for _, id := range ids { + if id == workspaceBuild.WorkspaceID && workspaceBuild.BuildNumber > buildNumbers[id] { + builds[id] = workspaceBuild + buildNumbers[id] = workspaceBuild.BuildNumber + } } } - - var templates []database.Template - for _, template := range q.templates { - if prepared != nil && prepared.Authorize(ctx, template.RBACObject()) != nil { - continue - } - - if template.Deleted != arg.Deleted { - continue - } - if arg.OrganizationID != uuid.Nil && template.OrganizationID != arg.OrganizationID { - continue - } - - if arg.ExactName != "" && !strings.EqualFold(template.Name, arg.ExactName) { - continue - } - - if len(arg.IDs) > 0 { - match := false - for _, id := range arg.IDs { - if template.ID == id { - match = true - break - } - } - if !match { - continue - } + var returnBuilds []database.WorkspaceBuild + for i, n := range buildNumbers { + if n > 0 { + b := builds[i] + returnBuilds = append(returnBuilds, b) } - templates = append(templates, template.DeepCopy()) } - if len(templates) > 0 { - slices.SortFunc(templates, func(i, j database.Template) bool { - if i.Name != j.Name { - return i.Name < j.Name - } - return i.ID.String() < j.ID.String() - }) - return templates, nil + if len(returnBuilds) == 0 { + return nil, sql.ErrNoRows } - - return nil, sql.ErrNoRows + return returnBuilds, nil } -func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg database.GetTemplateVersionsByTemplateIDParams) (version []database.TemplateVersion, err error) { - if err := validateDatabaseType(arg); err != nil { - return version, err - } - +func (q *fakeQuerier) GetLicenseByID(_ context.Context, id int32) (database.License, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, templateVersion := range q.templateVersions { - if templateVersion.TemplateID.UUID != arg.TemplateID { - continue + for _, license := range q.licenses { + if license.ID == id { + return license, nil } - version = append(version, templateVersion) } + return database.License{}, sql.ErrNoRows +} - // Database orders by created_at - slices.SortFunc(version, func(a, b database.TemplateVersion) bool { - if a.CreatedAt.Equal(b.CreatedAt) { - // Technically the postgres database also orders by uuid. So match - // that behavior - return a.ID.String() < b.ID.String() - } - return a.CreatedAt.Before(b.CreatedAt) - }) +func (q *fakeQuerier) GetLicenses(_ context.Context) ([]database.License, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - if arg.AfterID != uuid.Nil { - found := false - for i, v := range version { - if v.ID == arg.AfterID { - // We want to return all users after index i. - version = version[i+1:] - found = true - break - } - } + results := append([]database.License{}, q.licenses...) + sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) + return results, nil +} - // If no users after the time, then we return an empty list. - if !found { - return nil, sql.ErrNoRows - } - } +func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - if arg.OffsetOpt > 0 { - if int(arg.OffsetOpt) > len(version)-1 { - return nil, sql.ErrNoRows - } - version = version[arg.OffsetOpt:] + if q.logoURL == "" { + return "", sql.ErrNoRows } - if arg.LimitOpt > 0 { - if int(arg.LimitOpt) > len(version) { - arg.LimitOpt = int32(len(version)) + return q.logoURL, nil +} + +func (q *fakeQuerier) GetOrganizationByID(_ context.Context, id uuid.UUID) (database.Organization, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, organization := range q.organizations { + if organization.ID == id { + return organization, nil } - version = version[:arg.LimitOpt] } + return database.Organization{}, sql.ErrNoRows +} - if len(version) == 0 { - return nil, sql.ErrNoRows - } +func (q *fakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - return version, nil + for _, organization := range q.organizations { + if organization.Name == name { + return organization, nil + } + } + return database.Organization{}, sql.ErrNoRows } -func (q *fakeQuerier) GetTemplateVersionsCreatedAfter(_ context.Context, after time.Time) ([]database.TemplateVersion, error) { +func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - versions := make([]database.TemplateVersion, 0) - for _, version := range q.templateVersions { - if version.CreatedAt.After(after) { - versions = append(versions, version) + getOrganizationIDsByMemberIDRows := make([]database.GetOrganizationIDsByMemberIDsRow, 0, len(ids)) + for _, userID := range ids { + userOrganizationIDs := make([]uuid.UUID, 0) + for _, membership := range q.organizationMembers { + if membership.UserID == userID { + userOrganizationIDs = append(userOrganizationIDs, membership.OrganizationID) + } } + getOrganizationIDsByMemberIDRows = append(getOrganizationIDsByMemberIDRows, database.GetOrganizationIDsByMemberIDsRow{ + UserID: userID, + OrganizationIDs: userOrganizationIDs, + }) } - return versions, nil + if len(getOrganizationIDsByMemberIDRows) == 0 { + return nil, sql.ErrNoRows + } + return getOrganizationIDsByMemberIDRows, nil } -func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, arg database.GetTemplateVersionByTemplateIDAndNameParams) (database.TemplateVersion, error) { +func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) { if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersion{}, err + return database.OrganizationMember{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - for _, templateVersion := range q.templateVersions { - if templateVersion.TemplateID != arg.TemplateID { + for _, organizationMember := range q.organizationMembers { + if organizationMember.OrganizationID != arg.OrganizationID { continue } - if !strings.EqualFold(templateVersion.Name, arg.Name) { + if organizationMember.UserID != arg.UserID { continue } - return templateVersion, nil + return organizationMember, nil } - return database.TemplateVersion{}, sql.ErrNoRows + return database.OrganizationMember{}, sql.ErrNoRows } -func (q *fakeQuerier) GetTemplateVersionParameters(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionParameter, error) { +func (q *fakeQuerier) GetOrganizationMembershipsByUserID(_ context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) { q.mutex.RLock() defer q.mutex.RUnlock() - parameters := make([]database.TemplateVersionParameter, 0) - for _, param := range q.templateVersionParameters { - if param.TemplateVersionID != templateVersionID { + var memberships []database.OrganizationMember + for _, organizationMember := range q.organizationMembers { + mem := organizationMember + if mem.UserID != userID { continue } - parameters = append(parameters, param) + memberships = append(memberships, mem) } - return parameters, nil + return memberships, nil } -func (q *fakeQuerier) GetTemplateVersionVariables(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionVariable, error) { +func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) { q.mutex.RLock() defer q.mutex.RUnlock() - variables := make([]database.TemplateVersionVariable, 0) - for _, variable := range q.templateVersionVariables { - if variable.TemplateVersionID != templateVersionID { - continue - } - variables = append(variables, variable) + if len(q.organizations) == 0 { + return nil, sql.ErrNoRows } - return variables, nil + return q.organizations, nil } -func (q *fakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { +func (q *fakeQuerier) GetOrganizationsByUserID(_ context.Context, userID uuid.UUID) ([]database.Organization, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getTemplateVersionByIDNoLock(ctx, templateVersionID) -} - -func (q *fakeQuerier) getTemplateVersionByIDNoLock(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { - for _, templateVersion := range q.templateVersions { - if templateVersion.ID != templateVersionID { + organizations := make([]database.Organization, 0) + for _, organizationMember := range q.organizationMembers { + if organizationMember.UserID != userID { continue } - return templateVersion, nil - } - return database.TemplateVersion{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetTemplateVersionsByIDs(_ context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - versions := make([]database.TemplateVersion, 0) - for _, version := range q.templateVersions { - for _, id := range ids { - if id == version.ID { - versions = append(versions, version) - break + for _, organization := range q.organizations { + if organization.ID != organizationMember.OrganizationID { + continue } + organizations = append(organizations, organization) } } - if len(versions) == 0 { + if len(organizations) == 0 { return nil, sql.ErrNoRows } - - return versions, nil + return organizations, nil } -func (q *fakeQuerier) GetTemplateVersionByJobID(_ context.Context, jobID uuid.UUID) (database.TemplateVersion, error) { +func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { q.mutex.RLock() defer q.mutex.RUnlock() - for _, templateVersion := range q.templateVersions { - if templateVersion.JobID != jobID { + parameters := make([]database.ParameterSchema, 0) + for _, parameterSchema := range q.parameterSchemas { + if parameterSchema.JobID != jobID { continue } - return templateVersion, nil + parameters = append(parameters, parameterSchema) } - return database.TemplateVersion{}, sql.ErrNoRows + if len(parameters) == 0 { + return nil, sql.ErrNoRows + } + sort.Slice(parameters, func(i, j int) bool { + return parameters[i].Index < parameters[j].Index + }) + return parameters, nil } func (q *fakeQuerier) GetPreviousTemplateVersion(_ context.Context, arg database.GetPreviousTemplateVersionParams) (database.TemplateVersion, error) { @@ -2211,576 +2002,698 @@ func (q *fakeQuerier) GetPreviousTemplateVersion(_ context.Context, arg database previousTemplateVersions = append(previousTemplateVersions, templateVersion) } } - - if len(previousTemplateVersions) == 0 { - return database.TemplateVersion{}, sql.ErrNoRows + + if len(previousTemplateVersions) == 0 { + return database.TemplateVersion{}, sql.ErrNoRows + } + + sort.Slice(previousTemplateVersions, func(i, j int) bool { + return previousTemplateVersions[i].CreatedAt.After(previousTemplateVersions[j].CreatedAt) + }) + + return previousTemplateVersions[0], nil +} + +func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if len(q.provisionerDaemons) == 0 { + return nil, sql.ErrNoRows + } + return q.provisionerDaemons, nil +} + +func (q *fakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getProvisionerJobByIDNoLock(ctx, id) +} + +func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + jobs := make([]database.ProvisionerJob, 0) + for _, job := range q.provisionerJobs { + for _, id := range ids { + if id == job.ID { + jobs = append(jobs, job) + break + } + } + } + if len(jobs) == 0 { + return nil, sql.ErrNoRows } - sort.Slice(previousTemplateVersions, func(i, j int) bool { - return previousTemplateVersions[i].CreatedAt.After(previousTemplateVersions[j].CreatedAt) - }) - - return previousTemplateVersions[0], nil + return jobs, nil } -func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { +func (q *fakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after time.Time) ([]database.ProvisionerJob, error) { q.mutex.RLock() defer q.mutex.RUnlock() - parameters := make([]database.ParameterSchema, 0) - for _, parameterSchema := range q.parameterSchemas { - if parameterSchema.JobID != jobID { - continue + jobs := make([]database.ProvisionerJob, 0) + for _, job := range q.provisionerJobs { + if job.CreatedAt.After(after) { + jobs = append(jobs, job) } - parameters = append(parameters, parameterSchema) - } - if len(parameters) == 0 { - return nil, sql.ErrNoRows } - sort.Slice(parameters, func(i, j int) bool { - return parameters[i].Index < parameters[j].Index - }) - return parameters, nil + return jobs, nil } -func (q *fakeQuerier) GetTemplates(_ context.Context) ([]database.Template, error) { +func (q *fakeQuerier) GetProvisionerLogsAfterID(_ context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err + } + q.mutex.RLock() defer q.mutex.RUnlock() - templates := slices.Clone(q.templates) - for i := range templates { - templates[i] = templates[i].DeepCopy() - } - slices.SortFunc(templates, func(i, j database.Template) bool { - if i.Name != j.Name { - return i.Name < j.Name + logs := make([]database.ProvisionerJobLog, 0) + for _, jobLog := range q.provisionerJobLogs { + if jobLog.JobID != arg.JobID { + continue } - return i.ID.String() < j.ID.String() - }) - - return templates, nil + if arg.CreatedAfter != 0 && jobLog.ID < arg.CreatedAfter { + continue + } + logs = append(logs, jobLog) + } + return logs, nil } -func (q *fakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]database.TemplateUser, error) { +func (q *fakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UUID) (int64, error) { q.mutex.RLock() defer q.mutex.RUnlock() - var template database.Template - for _, t := range q.templates { - if t.ID == id { - template = t - break + var sum int64 + for _, member := range q.groupMembers { + if member.UserID != userID { + continue + } + for _, group := range q.groups { + if group.ID == member.GroupID { + sum += int64(group.QuotaAllowance) + } } } + return sum, nil +} - if template.ID == uuid.Nil { - return nil, sql.ErrNoRows - } +func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUID) (int64, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - users := make([]database.TemplateUser, 0, len(template.UserACL)) - for k, v := range template.UserACL { - user, err := q.getUserByIDNoLock(uuid.MustParse(k)) - if err != nil && xerrors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get user by ID: %w", err) - } - // We don't delete users from the map if they - // get deleted so just skip. - if xerrors.Is(err, sql.ErrNoRows) { + var sum int64 + for _, workspace := range q.workspaces { + if workspace.OwnerID != userID { continue } - - if user.Deleted || user.Status == database.UserStatusSuspended { + if workspace.Deleted { continue } - users = append(users, database.TemplateUser{ - User: user, - Actions: v, - }) + var lastBuild database.WorkspaceBuild + for _, build := range q.workspaceBuilds { + if build.WorkspaceID != workspace.ID { + continue + } + if build.CreatedAt.After(lastBuild.CreatedAt) { + lastBuild = build + } + } + sum += int64(lastBuild.DailyCost) } + return sum, nil +} - return users, nil +func (q *fakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.Replica, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + replicas := make([]database.Replica, 0) + for _, replica := range q.replicas { + if replica.UpdatedAt.After(updatedAt) && !replica.StoppedAt.Valid { + replicas = append(replicas, replica) + } + } + return replicas, nil } -func (q *fakeQuerier) GetTemplateGroupRoles(_ context.Context, id uuid.UUID) ([]database.TemplateGroup, error) { +func (q *fakeQuerier) GetServiceBanner(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() - var template database.Template - for _, t := range q.templates { - if t.ID == id { - template = t - break - } + if q.serviceBanner == nil { + return "", sql.ErrNoRows } - if template.ID == uuid.Nil { - return nil, sql.ErrNoRows + return string(q.serviceBanner), nil +} + +func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) { + if err := validateDatabaseType(arg); err != nil { + return database.GetTemplateAverageBuildTimeRow{}, err } - groups := make([]database.TemplateGroup, 0, len(template.GroupACL)) - for k, v := range template.GroupACL { - group, err := q.getGroupByIDNoLock(context.Background(), uuid.MustParse(k)) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get group by ID: %w", err) + var emptyRow database.GetTemplateAverageBuildTimeRow + var ( + startTimes []float64 + stopTimes []float64 + deleteTimes []float64 + ) + q.mutex.RLock() + defer q.mutex.RUnlock() + for _, wb := range q.workspaceBuilds { + version, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID) + if err != nil { + return emptyRow, err } - // We don't delete groups from the map if they - // get deleted so just skip. - if xerrors.Is(err, sql.ErrNoRows) { + if version.TemplateID != arg.TemplateID { continue } - groups = append(groups, database.TemplateGroup{ - Group: group, - Actions: v, - }) + job, err := q.getProvisionerJobByIDNoLock(ctx, wb.JobID) + if err != nil { + return emptyRow, err + } + if job.CompletedAt.Valid { + took := job.CompletedAt.Time.Sub(job.StartedAt.Time).Seconds() + switch wb.Transition { + case database.WorkspaceTransitionStart: + startTimes = append(startTimes, took) + case database.WorkspaceTransitionStop: + stopTimes = append(stopTimes, took) + case database.WorkspaceTransitionDelete: + deleteTimes = append(deleteTimes, took) + } + } } - return groups, nil + tryPercentile := func(fs []float64, p float64) float64 { + if len(fs) == 0 { + return -1 + } + sort.Float64s(fs) + return fs[int(float64(len(fs))*p/100)] + } + + var row database.GetTemplateAverageBuildTimeRow + row.Delete50, row.Delete95 = tryPercentile(deleteTimes, 50), tryPercentile(deleteTimes, 95) + row.Stop50, row.Stop95 = tryPercentile(stopTimes, 50), tryPercentile(stopTimes, 95) + row.Start50, row.Start95 = tryPercentile(startTimes, 50), tryPercentile(startTimes, 95) + return row, nil } -func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) { +func (q *fakeQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (database.Template, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getTemplateByIDNoLock(ctx, id) +} + +func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg database.GetTemplateByOrganizationAndNameParams) (database.Template, error) { if err := validateDatabaseType(arg); err != nil { - return database.OrganizationMember{}, err + return database.Template{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - for _, organizationMember := range q.organizationMembers { - if organizationMember.OrganizationID != arg.OrganizationID { + for _, template := range q.templates { + if template.OrganizationID != arg.OrganizationID { continue } - if organizationMember.UserID != arg.UserID { + if !strings.EqualFold(template.Name, arg.Name) { continue } - return organizationMember, nil - } - return database.OrganizationMember{}, sql.ErrNoRows -} - -func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - getOrganizationIDsByMemberIDRows := make([]database.GetOrganizationIDsByMemberIDsRow, 0, len(ids)) - for _, userID := range ids { - userOrganizationIDs := make([]uuid.UUID, 0) - for _, membership := range q.organizationMembers { - if membership.UserID == userID { - userOrganizationIDs = append(userOrganizationIDs, membership.OrganizationID) - } + if template.Deleted != arg.Deleted { + continue } - getOrganizationIDsByMemberIDRows = append(getOrganizationIDsByMemberIDRows, database.GetOrganizationIDsByMemberIDsRow{ - UserID: userID, - OrganizationIDs: userOrganizationIDs, - }) - } - if len(getOrganizationIDsByMemberIDRows) == 0 { - return nil, sql.ErrNoRows + return template.DeepCopy(), nil } - return getOrganizationIDsByMemberIDRows, nil + return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) GetOrganizationMembershipsByUserID(_ context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) { +func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplateDAUsParams) ([]database.GetTemplateDAUsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() - var memberships []database.OrganizationMember - for _, organizationMember := range q.organizationMembers { - mem := organizationMember - if mem.UserID != userID { + seens := make(map[time.Time]map[uuid.UUID]struct{}) + + for _, as := range q.workspaceAgentStats { + if as.TemplateID != arg.TemplateID { + continue + } + if as.ConnectionCount == 0 { continue } - memberships = append(memberships, mem) - } - return memberships, nil -} -func (q *fakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) { - if err := validateDatabaseType(arg); err != nil { - return database.OrganizationMember{}, err - } + date := as.CreatedAt.UTC().Add(time.Duration(arg.TzOffset) * time.Hour * -1).Truncate(time.Hour * 24) - q.mutex.Lock() - defer q.mutex.Unlock() + dateEntry := seens[date] + if dateEntry == nil { + dateEntry = make(map[uuid.UUID]struct{}) + } + dateEntry[as.UserID] = struct{}{} + seens[date] = dateEntry + } - for i, mem := range q.organizationMembers { - if mem.UserID == arg.UserID && mem.OrganizationID == arg.OrgID { - uniqueRoles := make([]string, 0, len(arg.GrantedRoles)) - exist := make(map[string]struct{}) - for _, r := range arg.GrantedRoles { - if _, ok := exist[r]; ok { - continue - } - exist[r] = struct{}{} - uniqueRoles = append(uniqueRoles, r) - } - sort.Strings(uniqueRoles) + seenKeys := maps.Keys(seens) + sort.Slice(seenKeys, func(i, j int) bool { + return seenKeys[i].Before(seenKeys[j]) + }) - mem.Roles = uniqueRoles - q.organizationMembers[i] = mem - return mem, nil + var rs []database.GetTemplateDAUsRow + for _, key := range seenKeys { + ids := seens[key] + for id := range ids { + rs = append(rs, database.GetTemplateDAUsRow{ + Date: key, + UserID: id, + }) } } - return database.OrganizationMember{}, sql.ErrNoRows + return rs, nil } -func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) { +func (q *fakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { q.mutex.RLock() defer q.mutex.RUnlock() - if len(q.provisionerDaemons) == 0 { - return nil, sql.ErrNoRows - } - return q.provisionerDaemons, nil + return q.getTemplateVersionByIDNoLock(ctx, templateVersionID) } -func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetTemplateVersionByJobID(_ context.Context, jobID uuid.UUID) (database.TemplateVersion, error) { q.mutex.RLock() defer q.mutex.RUnlock() - // The schema sorts this by created at, so we iterate the array backwards. - for i := len(q.workspaceAgents) - 1; i >= 0; i-- { - agent := q.workspaceAgents[i] - if agent.AuthToken == authToken { - return agent, nil + for _, templateVersion := range q.templateVersions { + if templateVersion.JobID != jobID { + continue } + return templateVersion, nil } - return database.WorkspaceAgent{}, sql.ErrNoRows + return database.TemplateVersion{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, arg database.GetTemplateVersionByTemplateIDAndNameParams) (database.TemplateVersion, error) { + if err := validateDatabaseType(arg); err != nil { + return database.TemplateVersion{}, err + } + q.mutex.RLock() defer q.mutex.RUnlock() - return q.getWorkspaceAgentByIDNoLock(ctx, id) -} - -func (q *fakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { - // The schema sorts this by created at, so we iterate the array backwards. - for i := len(q.workspaceAgents) - 1; i >= 0; i-- { - agent := q.workspaceAgents[i] - if agent.ID == id { - return agent, nil + for _, templateVersion := range q.templateVersions { + if templateVersion.TemplateID != arg.TemplateID { + continue + } + if !strings.EqualFold(templateVersion.Name, arg.Name) { + continue } + return templateVersion, nil } - return database.WorkspaceAgent{}, sql.ErrNoRows + return database.TemplateVersion{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetTemplateVersionParameters(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionParameter, error) { q.mutex.RLock() defer q.mutex.RUnlock() - // The schema sorts this by created at, so we iterate the array backwards. - for i := len(q.workspaceAgents) - 1; i >= 0; i-- { - agent := q.workspaceAgents[i] - if agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID { - return agent, nil + parameters := make([]database.TemplateVersionParameter, 0) + for _, param := range q.templateVersionParameters { + if param.TemplateVersionID != templateVersionID { + continue } + parameters = append(parameters, param) } - return database.WorkspaceAgent{}, sql.ErrNoRows + return parameters, nil } -func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetTemplateVersionVariables(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionVariable, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) -} - -func (q *fakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { - workspaceAgents := make([]database.WorkspaceAgent, 0) - for _, agent := range q.workspaceAgents { - for _, resourceID := range resourceIDs { - if agent.ResourceID != resourceID { - continue - } - workspaceAgents = append(workspaceAgents, agent) + variables := make([]database.TemplateVersionVariable, 0) + for _, variable := range q.templateVersionVariables { + if variable.TemplateVersionID != templateVersionID { + continue } + variables = append(variables, variable) } - return workspaceAgents, nil + return variables, nil } -func (q *fakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetTemplateVersionsByIDs(_ context.Context, ids []uuid.UUID) ([]database.TemplateVersion, error) { q.mutex.RLock() defer q.mutex.RUnlock() - workspaceAgents := make([]database.WorkspaceAgent, 0) - for _, agent := range q.workspaceAgents { - if agent.CreatedAt.After(after) { - workspaceAgents = append(workspaceAgents, agent) + versions := make([]database.TemplateVersion, 0) + for _, version := range q.templateVersions { + for _, id := range ids { + if id == version.ID { + versions = append(versions, version) + break + } } } - return workspaceAgents, nil + if len(versions) == 0 { + return nil, sql.ErrNoRows + } + + return versions, nil } -func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { +func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg database.GetTemplateVersionsByTemplateIDParams) (version []database.TemplateVersion, err error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceApp{}, err + return version, err } q.mutex.RLock() defer q.mutex.RUnlock() - for _, app := range q.workspaceApps { - if app.AgentID != arg.AgentID { - continue - } - if app.Slug != arg.Slug { + for _, templateVersion := range q.templateVersions { + if templateVersion.TemplateID.UUID != arg.TemplateID { continue } - return app, nil + version = append(version, templateVersion) } - return database.WorkspaceApp{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + // Database orders by created_at + slices.SortFunc(version, func(a, b database.TemplateVersion) bool { + if a.CreatedAt.Equal(b.CreatedAt) { + // Technically the postgres database also orders by uuid. So match + // that behavior + return a.ID.String() < b.ID.String() + } + return a.CreatedAt.Before(b.CreatedAt) + }) - return q.getProvisionerJobByIDNoLock(ctx, id) -} + if arg.AfterID != uuid.Nil { + found := false + for i, v := range version { + if v.ID == arg.AfterID { + // We want to return all users after index i. + version = version[i+1:] + found = true + break + } + } -func (q *fakeQuerier) getProvisionerJobByIDNoLock(_ context.Context, id uuid.UUID) (database.ProvisionerJob, error) { - for _, provisionerJob := range q.provisionerJobs { - if provisionerJob.ID != id { - continue + // If no users after the time, then we return an empty list. + if !found { + return nil, sql.ErrNoRows } - return provisionerJob, nil } - return database.ProvisionerJob{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID) (database.WorkspaceResource, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + if arg.OffsetOpt > 0 { + if int(arg.OffsetOpt) > len(version)-1 { + return nil, sql.ErrNoRows + } + version = version[arg.OffsetOpt:] + } - for _, resource := range q.workspaceResources { - if resource.ID == id { - return resource, nil + if arg.LimitOpt > 0 { + if int(arg.LimitOpt) > len(version) { + arg.LimitOpt = int32(len(version)) } + version = version[:arg.LimitOpt] } - return database.WorkspaceResource{}, sql.ErrNoRows + + if len(version) == 0 { + return nil, sql.ErrNoRows + } + + return version, nil } -func (q *fakeQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { +func (q *fakeQuerier) GetTemplateVersionsCreatedAfter(_ context.Context, after time.Time) ([]database.TemplateVersion, error) { q.mutex.RLock() defer q.mutex.RUnlock() - return q.getWorkspaceResourcesByJobIDNoLock(ctx, jobID) -} - -func (q *fakeQuerier) getWorkspaceResourcesByJobIDNoLock(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { - resources := make([]database.WorkspaceResource, 0) - for _, resource := range q.workspaceResources { - if resource.JobID != jobID { - continue + versions := make([]database.TemplateVersion, 0) + for _, version := range q.templateVersions { + if version.CreatedAt.After(after) { + versions = append(versions, version) } - resources = append(resources, resource) } - return resources, nil + return versions, nil } -func (q *fakeQuerier) GetWorkspaceResourcesByJobIDs(_ context.Context, jobIDs []uuid.UUID) ([]database.WorkspaceResource, error) { +func (q *fakeQuerier) GetTemplates(_ context.Context) ([]database.Template, error) { q.mutex.RLock() defer q.mutex.RUnlock() - resources := make([]database.WorkspaceResource, 0) - for _, resource := range q.workspaceResources { - for _, jobID := range jobIDs { - if resource.JobID != jobID { - continue - } - resources = append(resources, resource) + templates := slices.Clone(q.templates) + for i := range templates { + templates[i] = templates[i].DeepCopy() + } + slices.SortFunc(templates, func(i, j database.Template) bool { + if i.Name != j.Name { + return i.Name < j.Name } + return i.ID.String() < j.ID.String() + }) + + return templates, nil +} + +func (q *fakeQuerier) GetTemplatesWithFilter(ctx context.Context, arg database.GetTemplatesWithFilterParams) ([]database.Template, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err } - return resources, nil + + return q.GetAuthorizedTemplates(ctx, arg, nil) } -func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceResource, error) { +func (q *fakeQuerier) GetUnexpiredLicenses(_ context.Context) ([]database.License, error) { q.mutex.RLock() defer q.mutex.RUnlock() - resources := make([]database.WorkspaceResource, 0) - for _, resource := range q.workspaceResources { - if resource.CreatedAt.After(after) { - resources = append(resources, resource) + now := time.Now() + var results []database.License + for _, l := range q.licenses { + if l.Exp.After(now) { + results = append(results, l) } } - return resources, nil + sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) + return results, nil } -func (q *fakeQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, after time.Time) ([]database.WorkspaceResourceMetadatum, error) { - resources, err := q.GetWorkspaceResourcesCreatedAfter(ctx, after) - if err != nil { - return nil, err - } - resourceIDs := map[uuid.UUID]struct{}{} - for _, resource := range resources { - resourceIDs[resource.ID] = struct{}{} +func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { + if err := validateDatabaseType(arg); err != nil { + return database.User{}, err } q.mutex.RLock() defer q.mutex.RUnlock() - metadata := make([]database.WorkspaceResourceMetadatum, 0) - for _, m := range q.workspaceResourceMetadata { - _, ok := resourceIDs[m.WorkspaceResourceID] - if !ok { - continue + for _, user := range q.users { + if !user.Deleted && (strings.EqualFold(user.Email, arg.Email) || strings.EqualFold(user.Username, arg.Username)) { + return user, nil } - metadata = append(metadata, m) } - return metadata, nil + return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) { +func (q *fakeQuerier) GetUserByID(_ context.Context, id uuid.UUID) (database.User, error) { q.mutex.RLock() defer q.mutex.RUnlock() - metadata := make([]database.WorkspaceResourceMetadatum, 0) - for _, metadatum := range q.workspaceResourceMetadata { - for _, id := range ids { - if metadatum.WorkspaceResourceID == id { - metadata = append(metadata, metadatum) - } + return q.getUserByIDNoLock(id) +} + +func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + existing := int64(0) + for _, u := range q.users { + if !u.Deleted { + existing++ } } - return metadata, nil + return existing, nil } -func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { +func (q *fakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) { q.mutex.RLock() defer q.mutex.RUnlock() - jobs := make([]database.ProvisionerJob, 0) - for _, job := range q.provisionerJobs { - for _, id := range ids { - if id == job.ID { - jobs = append(jobs, job) - break - } + for _, link := range q.userLinks { + if link.LinkedID == id { + return link, nil } } - if len(jobs) == 0 { - return nil, sql.ErrNoRows - } - - return jobs, nil + return database.UserLink{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after time.Time) ([]database.ProvisionerJob, error) { +func (q *fakeQuerier) GetUserLinkByUserIDLoginType(_ context.Context, params database.GetUserLinkByUserIDLoginTypeParams) (database.UserLink, error) { + if err := validateDatabaseType(params); err != nil { + return database.UserLink{}, err + } + q.mutex.RLock() defer q.mutex.RUnlock() - jobs := make([]database.ProvisionerJob, 0) - for _, job := range q.provisionerJobs { - if job.CreatedAt.After(after) { - jobs = append(jobs, job) + for _, link := range q.userLinks { + if link.UserID == params.UserID && link.LoginType == params.LoginType { + return link, nil } } - return jobs, nil + return database.UserLink{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerLogsAfterID(_ context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { - if err := validateDatabaseType(arg); err != nil { +func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.GetUsersRow, error) { + if err := validateDatabaseType(params); err != nil { return nil, err } q.mutex.RLock() defer q.mutex.RUnlock() - logs := make([]database.ProvisionerJobLog, 0) - for _, jobLog := range q.provisionerJobLogs { - if jobLog.JobID != arg.JobID { - continue + // Avoid side-effect of sorting. + users := make([]database.User, len(q.users)) + copy(users, q.users) + + // Database orders by username + slices.SortFunc(users, func(a, b database.User) bool { + return strings.ToLower(a.Username) < strings.ToLower(b.Username) + }) + + // Filter out deleted since they should never be returned.. + tmp := make([]database.User, 0, len(users)) + for _, user := range users { + if !user.Deleted { + tmp = append(tmp, user) } - if arg.CreatedAfter != 0 && jobLog.ID < arg.CreatedAfter { - continue + } + users = tmp + + if params.AfterID != uuid.Nil { + found := false + for i, v := range users { + if v.ID == params.AfterID { + // We want to return all users after index i. + users = users[i+1:] + found = true + break + } + } + + // If no users after the time, then we return an empty list. + if !found { + return []database.GetUsersRow{}, nil } - logs = append(logs, jobLog) } - return logs, nil -} -func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { - if err := validateDatabaseType(arg); err != nil { - return database.APIKey{}, err + if params.Search != "" { + tmp := make([]database.User, 0, len(users)) + for i, user := range users { + if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) { + tmp = append(tmp, users[i]) + } else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) { + tmp = append(tmp, users[i]) + } + } + users = tmp } - q.mutex.Lock() - defer q.mutex.Unlock() + if len(params.Status) > 0 { + usersFilteredByStatus := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool { + return strings.EqualFold(string(a), string(b)) + }) { + usersFilteredByStatus = append(usersFilteredByStatus, users[i]) + } + } + users = usersFilteredByStatus + } - if arg.LifetimeSeconds == 0 { - arg.LifetimeSeconds = 86400 + if len(params.RbacRole) > 0 && !slice.Contains(params.RbacRole, rbac.RoleMember()) { + usersFilteredByRole := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) { + usersFilteredByRole = append(usersFilteredByRole, users[i]) + } + } + users = usersFilteredByRole } - for _, u := range q.users { - if u.ID == arg.UserID && u.Deleted { - return database.APIKey{}, xerrors.Errorf("refusing to create APIKey for deleted user") + beforePageCount := len(users) + + if params.OffsetOpt > 0 { + if int(params.OffsetOpt) > len(users)-1 { + return []database.GetUsersRow{}, nil } + users = users[params.OffsetOpt:] } - //nolint:gosimple - key := database.APIKey{ - ID: arg.ID, - LifetimeSeconds: arg.LifetimeSeconds, - HashedSecret: arg.HashedSecret, - IPAddress: arg.IPAddress, - UserID: arg.UserID, - ExpiresAt: arg.ExpiresAt, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - LastUsed: arg.LastUsed, - LoginType: arg.LoginType, - Scope: arg.Scope, - TokenName: arg.TokenName, + if params.LimitOpt > 0 { + if int(params.LimitOpt) > len(users) { + params.LimitOpt = int32(len(users)) + } + users = users[:params.LimitOpt] } - q.apiKeys = append(q.apiKeys, key) - return key, nil + + return convertUsers(users, int64(beforePageCount)), nil } -func (q *fakeQuerier) UpdateWorkspaceAgentMetadata(_ context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]database.User, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - updated := database.WorkspaceAgentMetadatum{ - WorkspaceAgentID: arg.WorkspaceAgentID, - Key: arg.Key, - Value: arg.Value, - Error: arg.Error, - CollectedAt: arg.CollectedAt, + users := make([]database.User, 0) + for _, user := range q.users { + for _, id := range ids { + if user.ID != id { + continue + } + users = append(users, user) + } } + return users, nil +} - for i, m := range q.workspaceAgentMetadata { - if m.WorkspaceAgentID == arg.WorkspaceAgentID && m.Key == arg.Key { - q.workspaceAgentMetadata[i] = updated - return nil +func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + // The schema sorts this by created at, so we iterate the array backwards. + for i := len(q.workspaceAgents) - 1; i >= 0; i-- { + agent := q.workspaceAgents[i] + if agent.AuthToken == authToken { + return agent, nil } } + return database.WorkspaceAgent{}, sql.ErrNoRows +} - return nil +func (q *fakeQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getWorkspaceAgentByIDNoLock(ctx, id) } -func (q *fakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - metadatum := database.WorkspaceAgentMetadatum{ - WorkspaceAgentID: arg.WorkspaceAgentID, - Script: arg.Script, - DisplayName: arg.DisplayName, - Key: arg.Key, - Timeout: arg.Timeout, - Interval: arg.Interval, + // The schema sorts this by created at, so we iterate the array backwards. + for i := len(q.workspaceAgents) - 1; i >= 0; i-- { + agent := q.workspaceAgents[i] + if agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID { + return agent, nil + } } - - q.workspaceAgentMetadata = append(q.workspaceAgentMetadata, metadatum) - return nil + return database.WorkspaceAgent{}, sql.ErrNoRows } func (q *fakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { @@ -2796,870 +2709,850 @@ func (q *fakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgen return metadata, nil } -func (q *fakeQuerier) InsertFile(_ context.Context, arg database.InsertFileParams) (database.File, error) { +func (q *fakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) { if err := validateDatabaseType(arg); err != nil { - return database.File{}, err + return nil, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - file := database.File{ - ID: arg.ID, - Hash: arg.Hash, - CreatedAt: arg.CreatedAt, - CreatedBy: arg.CreatedBy, - Mimetype: arg.Mimetype, - Data: arg.Data, + logs := []database.WorkspaceAgentStartupLog{} + for _, log := range q.workspaceAgentLogs { + if log.AgentID != arg.AgentID { + continue + } + if arg.CreatedAfter != 0 && log.ID < arg.CreatedAfter { + continue + } + logs = append(logs, log) } - q.files = append(q.files, file) - return file, nil + return logs, nil } -func (q *fakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Organization{}, err +func (q *fakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) + } } - q.mutex.Lock() - defer q.mutex.Unlock() + latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + latestAgentStats[agentStat.AgentID] = agentStat + } + } - organization := database.Organization{ - ID: arg.ID, - Name: arg.Name, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, + statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsRow{} + for _, agentStat := range latestAgentStats { + stat := statByAgent[agentStat.AgentID] + stat.SessionCountVSCode += agentStat.SessionCountVSCode + stat.SessionCountJetBrains += agentStat.SessionCountJetBrains + stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY + stat.SessionCountSSH += agentStat.SessionCountSSH + statByAgent[stat.AgentID] = stat } - q.organizations = append(q.organizations, organization) - return organization, nil -} -func (q *fakeQuerier) InsertOrganizationMember(_ context.Context, arg database.InsertOrganizationMemberParams) (database.OrganizationMember, error) { - if err := validateDatabaseType(arg); err != nil { - return database.OrganizationMember{}, err + latenciesByAgent := map[uuid.UUID][]float64{} + minimumDateByAgent := map[uuid.UUID]time.Time{} + for _, agentStat := range agentStatsCreatedAfter { + if agentStat.ConnectionMedianLatencyMS <= 0 { + continue + } + stat := statByAgent[agentStat.AgentID] + minimumDate := minimumDateByAgent[agentStat.AgentID] + if agentStat.CreatedAt.Before(minimumDate) || minimumDate.IsZero() { + minimumDateByAgent[agentStat.AgentID] = agentStat.CreatedAt + } + stat.WorkspaceRxBytes += agentStat.RxBytes + stat.WorkspaceTxBytes += agentStat.TxBytes + statByAgent[agentStat.AgentID] = stat + latenciesByAgent[agentStat.AgentID] = append(latenciesByAgent[agentStat.AgentID], agentStat.ConnectionMedianLatencyMS) } - q.mutex.Lock() - defer q.mutex.Unlock() + tryPercentile := func(fs []float64, p float64) float64 { + if len(fs) == 0 { + return -1 + } + sort.Float64s(fs) + return fs[int(float64(len(fs))*p/100)] + } - //nolint:gosimple - organizationMember := database.OrganizationMember{ - OrganizationID: arg.OrganizationID, - UserID: arg.UserID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - Roles: arg.Roles, + for _, stat := range statByAgent { + stat.AggregatedFrom = minimumDateByAgent[stat.AgentID] + statByAgent[stat.AgentID] = stat + + latencies, ok := latenciesByAgent[stat.AgentID] + if !ok { + continue + } + stat.WorkspaceConnectionLatency50 = tryPercentile(latencies, 50) + stat.WorkspaceConnectionLatency95 = tryPercentile(latencies, 95) + statByAgent[stat.AgentID] = stat } - q.organizationMembers = append(q.organizationMembers, organizationMember) - return organizationMember, nil + + stats := make([]database.GetWorkspaceAgentStatsRow, 0, len(statByAgent)) + for _, agent := range statByAgent { + stats = append(stats, agent) + } + return stats, nil } -func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) (database.Template, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err +func (q *fakeQuerier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsAndLabelsRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) + latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} + + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) + latestAgentStats[agentStat.AgentID] = agentStat + } } - q.mutex.Lock() - defer q.mutex.Unlock() + statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsAndLabelsRow{} - //nolint:gosimple - template := database.Template{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OrganizationID: arg.OrganizationID, - Name: arg.Name, - Provisioner: arg.Provisioner, - ActiveVersionID: arg.ActiveVersionID, - Description: arg.Description, - CreatedBy: arg.CreatedBy, - UserACL: arg.UserACL, - GroupACL: arg.GroupACL, - DisplayName: arg.DisplayName, - Icon: arg.Icon, - AllowUserCancelWorkspaceJobs: arg.AllowUserCancelWorkspaceJobs, - AllowUserAutostart: true, - AllowUserAutostop: true, + // Session and connection metrics + for _, agentStat := range latestAgentStats { + stat := statByAgent[agentStat.AgentID] + stat.SessionCountVSCode += agentStat.SessionCountVSCode + stat.SessionCountJetBrains += agentStat.SessionCountJetBrains + stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY + stat.SessionCountSSH += agentStat.SessionCountSSH + stat.ConnectionCount += agentStat.ConnectionCount + if agentStat.ConnectionMedianLatencyMS >= 0 && stat.ConnectionMedianLatencyMS < agentStat.ConnectionMedianLatencyMS { + stat.ConnectionMedianLatencyMS = agentStat.ConnectionMedianLatencyMS + } + statByAgent[agentStat.AgentID] = stat } - q.templates = append(q.templates, template) - return template.DeepCopy(), nil -} -func (q *fakeQuerier) InsertTemplateVersion(_ context.Context, arg database.InsertTemplateVersionParams) (database.TemplateVersion, error) { - if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersion{}, err + // Tx, Rx metrics + for _, agentStat := range agentStatsCreatedAfter { + stat := statByAgent[agentStat.AgentID] + stat.RxBytes += agentStat.RxBytes + stat.TxBytes += agentStat.TxBytes + statByAgent[agentStat.AgentID] = stat } - q.mutex.Lock() - defer q.mutex.Unlock() + // Labels + for _, agentStat := range agentStatsCreatedAfter { + stat := statByAgent[agentStat.AgentID] - //nolint:gosimple - version := database.TemplateVersion{ - ID: arg.ID, - TemplateID: arg.TemplateID, - OrganizationID: arg.OrganizationID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - Name: arg.Name, - Readme: arg.Readme, - JobID: arg.JobID, - CreatedBy: arg.CreatedBy, + user, err := q.getUserByIDNoLock(agentStat.UserID) + if err != nil { + return nil, err + } + + stat.Username = user.Username + + workspace, err := q.getWorkspaceByIDNoLock(ctx, agentStat.WorkspaceID) + if err != nil { + return nil, err + } + stat.WorkspaceName = workspace.Name + + agent, err := q.getWorkspaceAgentByIDNoLock(ctx, agentStat.AgentID) + if err != nil { + return nil, err + } + stat.AgentName = agent.Name + + statByAgent[agentStat.AgentID] = stat + } + + stats := make([]database.GetWorkspaceAgentStatsAndLabelsRow, 0, len(statByAgent)) + for _, agent := range statByAgent { + stats = append(stats, agent) + } + return stats, nil +} + +func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) +} + +func (q *fakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaceAgents := make([]database.WorkspaceAgent, 0) + for _, agent := range q.workspaceAgents { + if agent.CreatedAt.After(after) { + workspaceAgents = append(workspaceAgents, agent) + } } - q.templateVersions = append(q.templateVersions, version) - return version, nil + return workspaceAgents, nil } -func (q *fakeQuerier) InsertTemplateVersionParameter(_ context.Context, arg database.InsertTemplateVersionParameterParams) (database.TemplateVersionParameter, error) { - if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersionParameter{}, err +func (q *fakeQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + // Get latest build for workspace. + workspaceBuild, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) + if err != nil { + return nil, xerrors.Errorf("get latest workspace build: %w", err) } - q.mutex.Lock() - defer q.mutex.Unlock() + // Get resources for build. + resources, err := q.getWorkspaceResourcesByJobIDNoLock(ctx, workspaceBuild.JobID) + if err != nil { + return nil, xerrors.Errorf("get workspace resources: %w", err) + } + if len(resources) == 0 { + return []database.WorkspaceAgent{}, nil + } - //nolint:gosimple - param := database.TemplateVersionParameter{ - TemplateVersionID: arg.TemplateVersionID, - Name: arg.Name, - DisplayName: arg.DisplayName, - Description: arg.Description, - Type: arg.Type, - Mutable: arg.Mutable, - DefaultValue: arg.DefaultValue, - Icon: arg.Icon, - Options: arg.Options, - ValidationError: arg.ValidationError, - ValidationRegex: arg.ValidationRegex, - ValidationMin: arg.ValidationMin, - ValidationMax: arg.ValidationMax, - ValidationMonotonic: arg.ValidationMonotonic, - Required: arg.Required, - LegacyVariableName: arg.LegacyVariableName, + resourceIDs := make([]uuid.UUID, len(resources)) + for i, resource := range resources { + resourceIDs[i] = resource.ID } - q.templateVersionParameters = append(q.templateVersionParameters, param) - return param, nil + + agents, err := q.getWorkspaceAgentsByResourceIDsNoLock(ctx, resourceIDs) + if err != nil { + return nil, xerrors.Errorf("get workspace agents: %w", err) + } + + return agents, nil } -func (q *fakeQuerier) InsertTemplateVersionVariable(_ context.Context, arg database.InsertTemplateVersionVariableParams) (database.TemplateVersionVariable, error) { +func (q *fakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersionVariable{}, err + return database.WorkspaceApp{}, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - variable := database.TemplateVersionVariable{ - TemplateVersionID: arg.TemplateVersionID, - Name: arg.Name, - Description: arg.Description, - Type: arg.Type, - Value: arg.Value, - DefaultValue: arg.DefaultValue, - Required: arg.Required, - Sensitive: arg.Sensitive, + for _, app := range q.workspaceApps { + if app.AgentID != arg.AgentID { + continue + } + if app.Slug != arg.Slug { + continue + } + return app, nil } - q.templateVersionVariables = append(q.templateVersionVariables, variable) - return variable, nil + return database.WorkspaceApp{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.InsertProvisionerJobLogsParams) ([]database.ProvisionerJobLog, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - logs := make([]database.ProvisionerJobLog, 0) - id := int64(1) - if len(q.provisionerJobLogs) > 0 { - id = q.provisionerJobLogs[len(q.provisionerJobLogs)-1].ID + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + if app.AgentID == id { + apps = append(apps, app) + } } - for index, output := range arg.Output { - id++ - logs = append(logs, database.ProvisionerJobLog{ - ID: id, - JobID: arg.JobID, - CreatedAt: arg.CreatedAt[index], - Source: arg.Source[index], - Level: arg.Level[index], - Stage: arg.Stage[index], - Output: output, - }) + if len(apps) == 0 { + return nil, sql.ErrNoRows } - q.provisionerJobLogs = append(q.provisionerJobLogs, logs...) - return logs, nil + return apps, nil } -func (q *fakeQuerier) InsertProvisionerDaemon(_ context.Context, arg database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { - if err := validateDatabaseType(arg); err != nil { - return database.ProvisionerDaemon{}, err +func (q *fakeQuerier) GetWorkspaceAppsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + for _, id := range ids { + if app.AgentID == id { + apps = append(apps, app) + break + } + } } + return apps, nil +} - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceAppsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceApp, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - daemon := database.ProvisionerDaemon{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - Name: arg.Name, - Provisioners: arg.Provisioners, - Tags: arg.Tags, + apps := make([]database.WorkspaceApp, 0) + for _, app := range q.workspaceApps { + if app.CreatedAt.After(after) { + apps = append(apps, app) + } } - q.provisionerDaemons = append(q.provisionerDaemons, daemon) - return daemon, nil + return apps, nil } -func (q *fakeQuerier) InsertProvisionerJob(_ context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { - if err := validateDatabaseType(arg); err != nil { - return database.ProvisionerJob{}, err - } +func (q *fakeQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - q.mutex.Lock() - defer q.mutex.Unlock() + return q.getWorkspaceBuildByIDNoLock(ctx, id) +} - job := database.ProvisionerJob{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OrganizationID: arg.OrganizationID, - InitiatorID: arg.InitiatorID, - Provisioner: arg.Provisioner, - StorageMethod: arg.StorageMethod, - FileID: arg.FileID, - Type: arg.Type, - Input: arg.Input, - Tags: arg.Tags, +func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, build := range q.workspaceBuilds { + if build.JobID == jobID { + return build, nil + } } - q.provisionerJobs = append(q.provisionerJobs, job) - return job, nil + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceAgent{}, err + return database.WorkspaceBuild{}, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - agent := database.WorkspaceAgent{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - ResourceID: arg.ResourceID, - AuthToken: arg.AuthToken, - AuthInstanceID: arg.AuthInstanceID, - EnvironmentVariables: arg.EnvironmentVariables, - Name: arg.Name, - Architecture: arg.Architecture, - OperatingSystem: arg.OperatingSystem, - Directory: arg.Directory, - StartupScriptBehavior: arg.StartupScriptBehavior, - StartupScript: arg.StartupScript, - InstanceMetadata: arg.InstanceMetadata, - ResourceMetadata: arg.ResourceMetadata, - ConnectionTimeoutSeconds: arg.ConnectionTimeoutSeconds, - TroubleshootingURL: arg.TroubleshootingURL, - MOTDFile: arg.MOTDFile, - LifecycleState: database.WorkspaceAgentLifecycleStateCreated, - ShutdownScript: arg.ShutdownScript, + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.WorkspaceID != arg.WorkspaceID { + continue + } + if workspaceBuild.BuildNumber != arg.BuildNumber { + continue + } + return workspaceBuild, nil } - - q.workspaceAgents = append(q.workspaceAgents, agent) - return agent, nil + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) { - if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceResource{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBuildID uuid.UUID) ([]database.WorkspaceBuildParameter, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - resource := database.WorkspaceResource{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - JobID: arg.JobID, - Transition: arg.Transition, - Type: arg.Type, - Name: arg.Name, - Hide: arg.Hide, - Icon: arg.Icon, - DailyCost: arg.DailyCost, + params := make([]database.WorkspaceBuildParameter, 0) + for _, param := range q.workspaceBuildParameters { + if param.WorkspaceBuildID != workspaceBuildID { + continue + } + params = append(params, param) } - q.workspaceResources = append(q.workspaceResources, resource) - return resource, nil + return params, nil } -func (q *fakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg database.InsertWorkspaceResourceMetadataParams) ([]database.WorkspaceResourceMetadatum, error) { - if err := validateDatabaseType(arg); err != nil { +func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context, + params database.GetWorkspaceBuildsByWorkspaceIDParams, +) ([]database.WorkspaceBuild, error) { + if err := validateDatabaseType(params); err != nil { return nil, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - metadata := make([]database.WorkspaceResourceMetadatum, 0) - id := int64(1) - if len(q.workspaceResourceMetadata) > 0 { - id = q.workspaceResourceMetadata[len(q.workspaceResourceMetadata)-1].ID + history := make([]database.WorkspaceBuild, 0) + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.CreatedAt.Before(params.Since) { + continue + } + if workspaceBuild.WorkspaceID == params.WorkspaceID { + history = append(history, workspaceBuild) + } } - for index, key := range arg.Key { - id++ - value := arg.Value[index] - metadata = append(metadata, database.WorkspaceResourceMetadatum{ - ID: id, - WorkspaceResourceID: arg.WorkspaceResourceID, - Key: key, - Value: sql.NullString{ - String: value, - Valid: value != "", - }, - Sensitive: arg.Sensitive[index], - }) + + // Order by build_number + slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool { + // use greater than since we want descending order + return a.BuildNumber > b.BuildNumber + }) + + if params.AfterID != uuid.Nil { + found := false + for i, v := range history { + if v.ID == params.AfterID { + // We want to return all builds after index i. + history = history[i+1:] + found = true + break + } + } + + // If no builds after the time, then we return an empty list. + if !found { + return nil, sql.ErrNoRows + } } - q.workspaceResourceMetadata = append(q.workspaceResourceMetadata, metadata...) - return metadata, nil -} -func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParams) (database.User, error) { - if err := validateDatabaseType(arg); err != nil { - return database.User{}, err + if params.OffsetOpt > 0 { + if int(params.OffsetOpt) > len(history)-1 { + return nil, sql.ErrNoRows + } + history = history[params.OffsetOpt:] } - // There is a common bug when using dbfake that 2 inserted users have the - // same created_at time. This causes user order to not be deterministic, - // which breaks some unit tests. - // To fix this, we make sure that the created_at time is always greater - // than the last user's created_at time. - allUsers, _ := q.GetUsers(context.Background(), database.GetUsersParams{}) - if len(allUsers) > 0 { - lastUser := allUsers[len(allUsers)-1] - if arg.CreatedAt.Before(lastUser.CreatedAt) || - arg.CreatedAt.Equal(lastUser.CreatedAt) { - // 1 ms is a good enough buffer. - arg.CreatedAt = lastUser.CreatedAt.Add(time.Millisecond) + if params.LimitOpt > 0 { + if int(params.LimitOpt) > len(history) { + params.LimitOpt = int32(len(history)) } + history = history[:params.LimitOpt] } - q.mutex.Lock() - defer q.mutex.Unlock() + if len(history) == 0 { + return nil, sql.ErrNoRows + } + return history, nil +} - for _, user := range q.users { - if user.Username == arg.Username && !user.Deleted { - return database.User{}, errDuplicateKey +func (q *fakeQuerier) GetWorkspaceBuildsCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaceBuilds := make([]database.WorkspaceBuild, 0) + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.CreatedAt.After(after) { + workspaceBuilds = append(workspaceBuilds, workspaceBuild) } } + return workspaceBuilds, nil +} - user := database.User{ - ID: arg.ID, - Email: arg.Email, - HashedPassword: arg.HashedPassword, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - Username: arg.Username, - Status: database.UserStatusActive, - RBACRoles: arg.RBACRoles, - LoginType: arg.LoginType, - } - q.users = append(q.users, user) - return user, nil +func (q *fakeQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (database.Workspace, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getWorkspaceByAgentIDNoLock(ctx, agentID) } -func (q *fakeQuerier) UpdateUserRoles(_ context.Context, arg database.UpdateUserRolesParams) (database.User, error) { +func (q *fakeQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (database.Workspace, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + return q.getWorkspaceByIDNoLock(ctx, id) +} + +func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg database.GetWorkspaceByOwnerIDAndNameParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { - return database.User{}, err + return database.Workspace{}, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - for index, user := range q.users { - if user.ID != arg.ID { + var found *database.Workspace + for _, workspace := range q.workspaces { + workspace := workspace + if workspace.OwnerID != arg.OwnerID { continue } - - // Set new roles - user.RBACRoles = arg.GrantedRoles - // Remove duplicates and sort - uniqueRoles := make([]string, 0, len(user.RBACRoles)) - exist := make(map[string]struct{}) - for _, r := range user.RBACRoles { - if _, ok := exist[r]; ok { - continue - } - exist[r] = struct{}{} - uniqueRoles = append(uniqueRoles, r) + if !strings.EqualFold(workspace.Name, arg.Name) { + continue + } + if workspace.Deleted != arg.Deleted { + continue } - sort.Strings(uniqueRoles) - user.RBACRoles = uniqueRoles - q.users[index] = user - return user, nil + // Return the most recent workspace with the given name + if found == nil || workspace.CreatedAt.After(found.CreatedAt) { + found = &workspace + } } - return database.User{}, sql.ErrNoRows + if found != nil { + return *found, nil + } + return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) { - if err := validateDatabaseType(arg); err != nil { - return database.User{}, err +func (q *fakeQuerier) GetWorkspaceByWorkspaceAppID(_ context.Context, workspaceAppID uuid.UUID) (database.Workspace, error) { + if err := validateDatabaseType(workspaceAppID); err != nil { + return database.Workspace{}, err } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - for index, user := range q.users { - if user.ID != arg.ID { - continue + for _, workspaceApp := range q.workspaceApps { + workspaceApp := workspaceApp + if workspaceApp.ID == workspaceAppID { + return q.getWorkspaceByAgentIDNoLock(context.Background(), workspaceApp.AgentID) } - user.Email = arg.Email - user.Username = arg.Username - user.AvatarURL = arg.AvatarURL - q.users[index] = user - return user, nil } - return database.User{}, sql.ErrNoRows + return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUserStatusParams) (database.User, error) { - if err := validateDatabaseType(arg); err != nil { - return database.User{}, err - } +func (q *fakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - q.mutex.Lock() - defer q.mutex.Unlock() + cpy := make([]database.WorkspaceProxy, 0, len(q.workspaceProxies)) - for index, user := range q.users { - if user.ID != arg.ID { - continue + for _, p := range q.workspaceProxies { + if !p.Deleted { + cpy = append(cpy, p) } - user.Status = arg.Status - user.UpdatedAt = arg.UpdatedAt - q.users[index] = user - return user, nil } - return database.User{}, sql.ErrNoRows + return cpy, nil } -func (q *fakeQuerier) UpdateUserLastSeenAt(_ context.Context, arg database.UpdateUserLastSeenAtParams) (database.User, error) { - if err := validateDatabaseType(arg); err != nil { - return database.User{}, err +func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, params database.GetWorkspaceProxyByHostnameParams) (database.WorkspaceProxy, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + // Return zero rows if this is called with a non-sanitized hostname. The SQL + // version of this query does the same thing. + if !validProxyByHostnameRegex.MatchString(params.Hostname) { + return database.WorkspaceProxy{}, sql.ErrNoRows + } + + // This regex matches the SQL version. + accessURLRegex := regexp.MustCompile(`[^:]*://` + regexp.QuoteMeta(params.Hostname) + `([:/]?.)*`) + + for _, proxy := range q.workspaceProxies { + if proxy.Deleted { + continue + } + if params.AllowAccessUrl && accessURLRegex.MatchString(proxy.Url) { + return proxy, nil + } + + // Compile the app hostname regex. This is slow sadly. + if params.AllowWildcardHostname { + wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname) + if err != nil { + return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err) + } + if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok { + return proxy, nil + } + } } - q.mutex.Lock() - defer q.mutex.Unlock() + return database.WorkspaceProxy{}, sql.ErrNoRows +} - for index, user := range q.users { - if user.ID != arg.ID { - continue +func (q *fakeQuerier) GetWorkspaceProxyByID(_ context.Context, id uuid.UUID) (database.WorkspaceProxy, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, proxy := range q.workspaceProxies { + if proxy.ID == id { + return proxy, nil } - user.LastSeenAt = arg.LastSeenAt - user.UpdatedAt = arg.UpdatedAt - q.users[index] = user - return user, nil } - return database.User{}, sql.ErrNoRows + return database.WorkspaceProxy{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - +func (q *fakeQuerier) GetWorkspaceProxyByName(_ context.Context, name string) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() - for i, user := range q.users { - if user.ID != arg.ID { + for _, proxy := range q.workspaceProxies { + if proxy.Deleted { continue } - user.HashedPassword = arg.HashedPassword - q.users[i] = user - return nil + if proxy.Name == name { + return proxy, nil + } } - return sql.ErrNoRows + return database.WorkspaceProxy{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) { - if err := validateDatabaseType(arg); err != nil { - return database.Workspace{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID) (database.WorkspaceResource, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - //nolint:gosimple - workspace := database.Workspace{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OwnerID: arg.OwnerID, - OrganizationID: arg.OrganizationID, - TemplateID: arg.TemplateID, - Name: arg.Name, - AutostartSchedule: arg.AutostartSchedule, - Ttl: arg.Ttl, - LastUsedAt: arg.LastUsedAt, + for _, resource := range q.workspaceResources { + if resource.ID == id { + return resource, nil + } } - q.workspaces = append(q.workspaces, workspace) - return workspace, nil + return database.WorkspaceResource{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { - if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceResourceMetadatum, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - workspaceBuild := database.WorkspaceBuild{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - WorkspaceID: arg.WorkspaceID, - TemplateVersionID: arg.TemplateVersionID, - BuildNumber: arg.BuildNumber, - Transition: arg.Transition, - InitiatorID: arg.InitiatorID, - JobID: arg.JobID, - ProvisionerState: arg.ProvisionerState, - Deadline: arg.Deadline, - Reason: arg.Reason, + metadata := make([]database.WorkspaceResourceMetadatum, 0) + for _, metadatum := range q.workspaceResourceMetadata { + for _, id := range ids { + if metadatum.WorkspaceResourceID == id { + metadata = append(metadata, metadatum) + } + } } - q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild) - return workspaceBuild, nil + return metadata, nil } -func (q *fakeQuerier) InsertWorkspaceBuildParameters(_ context.Context, arg database.InsertWorkspaceBuildParametersParams) error { - if err := validateDatabaseType(arg); err != nil { - return err +func (q *fakeQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, after time.Time) ([]database.WorkspaceResourceMetadatum, error) { + resources, err := q.GetWorkspaceResourcesCreatedAfter(ctx, after) + if err != nil { + return nil, err + } + resourceIDs := map[uuid.UUID]struct{}{} + for _, resource := range resources { + resourceIDs[resource.ID] = struct{}{} } - q.mutex.Lock() - defer q.mutex.Unlock() + q.mutex.RLock() + defer q.mutex.RUnlock() - for index, name := range arg.Name { - q.workspaceBuildParameters = append(q.workspaceBuildParameters, database.WorkspaceBuildParameter{ - WorkspaceBuildID: arg.WorkspaceBuildID, - Name: name, - Value: arg.Value[index], - }) + metadata := make([]database.WorkspaceResourceMetadatum, 0) + for _, m := range q.workspaceResourceMetadata { + _, ok := resourceIDs[m.WorkspaceResourceID] + if !ok { + continue + } + metadata = append(metadata, m) } - return nil + return metadata, nil } -func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { - if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceApp{}, err - } +func (q *fakeQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - q.mutex.Lock() - defer q.mutex.Unlock() + return q.getWorkspaceResourcesByJobIDNoLock(ctx, jobID) +} - if arg.SharingLevel == "" { - arg.SharingLevel = database.AppSharingLevelOwner - } +func (q *fakeQuerier) GetWorkspaceResourcesByJobIDs(_ context.Context, jobIDs []uuid.UUID) ([]database.WorkspaceResource, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - // nolint:gosimple - workspaceApp := database.WorkspaceApp{ - ID: arg.ID, - AgentID: arg.AgentID, - CreatedAt: arg.CreatedAt, - Slug: arg.Slug, - DisplayName: arg.DisplayName, - Icon: arg.Icon, - Command: arg.Command, - Url: arg.Url, - External: arg.External, - Subdomain: arg.Subdomain, - SharingLevel: arg.SharingLevel, - HealthcheckUrl: arg.HealthcheckUrl, - HealthcheckInterval: arg.HealthcheckInterval, - HealthcheckThreshold: arg.HealthcheckThreshold, - Health: arg.Health, + resources := make([]database.WorkspaceResource, 0) + for _, resource := range q.workspaceResources { + for _, jobID := range jobIDs { + if resource.JobID != jobID { + continue + } + resources = append(resources, resource) + } } - q.workspaceApps = append(q.workspaceApps, workspaceApp) - return workspaceApp, nil + return resources, nil } -func (q *fakeQuerier) UpdateWorkspaceAppHealthByID(_ context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() +func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceResource, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - for index, app := range q.workspaceApps { - if app.ID != arg.ID { - continue + resources := make([]database.WorkspaceResource, 0) + for _, resource := range q.workspaceResources { + if resource.CreatedAt.After(after) { + resources = append(resources, resource) } - app.Health = arg.Health - q.workspaceApps[index] = app - return nil } - return sql.ErrNoRows + return resources, nil } -func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { +func (q *fakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { if err := validateDatabaseType(arg); err != nil { - return err + return nil, err } - q.mutex.Lock() - defer q.mutex.Unlock() - - for index, apiKey := range q.apiKeys { - if apiKey.ID != arg.ID { - continue - } - apiKey.LastUsed = arg.LastUsed - apiKey.ExpiresAt = arg.ExpiresAt - apiKey.IPAddress = arg.IPAddress - q.apiKeys[index] = apiKey - return nil - } - return sql.ErrNoRows + // A nil auth filter means no auth filter. + workspaceRows, err := q.GetAuthorizedWorkspaces(ctx, arg, nil) + return workspaceRows, err } -func (q *fakeQuerier) UpdateTemplateActiveVersionByID(_ context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } +func (q *fakeQuerier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() - q.mutex.Lock() - defer q.mutex.Unlock() + workspaces := []database.Workspace{} + for _, workspace := range q.workspaces { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return nil, err + } - for index, template := range q.templates { - if template.ID != arg.ID { + if build.Transition == database.WorkspaceTransitionStart && !build.Deadline.IsZero() && build.Deadline.Before(now) { + workspaces = append(workspaces, workspace) continue } - template.ActiveVersionID = arg.ActiveVersionID - template.UpdatedAt = arg.UpdatedAt - q.templates[index] = template - return nil - } - return sql.ErrNoRows -} - -func (q *fakeQuerier) UpdateTemplateDeletedByID(_ context.Context, arg database.UpdateTemplateDeletedByIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - for index, template := range q.templates { - if template.ID != arg.ID { + if build.Transition == database.WorkspaceTransitionStop && workspace.AutostartSchedule.Valid { + workspaces = append(workspaces, workspace) continue } - template.Deleted = arg.Deleted - template.UpdatedAt = arg.UpdatedAt - q.templates[index] = template - return nil } - return sql.ErrNoRows + + return workspaces, nil } -func (q *fakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { +func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { if err := validateDatabaseType(arg); err != nil { - return database.Template{}, err + return database.APIKey{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for i, template := range q.templates { - if template.ID == arg.ID { - template.GroupACL = arg.GroupACL - template.UserACL = arg.UserACL + if arg.LifetimeSeconds == 0 { + arg.LifetimeSeconds = 86400 + } - q.templates[i] = template - return template.DeepCopy(), nil + for _, u := range q.users { + if u.ID == arg.UserID && u.Deleted { + return database.APIKey{}, xerrors.Errorf("refusing to create APIKey for deleted user") } } - return database.Template{}, sql.ErrNoRows + //nolint:gosimple + key := database.APIKey{ + ID: arg.ID, + LifetimeSeconds: arg.LifetimeSeconds, + HashedSecret: arg.HashedSecret, + IPAddress: arg.IPAddress, + UserID: arg.UserID, + ExpiresAt: arg.ExpiresAt, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + LastUsed: arg.LastUsed, + LoginType: arg.LoginType, + Scope: arg.Scope, + TokenName: arg.TokenName, + } + q.apiKeys = append(q.apiKeys, key) + return key, nil } -func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { +func (q *fakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID) (database.Group, error) { + return q.InsertGroup(ctx, database.InsertGroupParams{ + ID: orgID, + Name: database.AllUsersGroup, + OrganizationID: orgID, + }) +} + +func (q *fakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAuditLogParams) (database.AuditLog, error) { if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersion{}, err + return database.AuditLog{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, templateVersion := range q.templateVersions { - if templateVersion.ID != arg.ID { - continue - } - templateVersion.TemplateID = arg.TemplateID - templateVersion.UpdatedAt = arg.UpdatedAt - templateVersion.Name = arg.Name - q.templateVersions[index] = templateVersion - return templateVersion, nil - } - return database.TemplateVersion{}, sql.ErrNoRows -} + alog := database.AuditLog(arg) -func (q *fakeQuerier) UpdateTemplateVersionDescriptionByJobID(_ context.Context, arg database.UpdateTemplateVersionDescriptionByJobIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } + q.auditLogs = append(q.auditLogs, alog) + slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) bool { + return a.Time.Before(b.Time) + }) + + return alog, nil +} +func (q *fakeQuerier) InsertDERPMeshKey(_ context.Context, id string) error { q.mutex.Lock() defer q.mutex.Unlock() - for index, templateVersion := range q.templateVersions { - if templateVersion.JobID != arg.JobID { - continue - } - templateVersion.Readme = arg.Readme - templateVersion.UpdatedAt = arg.UpdatedAt - q.templateVersions[index] = templateVersion - return nil - } - return sql.ErrNoRows + q.derpMeshKey = id + return nil } -func (q *fakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Context, arg database.UpdateTemplateVersionGitAuthProvidersByJobIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - +func (q *fakeQuerier) InsertDeploymentID(_ context.Context, id string) error { q.mutex.Lock() defer q.mutex.Unlock() - for index, templateVersion := range q.templateVersions { - if templateVersion.JobID != arg.JobID { - continue - } - templateVersion.GitAuthProviders = arg.GitAuthProviders - templateVersion.UpdatedAt = arg.UpdatedAt - q.templateVersions[index] = templateVersion - return nil - } - return sql.ErrNoRows + q.deploymentID = id + return nil } -func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error { +func (q *fakeQuerier) InsertFile(_ context.Context, arg database.InsertFileParams) (database.File, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.File{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, agent := range q.workspaceAgents { - if agent.ID != arg.ID { - continue - } - agent.FirstConnectedAt = arg.FirstConnectedAt - agent.LastConnectedAt = arg.LastConnectedAt - agent.DisconnectedAt = arg.DisconnectedAt - agent.UpdatedAt = arg.UpdatedAt - q.workspaceAgents[index] = agent - return nil + //nolint:gosimple + file := database.File{ + ID: arg.ID, + Hash: arg.Hash, + CreatedAt: arg.CreatedAt, + CreatedBy: arg.CreatedBy, + Mimetype: arg.Mimetype, + Data: arg.Data, } - return sql.ErrNoRows + q.files = append(q.files, file) + return file, nil } -func (q *fakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg database.UpdateWorkspaceAgentStartupByIDParams) error { +func (q *fakeQuerier) InsertGitAuthLink(_ context.Context, arg database.InsertGitAuthLinkParams) (database.GitAuthLink, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.GitAuthLink{}, err } q.mutex.Lock() defer q.mutex.Unlock() - - for index, agent := range q.workspaceAgents { - if agent.ID != arg.ID { - continue - } - - agent.Version = arg.Version - agent.ExpandedDirectory = arg.ExpandedDirectory - agent.Subsystem = arg.Subsystem - q.workspaceAgents[index] = agent - return nil + // nolint:gosimple + gitAuthLink := database.GitAuthLink{ + ProviderID: arg.ProviderID, + UserID: arg.UserID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + OAuthAccessToken: arg.OAuthAccessToken, + OAuthRefreshToken: arg.OAuthRefreshToken, + OAuthExpiry: arg.OAuthExpiry, } - return sql.ErrNoRows + q.gitAuthLinks = append(q.gitAuthLinks, gitAuthLink) + return gitAuthLink, nil } -func (q *fakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) { +func (q *fakeQuerier) InsertGitSSHKey(_ context.Context, arg database.InsertGitSSHKeyParams) (database.GitSSHKey, error) { if err := validateDatabaseType(arg); err != nil { - return nil, err + return database.GitSSHKey{}, err } - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - logs := []database.WorkspaceAgentStartupLog{} - for _, log := range q.workspaceAgentLogs { - if log.AgentID != arg.AgentID { - continue - } - if arg.CreatedAfter != 0 && log.ID < arg.CreatedAfter { - continue - } - logs = append(logs, log) + //nolint:gosimple + gitSSHKey := database.GitSSHKey{ + UserID: arg.UserID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + PrivateKey: arg.PrivateKey, + PublicKey: arg.PublicKey, } - return logs, nil + q.gitSSHKey = append(q.gitSSHKey, gitSSHKey) + return gitSSHKey, nil } -func (q *fakeQuerier) InsertWorkspaceAgentStartupLogs(_ context.Context, arg database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) { +func (q *fakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupParams) (database.Group, error) { if err := validateDatabaseType(arg); err != nil { - return nil, err + return database.Group{}, err } q.mutex.Lock() defer q.mutex.Unlock() - logs := []database.WorkspaceAgentStartupLog{} - id := int64(1) - if len(q.workspaceAgentLogs) > 0 { - id = q.workspaceAgentLogs[len(q.workspaceAgentLogs)-1].ID - } - outputLength := int32(0) - for index, output := range arg.Output { - id++ - logs = append(logs, database.WorkspaceAgentStartupLog{ - ID: id, - AgentID: arg.AgentID, - CreatedAt: arg.CreatedAt[index], - Level: arg.Level[index], - Output: output, - }) - outputLength += int32(len(output)) - } - for index, agent := range q.workspaceAgents { - if agent.ID != arg.AgentID { - continue - } - // Greater than 1MB, same as the PostgreSQL constraint! - if agent.StartupLogsLength+outputLength > (1 << 20) { - return nil, &pq.Error{ - Constraint: "max_startup_logs_length", - Table: "workspace_agents", - } + for _, group := range q.groups { + if group.OrganizationID == arg.OrganizationID && + group.Name == arg.Name { + return database.Group{}, errDuplicateKey } - agent.StartupLogsLength += outputLength - q.workspaceAgents[index] = agent - break } - q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...) - return logs, nil + + //nolint:gosimple + group := database.Group{ + ID: arg.ID, + Name: arg.Name, + OrganizationID: arg.OrganizationID, + AvatarURL: arg.AvatarURL, + QuotaAllowance: arg.QuotaAllowance, + } + + q.groups = append(q.groups, group) + + return group, nil } -func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error { +func (q *fakeQuerier) InsertGroupMember(_ context.Context, arg database.InsertGroupMemberParams) error { if err := validateDatabaseType(arg); err != nil { return err } @@ -3667,1320 +3560,1370 @@ func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.U q.mutex.Lock() defer q.mutex.Unlock() - for index, job := range q.provisionerJobs { - if arg.ID != job.ID { - continue + for _, member := range q.groupMembers { + if member.GroupID == arg.GroupID && + member.UserID == arg.UserID { + return errDuplicateKey } - job.UpdatedAt = arg.UpdatedAt - q.provisionerJobs[index] = job - return nil } - return sql.ErrNoRows + + //nolint:gosimple + q.groupMembers = append(q.groupMembers, database.GroupMember{ + GroupID: arg.GroupID, + UserID: arg.UserID, + }) + + return nil } -func (q *fakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg database.UpdateProvisionerJobWithCancelByIDParams) error { +func (q *fakeQuerier) InsertLicense( + _ context.Context, arg database.InsertLicenseParams, +) (database.License, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.License{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, job := range q.provisionerJobs { - if arg.ID != job.ID { - continue - } - job.CanceledAt = arg.CanceledAt - job.CompletedAt = arg.CompletedAt - q.provisionerJobs[index] = job - return nil + l := database.License{ + ID: q.lastLicenseID + 1, + UploadedAt: arg.UploadedAt, + JWT: arg.JWT, + Exp: arg.Exp, } - return sql.ErrNoRows + q.lastLicenseID = l.ID + q.licenses = append(q.licenses, l) + return l, nil } -func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, arg database.UpdateProvisionerJobWithCompleteByIDParams) error { +func (q *fakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.Organization{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, job := range q.provisionerJobs { - if arg.ID != job.ID { - continue - } - job.UpdatedAt = arg.UpdatedAt - job.CompletedAt = arg.CompletedAt - job.Error = arg.Error - job.ErrorCode = arg.ErrorCode - q.provisionerJobs[index] = job - return nil + organization := database.Organization{ + ID: arg.ID, + Name: arg.Name, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, } - return sql.ErrNoRows + q.organizations = append(q.organizations, organization) + return organization, nil } -func (q *fakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWorkspaceParams) (database.Workspace, error) { +func (q *fakeQuerier) InsertOrganizationMember(_ context.Context, arg database.InsertOrganizationMemberParams) (database.OrganizationMember, error) { if err := validateDatabaseType(arg); err != nil { - return database.Workspace{}, err + return database.OrganizationMember{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for i, workspace := range q.workspaces { - if workspace.Deleted || workspace.ID != arg.ID { - continue - } - for _, other := range q.workspaces { - if other.Deleted || other.ID == workspace.ID || workspace.OwnerID != other.OwnerID { - continue - } - if other.Name == arg.Name { - return database.Workspace{}, errDuplicateKey - } - } - - workspace.Name = arg.Name - q.workspaces[i] = workspace - - return workspace, nil + //nolint:gosimple + organizationMember := database.OrganizationMember{ + OrganizationID: arg.OrganizationID, + UserID: arg.UserID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + Roles: arg.Roles, } - - return database.Workspace{}, sql.ErrNoRows + q.organizationMembers = append(q.organizationMembers, organizationMember) + return organizationMember, nil } -func (q *fakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.UpdateWorkspaceAutostartParams) error { +func (q *fakeQuerier) InsertProvisionerDaemon(_ context.Context, arg database.InsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.ProvisionerDaemon{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, workspace := range q.workspaces { - if workspace.ID != arg.ID { - continue - } - workspace.AutostartSchedule = arg.AutostartSchedule - q.workspaces[index] = workspace - return nil + daemon := database.ProvisionerDaemon{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + Name: arg.Name, + Provisioners: arg.Provisioners, + Tags: arg.Tags, } - - return sql.ErrNoRows + q.provisionerDaemons = append(q.provisionerDaemons, daemon) + return daemon, nil } -func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateWorkspaceTTLParams) error { +func (q *fakeQuerier) InsertProvisionerJob(_ context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.ProvisionerJob{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, workspace := range q.workspaces { - if workspace.ID != arg.ID { - continue - } - workspace.Ttl = arg.Ttl - q.workspaces[index] = workspace - return nil + job := database.ProvisionerJob{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + OrganizationID: arg.OrganizationID, + InitiatorID: arg.InitiatorID, + Provisioner: arg.Provisioner, + StorageMethod: arg.StorageMethod, + FileID: arg.FileID, + Type: arg.Type, + Input: arg.Input, + Tags: arg.Tags, } - - return sql.ErrNoRows + q.provisionerJobs = append(q.provisionerJobs, job) + return job, nil } -func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { +func (q *fakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.InsertProvisionerJobLogsParams) ([]database.ProvisionerJobLog, error) { if err := validateDatabaseType(arg); err != nil { - return err + return nil, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, workspace := range q.workspaces { - if workspace.ID != arg.ID { - continue - } - workspace.LastUsedAt = arg.LastUsedAt - q.workspaces[index] = workspace - return nil + logs := make([]database.ProvisionerJobLog, 0) + id := int64(1) + if len(q.provisionerJobLogs) > 0 { + id = q.provisionerJobLogs[len(q.provisionerJobLogs)-1].ID } - - return sql.ErrNoRows -} - -func (q *fakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database.GetDeploymentWorkspaceStatsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - stat := database.GetDeploymentWorkspaceStatsRow{} - for _, workspace := range q.workspaces { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return stat, err - } - job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) - if err != nil { - return stat, err - } - if !job.StartedAt.Valid { - stat.PendingWorkspaces++ - continue - } - if job.StartedAt.Valid && - !job.CanceledAt.Valid && - time.Since(job.UpdatedAt) <= 30*time.Second && - !job.CompletedAt.Valid { - stat.BuildingWorkspaces++ - continue - } - if job.CompletedAt.Valid && - !job.CanceledAt.Valid && - !job.Error.Valid { - if build.Transition == database.WorkspaceTransitionStart { - stat.RunningWorkspaces++ - } - if build.Transition == database.WorkspaceTransitionStop { - stat.StoppedWorkspaces++ - } - continue - } - if job.CanceledAt.Valid || job.Error.Valid { - stat.FailedWorkspaces++ - continue - } + for index, output := range arg.Output { + id++ + logs = append(logs, database.ProvisionerJobLog{ + ID: id, + JobID: arg.JobID, + CreatedAt: arg.CreatedAt[index], + Source: arg.Source[index], + Level: arg.Level[index], + Stage: arg.Stage[index], + Output: output, + }) } - return stat, nil + q.provisionerJobLogs = append(q.provisionerJobLogs, logs...) + return logs, nil } -func (q *fakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) - for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { - agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) - } - } - - latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} - for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { - latestAgentStats[agentStat.AgentID] = agentStat - } +func (q *fakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplicaParams) (database.Replica, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Replica{}, err } - statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsRow{} - for _, agentStat := range latestAgentStats { - stat := statByAgent[agentStat.AgentID] - stat.SessionCountVSCode += agentStat.SessionCountVSCode - stat.SessionCountJetBrains += agentStat.SessionCountJetBrains - stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY - stat.SessionCountSSH += agentStat.SessionCountSSH - statByAgent[stat.AgentID] = stat - } + q.mutex.Lock() + defer q.mutex.Unlock() - latenciesByAgent := map[uuid.UUID][]float64{} - minimumDateByAgent := map[uuid.UUID]time.Time{} - for _, agentStat := range agentStatsCreatedAfter { - if agentStat.ConnectionMedianLatencyMS <= 0 { - continue - } - stat := statByAgent[agentStat.AgentID] - minimumDate := minimumDateByAgent[agentStat.AgentID] - if agentStat.CreatedAt.Before(minimumDate) || minimumDate.IsZero() { - minimumDateByAgent[agentStat.AgentID] = agentStat.CreatedAt - } - stat.WorkspaceRxBytes += agentStat.RxBytes - stat.WorkspaceTxBytes += agentStat.TxBytes - statByAgent[agentStat.AgentID] = stat - latenciesByAgent[agentStat.AgentID] = append(latenciesByAgent[agentStat.AgentID], agentStat.ConnectionMedianLatencyMS) + replica := database.Replica{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + StartedAt: arg.StartedAt, + UpdatedAt: arg.UpdatedAt, + Hostname: arg.Hostname, + RegionID: arg.RegionID, + RelayAddress: arg.RelayAddress, + Version: arg.Version, + DatabaseLatency: arg.DatabaseLatency, } + q.replicas = append(q.replicas, replica) + return replica, nil +} - tryPercentile := func(fs []float64, p float64) float64 { - if len(fs) == 0 { - return -1 - } - sort.Float64s(fs) - return fs[int(float64(len(fs))*p/100)] +func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) (database.Template, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Template{}, err } - for _, stat := range statByAgent { - stat.AggregatedFrom = minimumDateByAgent[stat.AgentID] - statByAgent[stat.AgentID] = stat + q.mutex.Lock() + defer q.mutex.Unlock() - latencies, ok := latenciesByAgent[stat.AgentID] - if !ok { - continue - } - stat.WorkspaceConnectionLatency50 = tryPercentile(latencies, 50) - stat.WorkspaceConnectionLatency95 = tryPercentile(latencies, 95) - statByAgent[stat.AgentID] = stat + //nolint:gosimple + template := database.Template{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + OrganizationID: arg.OrganizationID, + Name: arg.Name, + Provisioner: arg.Provisioner, + ActiveVersionID: arg.ActiveVersionID, + Description: arg.Description, + CreatedBy: arg.CreatedBy, + UserACL: arg.UserACL, + GroupACL: arg.GroupACL, + DisplayName: arg.DisplayName, + Icon: arg.Icon, + AllowUserCancelWorkspaceJobs: arg.AllowUserCancelWorkspaceJobs, + AllowUserAutostart: true, + AllowUserAutostop: true, } + q.templates = append(q.templates, template) + return template.DeepCopy(), nil +} - stats := make([]database.GetWorkspaceAgentStatsRow, 0, len(statByAgent)) - for _, agent := range statByAgent { - stats = append(stats, agent) +func (q *fakeQuerier) InsertTemplateVersion(_ context.Context, arg database.InsertTemplateVersionParams) (database.TemplateVersion, error) { + if err := validateDatabaseType(arg); err != nil { + return database.TemplateVersion{}, err } - return stats, nil -} -func (q *fakeQuerier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsAndLabelsRow, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) - latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} + //nolint:gosimple + version := database.TemplateVersion{ + ID: arg.ID, + TemplateID: arg.TemplateID, + OrganizationID: arg.OrganizationID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + Name: arg.Name, + Readme: arg.Readme, + JobID: arg.JobID, + CreatedBy: arg.CreatedBy, + } + q.templateVersions = append(q.templateVersions, version) + return version, nil +} - for _, agentStat := range q.workspaceAgentStats { - if agentStat.CreatedAt.After(createdAfter) { - agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) - latestAgentStats[agentStat.AgentID] = agentStat - } +func (q *fakeQuerier) InsertTemplateVersionParameter(_ context.Context, arg database.InsertTemplateVersionParameterParams) (database.TemplateVersionParameter, error) { + if err := validateDatabaseType(arg); err != nil { + return database.TemplateVersionParameter{}, err } - statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsAndLabelsRow{} + q.mutex.Lock() + defer q.mutex.Unlock() - // Session and connection metrics - for _, agentStat := range latestAgentStats { - stat := statByAgent[agentStat.AgentID] - stat.SessionCountVSCode += agentStat.SessionCountVSCode - stat.SessionCountJetBrains += agentStat.SessionCountJetBrains - stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY - stat.SessionCountSSH += agentStat.SessionCountSSH - stat.ConnectionCount += agentStat.ConnectionCount - if agentStat.ConnectionMedianLatencyMS >= 0 && stat.ConnectionMedianLatencyMS < agentStat.ConnectionMedianLatencyMS { - stat.ConnectionMedianLatencyMS = agentStat.ConnectionMedianLatencyMS - } - statByAgent[agentStat.AgentID] = stat + //nolint:gosimple + param := database.TemplateVersionParameter{ + TemplateVersionID: arg.TemplateVersionID, + Name: arg.Name, + DisplayName: arg.DisplayName, + Description: arg.Description, + Type: arg.Type, + Mutable: arg.Mutable, + DefaultValue: arg.DefaultValue, + Icon: arg.Icon, + Options: arg.Options, + ValidationError: arg.ValidationError, + ValidationRegex: arg.ValidationRegex, + ValidationMin: arg.ValidationMin, + ValidationMax: arg.ValidationMax, + ValidationMonotonic: arg.ValidationMonotonic, + Required: arg.Required, + LegacyVariableName: arg.LegacyVariableName, } + q.templateVersionParameters = append(q.templateVersionParameters, param) + return param, nil +} - // Tx, Rx metrics - for _, agentStat := range agentStatsCreatedAfter { - stat := statByAgent[agentStat.AgentID] - stat.RxBytes += agentStat.RxBytes - stat.TxBytes += agentStat.TxBytes - statByAgent[agentStat.AgentID] = stat +func (q *fakeQuerier) InsertTemplateVersionVariable(_ context.Context, arg database.InsertTemplateVersionVariableParams) (database.TemplateVersionVariable, error) { + if err := validateDatabaseType(arg); err != nil { + return database.TemplateVersionVariable{}, err } - // Labels - for _, agentStat := range agentStatsCreatedAfter { - stat := statByAgent[agentStat.AgentID] + q.mutex.Lock() + defer q.mutex.Unlock() - user, err := q.getUserByIDNoLock(agentStat.UserID) - if err != nil { - return nil, err - } + //nolint:gosimple + variable := database.TemplateVersionVariable{ + TemplateVersionID: arg.TemplateVersionID, + Name: arg.Name, + Description: arg.Description, + Type: arg.Type, + Value: arg.Value, + DefaultValue: arg.DefaultValue, + Required: arg.Required, + Sensitive: arg.Sensitive, + } + q.templateVersionVariables = append(q.templateVersionVariables, variable) + return variable, nil +} - stat.Username = user.Username +func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParams) (database.User, error) { + if err := validateDatabaseType(arg); err != nil { + return database.User{}, err + } - workspace, err := q.getWorkspaceByIDNoLock(ctx, agentStat.WorkspaceID) - if err != nil { - return nil, err + // There is a common bug when using dbfake that 2 inserted users have the + // same created_at time. This causes user order to not be deterministic, + // which breaks some unit tests. + // To fix this, we make sure that the created_at time is always greater + // than the last user's created_at time. + allUsers, _ := q.GetUsers(context.Background(), database.GetUsersParams{}) + if len(allUsers) > 0 { + lastUser := allUsers[len(allUsers)-1] + if arg.CreatedAt.Before(lastUser.CreatedAt) || + arg.CreatedAt.Equal(lastUser.CreatedAt) { + // 1 ms is a good enough buffer. + arg.CreatedAt = lastUser.CreatedAt.Add(time.Millisecond) } - stat.WorkspaceName = workspace.Name + } - agent, err := q.getWorkspaceAgentByIDNoLock(ctx, agentStat.AgentID) - if err != nil { - return nil, err - } - stat.AgentName = agent.Name + q.mutex.Lock() + defer q.mutex.Unlock() - statByAgent[agentStat.AgentID] = stat + for _, user := range q.users { + if user.Username == arg.Username && !user.Deleted { + return database.User{}, errDuplicateKey + } } - stats := make([]database.GetWorkspaceAgentStatsAndLabelsRow, 0, len(statByAgent)) - for _, agent := range statByAgent { - stats = append(stats, agent) + user := database.User{ + ID: arg.ID, + Email: arg.Email, + HashedPassword: arg.HashedPassword, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + Username: arg.Username, + Status: database.UserStatusActive, + RBACRoles: arg.RBACRoles, + LoginType: arg.LoginType, } - return stats, nil + q.users = append(q.users, user) + return user, nil } -func (q *fakeQuerier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - workspaces := []database.Workspace{} - for _, workspace := range q.workspaces { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return nil, err - } +func (q *fakeQuerier) InsertUserGroupsByName(_ context.Context, arg database.InsertUserGroupsByNameParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() - if build.Transition == database.WorkspaceTransitionStart && !build.Deadline.IsZero() && build.Deadline.Before(now) { - workspaces = append(workspaces, workspace) - continue + var groupIDs []uuid.UUID + for _, group := range q.groups { + for _, groupName := range arg.GroupNames { + if group.Name == groupName { + groupIDs = append(groupIDs, group.ID) + } } + } - if build.Transition == database.WorkspaceTransitionStop && workspace.AutostartSchedule.Valid { - workspaces = append(workspaces, workspace) - continue - } + for _, groupID := range groupIDs { + q.groupMembers = append(q.groupMembers, database.GroupMember{ + UserID: arg.UserID, + GroupID: groupID, + }) } - return workspaces, nil + return nil } -func (q *fakeQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(_ context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } - +func (q *fakeQuerier) InsertUserLink(_ context.Context, args database.InsertUserLinkParams) (database.UserLink, error) { q.mutex.Lock() defer q.mutex.Unlock() - for index, workspace := range q.workspaces { - if workspace.TemplateID != arg.TemplateID || !workspace.Ttl.Valid || workspace.Ttl.Int64 < arg.TemplateMaxTTL { - continue - } - - workspace.Ttl = sql.NullInt64{Int64: arg.TemplateMaxTTL, Valid: true} - q.workspaces[index] = workspace + //nolint:gosimple + link := database.UserLink{ + UserID: args.UserID, + LoginType: args.LoginType, + LinkedID: args.LinkedID, + OAuthAccessToken: args.OAuthAccessToken, + OAuthRefreshToken: args.OAuthRefreshToken, + OAuthExpiry: args.OAuthExpiry, } - return nil + q.userLinks = append(q.userLinks, link) + + return link, nil } -func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.Workspace{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.ID != arg.ID { - continue - } - workspaceBuild.UpdatedAt = arg.UpdatedAt - workspaceBuild.ProvisionerState = arg.ProvisionerState - workspaceBuild.Deadline = arg.Deadline - workspaceBuild.MaxDeadline = arg.MaxDeadline - q.workspaceBuilds[index] = workspaceBuild - return workspaceBuild, nil + //nolint:gosimple + workspace := database.Workspace{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + OwnerID: arg.OwnerID, + OrganizationID: arg.OrganizationID, + TemplateID: arg.TemplateID, + Name: arg.Name, + AutostartSchedule: arg.AutostartSchedule, + Ttl: arg.Ttl, + LastUsedAt: arg.LastUsedAt, } - return database.WorkspaceBuild{}, sql.ErrNoRows + q.workspaces = append(q.workspaces, workspace) + return workspace, nil } -func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) { if err := validateDatabaseType(arg); err != nil { - return database.WorkspaceBuild{}, err + return database.WorkspaceAgent{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.ID != arg.ID { - continue - } - workspaceBuild.DailyCost = arg.DailyCost - q.workspaceBuilds[index] = workspaceBuild - return workspaceBuild, nil + agent := database.WorkspaceAgent{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + ResourceID: arg.ResourceID, + AuthToken: arg.AuthToken, + AuthInstanceID: arg.AuthInstanceID, + EnvironmentVariables: arg.EnvironmentVariables, + Name: arg.Name, + Architecture: arg.Architecture, + OperatingSystem: arg.OperatingSystem, + Directory: arg.Directory, + StartupScriptBehavior: arg.StartupScriptBehavior, + StartupScript: arg.StartupScript, + InstanceMetadata: arg.InstanceMetadata, + ResourceMetadata: arg.ResourceMetadata, + ConnectionTimeoutSeconds: arg.ConnectionTimeoutSeconds, + TroubleshootingURL: arg.TroubleshootingURL, + MOTDFile: arg.MOTDFile, + LifecycleState: database.WorkspaceAgentLifecycleStateCreated, + ShutdownScript: arg.ShutdownScript, } - return database.WorkspaceBuild{}, sql.ErrNoRows -} -func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { - if err := validateDatabaseType(arg); err != nil { - return err - } + q.workspaceAgents = append(q.workspaceAgents, agent) + return agent, nil +} +func (q *fakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { q.mutex.Lock() defer q.mutex.Unlock() - for index, workspace := range q.workspaces { - if workspace.ID != arg.ID { - continue - } - workspace.Deleted = arg.Deleted - q.workspaces[index] = workspace - return nil + //nolint:gosimple + metadatum := database.WorkspaceAgentMetadatum{ + WorkspaceAgentID: arg.WorkspaceAgentID, + Script: arg.Script, + DisplayName: arg.DisplayName, + Key: arg.Key, + Timeout: arg.Timeout, + Interval: arg.Interval, } - return sql.ErrNoRows + + q.workspaceAgentMetadata = append(q.workspaceAgentMetadata, metadatum) + return nil } -func (q *fakeQuerier) InsertGitSSHKey(_ context.Context, arg database.InsertGitSSHKeyParams) (database.GitSSHKey, error) { +func (q *fakeQuerier) InsertWorkspaceAgentStartupLogs(_ context.Context, arg database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) { if err := validateDatabaseType(arg); err != nil { - return database.GitSSHKey{}, err + return nil, err } q.mutex.Lock() defer q.mutex.Unlock() - //nolint:gosimple - gitSSHKey := database.GitSSHKey{ - UserID: arg.UserID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - PrivateKey: arg.PrivateKey, - PublicKey: arg.PublicKey, + logs := []database.WorkspaceAgentStartupLog{} + id := int64(1) + if len(q.workspaceAgentLogs) > 0 { + id = q.workspaceAgentLogs[len(q.workspaceAgentLogs)-1].ID } - q.gitSSHKey = append(q.gitSSHKey, gitSSHKey) - return gitSSHKey, nil -} - -func (q *fakeQuerier) GetGitSSHKey(_ context.Context, userID uuid.UUID) (database.GitSSHKey, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, key := range q.gitSSHKey { - if key.UserID == userID { - return key, nil + outputLength := int32(0) + for index, output := range arg.Output { + id++ + logs = append(logs, database.WorkspaceAgentStartupLog{ + ID: id, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt[index], + Level: arg.Level[index], + Output: output, + }) + outputLength += int32(len(output)) + } + for index, agent := range q.workspaceAgents { + if agent.ID != arg.AgentID { + continue + } + // Greater than 1MB, same as the PostgreSQL constraint! + if agent.StartupLogsLength+outputLength > (1 << 20) { + return nil, &pq.Error{ + Constraint: "max_startup_logs_length", + Table: "workspace_agents", + } } + agent.StartupLogsLength += outputLength + q.workspaceAgents[index] = agent + break } - return database.GitSSHKey{}, sql.ErrNoRows + q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...) + return logs, nil } -func (q *fakeQuerier) UpdateGitSSHKey(_ context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { - if err := validateDatabaseType(arg); err != nil { - return database.GitSSHKey{}, err +func (q *fakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) { + if err := validateDatabaseType(p); err != nil { + return database.WorkspaceAgentStat{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, key := range q.gitSSHKey { - if key.UserID != arg.UserID { - continue - } - key.UpdatedAt = arg.UpdatedAt - key.PrivateKey = arg.PrivateKey - key.PublicKey = arg.PublicKey - q.gitSSHKey[index] = key - return key, nil + stat := database.WorkspaceAgentStat{ + ID: p.ID, + CreatedAt: p.CreatedAt, + WorkspaceID: p.WorkspaceID, + AgentID: p.AgentID, + UserID: p.UserID, + ConnectionsByProto: p.ConnectionsByProto, + ConnectionCount: p.ConnectionCount, + RxPackets: p.RxPackets, + RxBytes: p.RxBytes, + TxPackets: p.TxPackets, + TxBytes: p.TxBytes, + TemplateID: p.TemplateID, + SessionCountVSCode: p.SessionCountVSCode, + SessionCountJetBrains: p.SessionCountJetBrains, + SessionCountReconnectingPTY: p.SessionCountReconnectingPTY, + SessionCountSSH: p.SessionCountSSH, + ConnectionMedianLatencyMS: p.ConnectionMedianLatencyMS, } - return database.GitSSHKey{}, sql.ErrNoRows + q.workspaceAgentStats = append(q.workspaceAgentStats, stat) + return stat, nil } -func (q *fakeQuerier) InsertGroupMember(_ context.Context, arg database.InsertGroupMemberParams) error { +func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.WorkspaceApp{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for _, member := range q.groupMembers { - if member.GroupID == arg.GroupID && - member.UserID == arg.UserID { - return errDuplicateKey - } + if arg.SharingLevel == "" { + arg.SharingLevel = database.AppSharingLevelOwner } - //nolint:gosimple - q.groupMembers = append(q.groupMembers, database.GroupMember{ - GroupID: arg.GroupID, - UserID: arg.UserID, - }) - - return nil + // nolint:gosimple + workspaceApp := database.WorkspaceApp{ + ID: arg.ID, + AgentID: arg.AgentID, + CreatedAt: arg.CreatedAt, + Slug: arg.Slug, + DisplayName: arg.DisplayName, + Icon: arg.Icon, + Command: arg.Command, + Url: arg.Url, + External: arg.External, + Subdomain: arg.Subdomain, + SharingLevel: arg.SharingLevel, + HealthcheckUrl: arg.HealthcheckUrl, + HealthcheckInterval: arg.HealthcheckInterval, + HealthcheckThreshold: arg.HealthcheckThreshold, + Health: arg.Health, + } + q.workspaceApps = append(q.workspaceApps, workspaceApp) + return workspaceApp, nil } -func (q *fakeQuerier) DeleteGroupMemberFromGroup(_ context.Context, arg database.DeleteGroupMemberFromGroupParams) error { +func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { + if err := validateDatabaseType(arg); err != nil { + return database.WorkspaceBuild{}, err + } + q.mutex.Lock() defer q.mutex.Unlock() - for i, member := range q.groupMembers { - if member.UserID == arg.UserID && member.GroupID == arg.GroupID { - q.groupMembers = append(q.groupMembers[:i], q.groupMembers[i+1:]...) - } + workspaceBuild := database.WorkspaceBuild{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + WorkspaceID: arg.WorkspaceID, + TemplateVersionID: arg.TemplateVersionID, + BuildNumber: arg.BuildNumber, + Transition: arg.Transition, + InitiatorID: arg.InitiatorID, + JobID: arg.JobID, + ProvisionerState: arg.ProvisionerState, + Deadline: arg.Deadline, + Reason: arg.Reason, } - return nil + q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild) + return workspaceBuild, nil } -func (q *fakeQuerier) InsertUserGroupsByName(_ context.Context, arg database.InsertUserGroupsByNameParams) error { +func (q *fakeQuerier) InsertWorkspaceBuildParameters(_ context.Context, arg database.InsertWorkspaceBuildParametersParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + q.mutex.Lock() defer q.mutex.Unlock() - var groupIDs []uuid.UUID - for _, group := range q.groups { - for _, groupName := range arg.GroupNames { - if group.Name == groupName { - groupIDs = append(groupIDs, group.ID) - } - } - } - - for _, groupID := range groupIDs { - q.groupMembers = append(q.groupMembers, database.GroupMember{ - UserID: arg.UserID, - GroupID: groupID, + for index, name := range arg.Name { + q.workspaceBuildParameters = append(q.workspaceBuildParameters, database.WorkspaceBuildParameter{ + WorkspaceBuildID: arg.WorkspaceBuildID, + Name: name, + Value: arg.Value[index], }) } - return nil } -func (q *fakeQuerier) DeleteGroupMembersByOrgAndUser(_ context.Context, arg database.DeleteGroupMembersByOrgAndUserParams) error { +func (q *fakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() - newMembers := q.groupMembers[:0] - for _, member := range q.groupMembers { - if member.UserID != arg.UserID { - // Do not delete the other members - newMembers = append(newMembers, member) - } else if member.UserID == arg.UserID { - // We only want to delete from groups in the organization in the args. - for _, group := range q.groups { - // Find the group that the member is apartof. - if group.ID == member.GroupID { - // Only add back the member if the organization ID does not match - // the arg organization ID. Since the arg is saying which - // org to delete. - if group.OrganizationID != arg.OrganizationID { - newMembers = append(newMembers, member) - } - break - } - } + for _, p := range q.workspaceProxies { + if !p.Deleted && p.Name == arg.Name { + return database.WorkspaceProxy{}, errDuplicateKey } } - q.groupMembers = newMembers - return nil + p := database.WorkspaceProxy{ + ID: arg.ID, + Name: arg.Name, + DisplayName: arg.DisplayName, + Icon: arg.Icon, + TokenHashedSecret: arg.TokenHashedSecret, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + Deleted: false, + } + q.workspaceProxies = append(q.workspaceProxies, p) + return p, nil } -func (q *fakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) { +func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) { if err := validateDatabaseType(arg); err != nil { - return database.Group{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, group := range q.groups { - if group.ID == arg.ID { - group.Name = arg.Name - group.AvatarURL = arg.AvatarURL - group.QuotaAllowance = arg.QuotaAllowance - q.groups[i] = group - return group, nil - } + return database.WorkspaceResource{}, err } - return database.Group{}, sql.ErrNoRows -} -func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() - for index, key := range q.gitSSHKey { - if key.UserID != userID { - continue - } - q.gitSSHKey[index] = q.gitSSHKey[len(q.gitSSHKey)-1] - q.gitSSHKey = q.gitSSHKey[:len(q.gitSSHKey)-1] - return nil + //nolint:gosimple + resource := database.WorkspaceResource{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + JobID: arg.JobID, + Transition: arg.Transition, + Type: arg.Type, + Name: arg.Name, + Hide: arg.Hide, + Icon: arg.Icon, + DailyCost: arg.DailyCost, } - return sql.ErrNoRows + q.workspaceResources = append(q.workspaceResources, resource) + return resource, nil } -func (q *fakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { +func (q *fakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg database.InsertWorkspaceResourceMetadataParams) ([]database.WorkspaceResourceMetadatum, error) { if err := validateDatabaseType(arg); err != nil { return nil, err } - q.mutex.RLock() - defer q.mutex.RUnlock() - - logs := make([]database.GetAuditLogsOffsetRow, 0, arg.Limit) - - // q.auditLogs are already sorted by time DESC, so no need to sort after the fact. - for _, alog := range q.auditLogs { - if arg.Offset > 0 { - arg.Offset-- - continue - } - if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { - continue - } - if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) { - continue - } - if arg.ResourceID != uuid.Nil && alog.ResourceID != arg.ResourceID { - continue - } - if arg.Username != "" { - user, err := q.getUserByIDNoLock(alog.UserID) - if err == nil && !strings.EqualFold(arg.Username, user.Username) { - continue - } - } - if arg.Email != "" { - user, err := q.getUserByIDNoLock(alog.UserID) - if err == nil && !strings.EqualFold(arg.Email, user.Email) { - continue - } - } - if !arg.DateFrom.IsZero() { - if alog.Time.Before(arg.DateFrom) { - continue - } - } - if !arg.DateTo.IsZero() { - if alog.Time.After(arg.DateTo) { - continue - } - } - if arg.BuildReason != "" { - workspaceBuild, err := q.getWorkspaceBuildByIDNoLock(context.Background(), alog.ResourceID) - if err == nil && !strings.EqualFold(arg.BuildReason, string(workspaceBuild.Reason)) { - continue - } - } - - user, err := q.getUserByIDNoLock(alog.UserID) - userValid := err == nil - - logs = append(logs, database.GetAuditLogsOffsetRow{ - ID: alog.ID, - RequestID: alog.RequestID, - OrganizationID: alog.OrganizationID, - Ip: alog.Ip, - UserAgent: alog.UserAgent, - ResourceType: alog.ResourceType, - ResourceID: alog.ResourceID, - ResourceTarget: alog.ResourceTarget, - ResourceIcon: alog.ResourceIcon, - Action: alog.Action, - Diff: alog.Diff, - StatusCode: alog.StatusCode, - AdditionalFields: alog.AdditionalFields, - UserID: alog.UserID, - UserUsername: sql.NullString{String: user.Username, Valid: userValid}, - UserEmail: sql.NullString{String: user.Email, Valid: userValid}, - UserCreatedAt: sql.NullTime{Time: user.CreatedAt, Valid: userValid}, - UserStatus: database.NullUserStatus{UserStatus: user.Status, Valid: userValid}, - UserRoles: user.RBACRoles, - Count: 0, - }) + q.mutex.Lock() + defer q.mutex.Unlock() - if len(logs) >= int(arg.Limit) { - break - } + metadata := make([]database.WorkspaceResourceMetadatum, 0) + id := int64(1) + if len(q.workspaceResourceMetadata) > 0 { + id = q.workspaceResourceMetadata[len(q.workspaceResourceMetadata)-1].ID } - - count := int64(len(logs)) - for i := range logs { - logs[i].Count = count + for index, key := range arg.Key { + id++ + value := arg.Value[index] + metadata = append(metadata, database.WorkspaceResourceMetadatum{ + ID: id, + WorkspaceResourceID: arg.WorkspaceResourceID, + Key: key, + Value: sql.NullString{ + String: value, + Valid: value != "", + }, + Sensitive: arg.Sensitive[index], + }) } - - return logs, nil + q.workspaceResourceMetadata = append(q.workspaceResourceMetadata, metadata...) + return metadata, nil } -func (q *fakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAuditLogParams) (database.AuditLog, error) { - if err := validateDatabaseType(arg); err != nil { - return database.AuditLog{}, err - } - +func (q *fakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() - alog := database.AuditLog(arg) - - q.auditLogs = append(q.auditLogs, alog) - slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) bool { - return a.Time.Before(b.Time) - }) + for i, p := range q.workspaceProxies { + if p.ID == arg.ID { + p.Url = arg.Url + p.WildcardHostname = arg.WildcardHostname + p.UpdatedAt = database.Now() + q.workspaceProxies[i] = p + return p, nil + } + } + return database.WorkspaceProxy{}, sql.ErrNoRows +} - return alog, nil +func (*fakeQuerier) TryAcquireLock(_ context.Context, _ int64) (bool, error) { + return false, xerrors.New("TryAcquireLock must only be called within a transaction") } -func (q *fakeQuerier) InsertDeploymentID(_ context.Context, id string) error { +func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + q.mutex.Lock() defer q.mutex.Unlock() - q.deploymentID = id - return nil + for index, apiKey := range q.apiKeys { + if apiKey.ID != arg.ID { + continue + } + apiKey.LastUsed = arg.LastUsed + apiKey.ExpiresAt = arg.ExpiresAt + apiKey.IPAddress = arg.IPAddress + q.apiKeys[index] = apiKey + return nil + } + return sql.ErrNoRows } -func (q *fakeQuerier) GetDeploymentID(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - return q.deploymentID, nil -} +func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGitAuthLinkParams) (database.GitAuthLink, error) { + if err := validateDatabaseType(arg); err != nil { + return database.GitAuthLink{}, err + } -func (q *fakeQuerier) InsertDERPMeshKey(_ context.Context, id string) error { q.mutex.Lock() defer q.mutex.Unlock() + for index, gitAuthLink := range q.gitAuthLinks { + if gitAuthLink.ProviderID != arg.ProviderID { + continue + } + if gitAuthLink.UserID != arg.UserID { + continue + } + gitAuthLink.UpdatedAt = arg.UpdatedAt + gitAuthLink.OAuthAccessToken = arg.OAuthAccessToken + gitAuthLink.OAuthRefreshToken = arg.OAuthRefreshToken + gitAuthLink.OAuthExpiry = arg.OAuthExpiry + q.gitAuthLinks[index] = gitAuthLink - q.derpMeshKey = id - return nil + return gitAuthLink, nil + } + return database.GitAuthLink{}, sql.ErrNoRows } -func (q *fakeQuerier) GetDERPMeshKey(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - return q.derpMeshKey, nil -} +func (q *fakeQuerier) UpdateGitSSHKey(_ context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) { + if err := validateDatabaseType(arg); err != nil { + return database.GitSSHKey{}, err + } -func (q *fakeQuerier) UpsertLastUpdateCheck(_ context.Context, data string) error { q.mutex.Lock() defer q.mutex.Unlock() - q.lastUpdateCheck = []byte(data) - return nil -} - -func (q *fakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - if q.lastUpdateCheck == nil { - return "", sql.ErrNoRows + for index, key := range q.gitSSHKey { + if key.UserID != arg.UserID { + continue + } + key.UpdatedAt = arg.UpdatedAt + key.PrivateKey = arg.PrivateKey + key.PublicKey = arg.PublicKey + q.gitSSHKey[index] = key + return key, nil } - return string(q.lastUpdateCheck), nil + return database.GitSSHKey{}, sql.ErrNoRows } -func (q *fakeQuerier) UpsertServiceBanner(_ context.Context, data string) error { - q.mutex.RLock() - defer q.mutex.RUnlock() - - q.serviceBanner = []byte(data) - return nil -} +func (q *fakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Group{}, err + } -func (q *fakeQuerier) GetServiceBanner(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - if q.serviceBanner == nil { - return "", sql.ErrNoRows + for i, group := range q.groups { + if group.ID == arg.ID { + group.Name = arg.Name + group.AvatarURL = arg.AvatarURL + group.QuotaAllowance = arg.QuotaAllowance + q.groups[i] = group + return group, nil + } } - - return string(q.serviceBanner), nil + return database.Group{}, sql.ErrNoRows } -func (q *fakeQuerier) UpsertLogoURL(_ context.Context, data string) error { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) { + if err := validateDatabaseType(arg); err != nil { + return database.OrganizationMember{}, err + } - q.logoURL = data - return nil -} + q.mutex.Lock() + defer q.mutex.Unlock() -func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + for i, mem := range q.organizationMembers { + if mem.UserID == arg.UserID && mem.OrganizationID == arg.OrgID { + uniqueRoles := make([]string, 0, len(arg.GrantedRoles)) + exist := make(map[string]struct{}) + for _, r := range arg.GrantedRoles { + if _, ok := exist[r]; ok { + continue + } + exist[r] = struct{}{} + uniqueRoles = append(uniqueRoles, r) + } + sort.Strings(uniqueRoles) - if q.logoURL == "" { - return "", sql.ErrNoRows + mem.Roles = uniqueRoles + q.organizationMembers[i] = mem + return mem, nil + } } - return q.logoURL, nil + return database.OrganizationMember{}, sql.ErrNoRows } -func (q *fakeQuerier) GetAppSecurityKey(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - return q.appSecurityKey, nil -} +func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } -func (q *fakeQuerier) UpsertAppSecurityKey(_ context.Context, data string) error { q.mutex.Lock() defer q.mutex.Unlock() - q.appSecurityKey = data - return nil + for index, job := range q.provisionerJobs { + if arg.ID != job.ID { + continue + } + job.UpdatedAt = arg.UpdatedAt + q.provisionerJobs[index] = job + return nil + } + return sql.ErrNoRows } -func (q *fakeQuerier) InsertLicense( - _ context.Context, arg database.InsertLicenseParams, -) (database.License, error) { +func (q *fakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg database.UpdateProvisionerJobWithCancelByIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.License{}, err + return err } q.mutex.Lock() defer q.mutex.Unlock() - l := database.License{ - ID: q.lastLicenseID + 1, - UploadedAt: arg.UploadedAt, - JWT: arg.JWT, - Exp: arg.Exp, - } - q.lastLicenseID = l.ID - q.licenses = append(q.licenses, l) - return l, nil -} - -func (q *fakeQuerier) GetLicenseByID(_ context.Context, id int32) (database.License, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, license := range q.licenses { - if license.ID == id { - return license, nil + for index, job := range q.provisionerJobs { + if arg.ID != job.ID { + continue } + job.CanceledAt = arg.CanceledAt + job.CompletedAt = arg.CompletedAt + q.provisionerJobs[index] = job + return nil } - return database.License{}, sql.ErrNoRows + return sql.ErrNoRows } -func (q *fakeQuerier) GetLicenses(_ context.Context) ([]database.License, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - results := append([]database.License{}, q.licenses...) - sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) - return results, nil -} +func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, arg database.UpdateProvisionerJobWithCompleteByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } -func (q *fakeQuerier) GetUnexpiredLicenses(_ context.Context) ([]database.License, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - now := time.Now() - var results []database.License - for _, l := range q.licenses { - if l.Exp.After(now) { - results = append(results, l) + for index, job := range q.provisionerJobs { + if arg.ID != job.ID { + continue } + job.UpdatedAt = arg.UpdatedAt + job.CompletedAt = arg.CompletedAt + job.Error = arg.Error + job.ErrorCode = arg.ErrorCode + q.provisionerJobs[index] = job + return nil } - sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID }) - return results, nil + return sql.ErrNoRows } -func (q *fakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error) { +func (q *fakeQuerier) UpdateReplica(_ context.Context, arg database.UpdateReplicaParams) (database.Replica, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Replica{}, err + } + q.mutex.Lock() defer q.mutex.Unlock() - for index, l := range q.licenses { - if l.ID == id { - q.licenses[index] = q.licenses[len(q.licenses)-1] - q.licenses = q.licenses[:len(q.licenses)-1] - return id, nil + for index, replica := range q.replicas { + if replica.ID != arg.ID { + continue } + replica.Hostname = arg.Hostname + replica.StartedAt = arg.StartedAt + replica.StoppedAt = arg.StoppedAt + replica.UpdatedAt = arg.UpdatedAt + replica.RelayAddress = arg.RelayAddress + replica.RegionID = arg.RegionID + replica.Version = arg.Version + replica.Error = arg.Error + replica.DatabaseLatency = arg.DatabaseLatency + q.replicas[index] = replica + return replica, nil } - return 0, sql.ErrNoRows + return database.Replica{}, sql.ErrNoRows } -func (*fakeQuerier) DeleteOldWorkspaceAgentStartupLogs(_ context.Context) error { - // noop - return nil -} +func (q *fakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Template{}, err + } -func (q *fakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - for _, link := range q.userLinks { - if link.LinkedID == id { - return link, nil + for i, template := range q.templates { + if template.ID == arg.ID { + template.GroupACL = arg.GroupACL + template.UserACL = arg.UserACL + + q.templates[i] = template + return template.DeepCopy(), nil } } - return database.UserLink{}, sql.ErrNoRows + + return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) GetUserLinkByUserIDLoginType(_ context.Context, params database.GetUserLinkByUserIDLoginTypeParams) (database.UserLink, error) { - if err := validateDatabaseType(params); err != nil { - return database.UserLink{}, err +func (q *fakeQuerier) UpdateTemplateActiveVersionByID(_ context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err } - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - for _, link := range q.userLinks { - if link.UserID == params.UserID && link.LoginType == params.LoginType { - return link, nil + for index, template := range q.templates { + if template.ID != arg.ID { + continue } + template.ActiveVersionID = arg.ActiveVersionID + template.UpdatedAt = arg.UpdatedAt + q.templates[index] = template + return nil } - return database.UserLink{}, sql.ErrNoRows + return sql.ErrNoRows } -func (q *fakeQuerier) InsertUserLink(_ context.Context, args database.InsertUserLinkParams) (database.UserLink, error) { +func (q *fakeQuerier) UpdateTemplateDeletedByID(_ context.Context, arg database.UpdateTemplateDeletedByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + q.mutex.Lock() defer q.mutex.Unlock() - //nolint:gosimple - link := database.UserLink{ - UserID: args.UserID, - LoginType: args.LoginType, - LinkedID: args.LinkedID, - OAuthAccessToken: args.OAuthAccessToken, - OAuthRefreshToken: args.OAuthRefreshToken, - OAuthExpiry: args.OAuthExpiry, + for index, template := range q.templates { + if template.ID != arg.ID { + continue + } + template.Deleted = arg.Deleted + template.UpdatedAt = arg.UpdatedAt + q.templates[index] = template + return nil } - - q.userLinks = append(q.userLinks, link) - - return link, nil + return sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserLinkedID(_ context.Context, params database.UpdateUserLinkedIDParams) (database.UserLink, error) { - if err := validateDatabaseType(params); err != nil { - return database.UserLink{}, err +func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.UpdateTemplateMetaByIDParams) (database.Template, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Template{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for i, link := range q.userLinks { - if link.UserID == params.UserID && link.LoginType == params.LoginType { - link.LinkedID = params.LinkedID - - q.userLinks[i] = link - return link, nil + for idx, tpl := range q.templates { + if tpl.ID != arg.ID { + continue } + tpl.UpdatedAt = database.Now() + tpl.Name = arg.Name + tpl.DisplayName = arg.DisplayName + tpl.Description = arg.Description + tpl.Icon = arg.Icon + q.templates[idx] = tpl + return tpl.DeepCopy(), nil } - return database.UserLink{}, sql.ErrNoRows + return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateUserLink(_ context.Context, params database.UpdateUserLinkParams) (database.UserLink, error) { - if err := validateDatabaseType(params); err != nil { - return database.UserLink{}, err +func (q *fakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) { + if err := validateDatabaseType(arg); err != nil { + return database.Template{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for i, link := range q.userLinks { - if link.UserID == params.UserID && link.LoginType == params.LoginType { - link.OAuthAccessToken = params.OAuthAccessToken - link.OAuthRefreshToken = params.OAuthRefreshToken - link.OAuthExpiry = params.OAuthExpiry - - q.userLinks[i] = link - return link, nil + for idx, tpl := range q.templates { + if tpl.ID != arg.ID { + continue } + tpl.AllowUserAutostart = arg.AllowUserAutostart + tpl.AllowUserAutostop = arg.AllowUserAutostop + tpl.UpdatedAt = database.Now() + tpl.DefaultTTL = arg.DefaultTTL + tpl.MaxTTL = arg.MaxTTL + tpl.FailureTTL = arg.FailureTTL + tpl.InactivityTTL = arg.InactivityTTL + q.templates[idx] = tpl + return tpl.DeepCopy(), nil } - return database.UserLink{}, sql.ErrNoRows + return database.Template{}, sql.ErrNoRows } -func (q *fakeQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateTemplateVersionByID(_ context.Context, arg database.UpdateTemplateVersionByIDParams) (database.TemplateVersion, error) { + if err := validateDatabaseType(arg); err != nil { + return database.TemplateVersion{}, err + } - return q.getGroupByIDNoLock(ctx, id) -} + q.mutex.Lock() + defer q.mutex.Unlock() -func (q *fakeQuerier) getGroupByIDNoLock(_ context.Context, id uuid.UUID) (database.Group, error) { - for _, group := range q.groups { - if group.ID == id { - return group, nil + for index, templateVersion := range q.templateVersions { + if templateVersion.ID != arg.ID { + continue } + templateVersion.TemplateID = arg.TemplateID + templateVersion.UpdatedAt = arg.UpdatedAt + templateVersion.Name = arg.Name + q.templateVersions[index] = templateVersion + return templateVersion, nil } - - return database.Group{}, sql.ErrNoRows + return database.TemplateVersion{}, sql.ErrNoRows } -func (q *fakeQuerier) GetGroupByOrgAndName(_ context.Context, arg database.GetGroupByOrgAndNameParams) (database.Group, error) { +func (q *fakeQuerier) UpdateTemplateVersionDescriptionByJobID(_ context.Context, arg database.UpdateTemplateVersionDescriptionByJobIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Group{}, err + return err } - q.mutex.RLock() - defer q.mutex.RUnlock() + q.mutex.Lock() + defer q.mutex.Unlock() - for _, group := range q.groups { - if group.OrganizationID == arg.OrganizationID && - group.Name == arg.Name { - return group, nil + for index, templateVersion := range q.templateVersions { + if templateVersion.JobID != arg.JobID { + continue } + templateVersion.Readme = arg.Readme + templateVersion.UpdatedAt = arg.UpdatedAt + q.templateVersions[index] = templateVersion + return nil } - - return database.Group{}, sql.ErrNoRows -} - -func (q *fakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID) (database.Group, error) { - return q.InsertGroup(ctx, database.InsertGroupParams{ - ID: orgID, - Name: database.AllUsersGroup, - OrganizationID: orgID, - }) + return sql.ErrNoRows } -func (q *fakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupParams) (database.Group, error) { +func (q *fakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Context, arg database.UpdateTemplateVersionGitAuthProvidersByJobIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.Group{}, err + return err } q.mutex.Lock() defer q.mutex.Unlock() - for _, group := range q.groups { - if group.OrganizationID == arg.OrganizationID && - group.Name == arg.Name { - return database.Group{}, errDuplicateKey + for index, templateVersion := range q.templateVersions { + if templateVersion.JobID != arg.JobID { + continue } + templateVersion.GitAuthProviders = arg.GitAuthProviders + templateVersion.UpdatedAt = arg.UpdatedAt + q.templateVersions[index] = templateVersion + return nil } + return sql.ErrNoRows +} - //nolint:gosimple - group := database.Group{ - ID: arg.ID, - Name: arg.Name, - OrganizationID: arg.OrganizationID, - AvatarURL: arg.AvatarURL, - QuotaAllowance: arg.QuotaAllowance, +func (q *fakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error { + if err := validateDatabaseType(params); err != nil { + return err } - q.groups = append(q.groups, group) + q.mutex.Lock() + defer q.mutex.Unlock() - return group, nil + for i, u := range q.users { + if u.ID == params.ID { + u.Deleted = params.Deleted + q.users[i] = u + // NOTE: In the real world, this is done by a trigger. + for i, k := range q.apiKeys { + if k.UserID == u.ID { + q.apiKeys[i] = q.apiKeys[len(q.apiKeys)-1] + q.apiKeys = q.apiKeys[:len(q.apiKeys)-1] + } + } + return nil + } + } + return sql.ErrNoRows } -func (q *fakeQuerier) GetGroupMembers(_ context.Context, groupID uuid.UUID) ([]database.User, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - var members []database.GroupMember - for _, member := range q.groupMembers { - if member.GroupID == groupID { - members = append(members, member) - } +func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error { + if err := validateDatabaseType(arg); err != nil { + return err } - users := make([]database.User, 0, len(members)) + q.mutex.Lock() + defer q.mutex.Unlock() - for _, member := range members { - for _, user := range q.users { - if user.ID == member.UserID && user.Status == database.UserStatusActive && !user.Deleted { - users = append(users, user) - break - } + for i, user := range q.users { + if user.ID != arg.ID { + continue } + user.HashedPassword = arg.HashedPassword + q.users[i] = user + return nil } - - return users, nil + return sql.ErrNoRows } -func (q *fakeQuerier) GetGroupsByOrganizationID(_ context.Context, organizationID uuid.UUID) ([]database.Group, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateUserLastSeenAt(_ context.Context, arg database.UpdateUserLastSeenAtParams) (database.User, error) { + if err := validateDatabaseType(arg); err != nil { + return database.User{}, err + } - var groups []database.Group - for _, group := range q.groups { - // Omit the allUsers group. - if group.OrganizationID == organizationID && group.ID != organizationID { - groups = append(groups, group) + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, user := range q.users { + if user.ID != arg.ID { + continue } + user.LastSeenAt = arg.LastSeenAt + user.UpdatedAt = arg.UpdatedAt + q.users[index] = user + return user, nil } - - return groups, nil + return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) DeleteGroupByID(_ context.Context, id uuid.UUID) error { +func (q *fakeQuerier) UpdateUserLink(_ context.Context, params database.UpdateUserLinkParams) (database.UserLink, error) { + if err := validateDatabaseType(params); err != nil { + return database.UserLink{}, err + } + q.mutex.Lock() defer q.mutex.Unlock() - for i, group := range q.groups { - if group.ID == id { - q.groups = append(q.groups[:i], q.groups[i+1:]...) - return nil + for i, link := range q.userLinks { + if link.UserID == params.UserID && link.LoginType == params.LoginType { + link.OAuthAccessToken = params.OAuthAccessToken + link.OAuthRefreshToken = params.OAuthRefreshToken + link.OAuthExpiry = params.OAuthExpiry + + q.userLinks[i] = link + return link, nil } } - return sql.ErrNoRows + return database.UserLink{}, sql.ErrNoRows } -func (q *fakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error { +func (q *fakeQuerier) UpdateUserLinkedID(_ context.Context, params database.UpdateUserLinkedIDParams) (database.UserLink, error) { + if err := validateDatabaseType(params); err != nil { + return database.UserLink{}, err + } + q.mutex.Lock() defer q.mutex.Unlock() - for i, replica := range q.replicas { - if replica.UpdatedAt.Before(before) { - q.replicas = append(q.replicas[:i], q.replicas[i+1:]...) + for i, link := range q.userLinks { + if link.UserID == params.UserID && link.LoginType == params.LoginType { + link.LinkedID = params.LinkedID + + q.userLinks[i] = link + return link, nil } } - return nil + return database.UserLink{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplicaParams) (database.Replica, error) { +func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { - return database.Replica{}, err + return database.User{}, err } q.mutex.Lock() defer q.mutex.Unlock() - replica := database.Replica{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - StartedAt: arg.StartedAt, - UpdatedAt: arg.UpdatedAt, - Hostname: arg.Hostname, - RegionID: arg.RegionID, - RelayAddress: arg.RelayAddress, - Version: arg.Version, - DatabaseLatency: arg.DatabaseLatency, + for index, user := range q.users { + if user.ID != arg.ID { + continue + } + user.Email = arg.Email + user.Username = arg.Username + user.AvatarURL = arg.AvatarURL + q.users[index] = user + return user, nil } - q.replicas = append(q.replicas, replica) - return replica, nil + return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateReplica(_ context.Context, arg database.UpdateReplicaParams) (database.Replica, error) { +func (q *fakeQuerier) UpdateUserRoles(_ context.Context, arg database.UpdateUserRolesParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { - return database.Replica{}, err + return database.User{}, err } q.mutex.Lock() defer q.mutex.Unlock() - for index, replica := range q.replicas { - if replica.ID != arg.ID { + for index, user := range q.users { + if user.ID != arg.ID { continue } - replica.Hostname = arg.Hostname - replica.StartedAt = arg.StartedAt - replica.StoppedAt = arg.StoppedAt - replica.UpdatedAt = arg.UpdatedAt - replica.RelayAddress = arg.RelayAddress - replica.RegionID = arg.RegionID - replica.Version = arg.Version - replica.Error = arg.Error - replica.DatabaseLatency = arg.DatabaseLatency - q.replicas[index] = replica - return replica, nil - } - return database.Replica{}, sql.ErrNoRows -} -func (q *fakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.Replica, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - replicas := make([]database.Replica, 0) - for _, replica := range q.replicas { - if replica.UpdatedAt.After(updatedAt) && !replica.StoppedAt.Valid { - replicas = append(replicas, replica) + // Set new roles + user.RBACRoles = arg.GrantedRoles + // Remove duplicates and sort + uniqueRoles := make([]string, 0, len(user.RBACRoles)) + exist := make(map[string]struct{}) + for _, r := range user.RBACRoles { + if _, ok := exist[r]; ok { + continue + } + exist[r] = struct{}{} + uniqueRoles = append(uniqueRoles, r) } + sort.Strings(uniqueRoles) + user.RBACRoles = uniqueRoles + + q.users[index] = user + return user, nil } - return replicas, nil + return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) GetGitAuthLink(_ context.Context, arg database.GetGitAuthLinkParams) (database.GitAuthLink, error) { +func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUserStatusParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { - return database.GitAuthLink{}, err + return database.User{}, err } - q.mutex.RLock() - defer q.mutex.RUnlock() - for _, gitAuthLink := range q.gitAuthLinks { - if arg.UserID != gitAuthLink.UserID { - continue - } - if arg.ProviderID != gitAuthLink.ProviderID { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, user := range q.users { + if user.ID != arg.ID { continue } - return gitAuthLink, nil + user.Status = arg.Status + user.UpdatedAt = arg.UpdatedAt + q.users[index] = user + return user, nil } - return database.GitAuthLink{}, sql.ErrNoRows + return database.User{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertGitAuthLink(_ context.Context, arg database.InsertGitAuthLinkParams) (database.GitAuthLink, error) { +func (q *fakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWorkspaceParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { - return database.GitAuthLink{}, err + return database.Workspace{}, err } q.mutex.Lock() defer q.mutex.Unlock() - // nolint:gosimple - gitAuthLink := database.GitAuthLink{ - ProviderID: arg.ProviderID, - UserID: arg.UserID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OAuthAccessToken: arg.OAuthAccessToken, - OAuthRefreshToken: arg.OAuthRefreshToken, - OAuthExpiry: arg.OAuthExpiry, + + for i, workspace := range q.workspaces { + if workspace.Deleted || workspace.ID != arg.ID { + continue + } + for _, other := range q.workspaces { + if other.Deleted || other.ID == workspace.ID || workspace.OwnerID != other.OwnerID { + continue + } + if other.Name == arg.Name { + return database.Workspace{}, errDuplicateKey + } + } + + workspace.Name = arg.Name + q.workspaces[i] = workspace + + return workspace, nil } - q.gitAuthLinks = append(q.gitAuthLinks, gitAuthLink) - return gitAuthLink, nil + + return database.Workspace{}, sql.ErrNoRows } -func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGitAuthLinkParams) (database.GitAuthLink, error) { +func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error { if err := validateDatabaseType(arg); err != nil { - return database.GitAuthLink{}, err + return err } q.mutex.Lock() defer q.mutex.Unlock() - for index, gitAuthLink := range q.gitAuthLinks { - if gitAuthLink.ProviderID != arg.ProviderID { - continue - } - if gitAuthLink.UserID != arg.UserID { + + for index, agent := range q.workspaceAgents { + if agent.ID != arg.ID { continue } - gitAuthLink.UpdatedAt = arg.UpdatedAt - gitAuthLink.OAuthAccessToken = arg.OAuthAccessToken - gitAuthLink.OAuthRefreshToken = arg.OAuthRefreshToken - gitAuthLink.OAuthExpiry = arg.OAuthExpiry - q.gitAuthLinks[index] = gitAuthLink - - return gitAuthLink, nil + agent.FirstConnectedAt = arg.FirstConnectedAt + agent.LastConnectedAt = arg.LastConnectedAt + agent.DisconnectedAt = arg.DisconnectedAt + agent.UpdatedAt = arg.UpdatedAt + q.workspaceAgents[index] = agent + return nil } - return database.GitAuthLink{}, sql.ErrNoRows + return sql.ErrNoRows } -func (q *fakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UUID) (int64, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } - var sum int64 - for _, member := range q.groupMembers { - if member.UserID != userID { - continue - } - for _, group := range q.groups { - if group.ID == member.GroupID { - sum += int64(group.QuotaAllowance) - } + q.mutex.Lock() + defer q.mutex.Unlock() + for i, agent := range q.workspaceAgents { + if agent.ID == arg.ID { + agent.LifecycleState = arg.LifecycleState + q.workspaceAgents[i] = agent + return nil } } - return sum, nil + return sql.ErrNoRows } -func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUID) (int64, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - var sum int64 - for _, workspace := range q.workspaces { - if workspace.OwnerID != userID { - continue - } - if workspace.Deleted { - continue - } +func (q *fakeQuerier) UpdateWorkspaceAgentMetadata(_ context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() - var lastBuild database.WorkspaceBuild - for _, build := range q.workspaceBuilds { - if build.WorkspaceID != workspace.ID { - continue - } - if build.CreatedAt.After(lastBuild.CreatedAt) { - lastBuild = build - } + //nolint:gosimple + updated := database.WorkspaceAgentMetadatum{ + WorkspaceAgentID: arg.WorkspaceAgentID, + Key: arg.Key, + Value: arg.Value, + Error: arg.Error, + CollectedAt: arg.CollectedAt, + } + + for i, m := range q.workspaceAgentMetadata { + if m.WorkspaceAgentID == arg.WorkspaceAgentID && m.Key == arg.Key { + q.workspaceAgentMetadata[i] = updated + return nil } - sum += int64(lastBuild.DailyCost) } - return sum, nil + + return nil } -func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error { +func (q *fakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg database.UpdateWorkspaceAgentStartupByIDParams) error { if err := validateDatabaseType(arg); err != nil { return err } q.mutex.Lock() defer q.mutex.Unlock() - for i, agent := range q.workspaceAgents { - if agent.ID == arg.ID { - agent.LifecycleState = arg.LifecycleState - q.workspaceAgents[i] = agent - return nil + + for index, agent := range q.workspaceAgents { + if agent.ID != arg.ID { + continue } + + agent.Version = arg.Version + agent.ExpandedDirectory = arg.ExpandedDirectory + agent.Subsystem = arg.Subsystem + q.workspaceAgents[index] = agent + return nil } return sql.ErrNoRows } @@ -5002,121 +4945,123 @@ func (q *fakeQuerier) UpdateWorkspaceAgentStartupLogOverflowByID(_ context.Conte return sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateWorkspaceAppHealthByID(_ context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } - cpy := make([]database.WorkspaceProxy, 0, len(q.workspaceProxies)) + q.mutex.Lock() + defer q.mutex.Unlock() - for _, p := range q.workspaceProxies { - if !p.Deleted { - cpy = append(cpy, p) + for index, app := range q.workspaceApps { + if app.ID != arg.ID { + continue } + app.Health = arg.Health + q.workspaceApps[index] = app + return nil } - return cpy, nil + return sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceProxyByID(_ context.Context, id uuid.UUID) (database.WorkspaceProxy, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() +func (q *fakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.UpdateWorkspaceAutostartParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } - for _, proxy := range q.workspaceProxies { - if proxy.ID == id { - return proxy, nil + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, workspace := range q.workspaces { + if workspace.ID != arg.ID { + continue } + workspace.AutostartSchedule = arg.AutostartSchedule + q.workspaces[index] = workspace + return nil } - return database.WorkspaceProxy{}, sql.ErrNoRows + + return sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceProxyByName(_ context.Context, name string) (database.WorkspaceProxy, error) { +func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { + if err := validateDatabaseType(arg); err != nil { + return database.WorkspaceBuild{}, err + } + q.mutex.Lock() defer q.mutex.Unlock() - for _, proxy := range q.workspaceProxies { - if proxy.Deleted { + for index, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.ID != arg.ID { continue } - if proxy.Name == name { - return proxy, nil - } + workspaceBuild.UpdatedAt = arg.UpdatedAt + workspaceBuild.ProvisionerState = arg.ProvisionerState + workspaceBuild.Deadline = arg.Deadline + workspaceBuild.MaxDeadline = arg.MaxDeadline + q.workspaceBuilds[index] = workspaceBuild + return workspaceBuild, nil } - return database.WorkspaceProxy{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, params database.GetWorkspaceProxyByHostnameParams) (database.WorkspaceProxy, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - // Return zero rows if this is called with a non-sanitized hostname. The SQL - // version of this query does the same thing. - if !validProxyByHostnameRegex.MatchString(params.Hostname) { - return database.WorkspaceProxy{}, sql.ErrNoRows +func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { + if err := validateDatabaseType(arg); err != nil { + return database.WorkspaceBuild{}, err } - // This regex matches the SQL version. - accessURLRegex := regexp.MustCompile(`[^:]*://` + regexp.QuoteMeta(params.Hostname) + `([:/]?.)*`) + q.mutex.Lock() + defer q.mutex.Unlock() - for _, proxy := range q.workspaceProxies { - if proxy.Deleted { + for index, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.ID != arg.ID { continue } - if params.AllowAccessUrl && accessURLRegex.MatchString(proxy.Url) { - return proxy, nil - } - - // Compile the app hostname regex. This is slow sadly. - if params.AllowWildcardHostname { - wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname) - if err != nil { - return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err) - } - if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok { - return proxy, nil - } - } + workspaceBuild.DailyCost = arg.DailyCost + q.workspaceBuilds[index] = workspaceBuild + return workspaceBuild, nil } - - return database.WorkspaceProxy{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { +func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + q.mutex.Lock() defer q.mutex.Unlock() - for _, p := range q.workspaceProxies { - if !p.Deleted && p.Name == arg.Name { - return database.WorkspaceProxy{}, errDuplicateKey + for index, workspace := range q.workspaces { + if workspace.ID != arg.ID { + continue } + workspace.Deleted = arg.Deleted + q.workspaces[index] = workspace + return nil } + return sql.ErrNoRows +} - p := database.WorkspaceProxy{ - ID: arg.ID, - Name: arg.Name, - DisplayName: arg.DisplayName, - Icon: arg.Icon, - TokenHashedSecret: arg.TokenHashedSecret, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - Deleted: false, +func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error { + if err := validateDatabaseType(arg); err != nil { + return err } - q.workspaceProxies = append(q.workspaceProxies, p) - return p, nil -} -func (q *fakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() - for i, p := range q.workspaceProxies { - if p.ID == arg.ID { - p.Url = arg.Url - p.WildcardHostname = arg.WildcardHostname - p.UpdatedAt = database.Now() - q.workspaceProxies[i] = p - return p, nil + for index, workspace := range q.workspaces { + if workspace.ID != arg.ID { + continue } + workspace.LastUsedAt = arg.LastUsedAt + q.workspaces[index] = workspace + return nil } - return database.WorkspaceProxy{}, sql.ErrNoRows + + return sql.ErrNoRows } func (q *fakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { @@ -5159,21 +5104,52 @@ func (q *fakeQuerier) UpdateWorkspaceProxyDeleted(_ context.Context, arg databas return sql.ErrNoRows } -// isNull is only used in dbfake, so reflect is ok. Use this to make the logic -// look more similar to the postgres. -func isNull(v interface{}) bool { - return !isNotNull(v) +func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateWorkspaceTTLParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, workspace := range q.workspaces { + if workspace.ID != arg.ID { + continue + } + workspace.Ttl = arg.Ttl + q.workspaces[index] = workspace + return nil + } + + return sql.ErrNoRows } -func isNotNull(v interface{}) bool { - return reflect.ValueOf(v).FieldByName("Valid").Bool() +func (q *fakeQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(_ context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error { + if err := validateDatabaseType(arg); err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, workspace := range q.workspaces { + if workspace.TemplateID != arg.TemplateID || !workspace.Ttl.Valid || workspace.Ttl.Int64 < arg.TemplateMaxTTL { + continue + } + + workspace.Ttl = sql.NullInt64{Int64: arg.TemplateMaxTTL, Valid: true} + q.workspaces[index] = workspace + } + + return nil } -func (q *fakeQuerier) GetDefaultProxyConfig(_ context.Context) (database.GetDefaultProxyConfigRow, error) { - return database.GetDefaultProxyConfigRow{ - DisplayName: q.defaultProxyDisplayName, - IconUrl: q.defaultProxyIconURL, - }, nil +func (q *fakeQuerier) UpsertAppSecurityKey(_ context.Context, data string) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.appSecurityKey = data + return nil } func (q *fakeQuerier) UpsertDefaultProxy(_ context.Context, arg database.UpsertDefaultProxyParams) error { @@ -5181,3 +5157,27 @@ func (q *fakeQuerier) UpsertDefaultProxy(_ context.Context, arg database.UpsertD q.defaultProxyIconURL = arg.IconUrl return nil } + +func (q *fakeQuerier) UpsertLastUpdateCheck(_ context.Context, data string) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + q.lastUpdateCheck = []byte(data) + return nil +} + +func (q *fakeQuerier) UpsertLogoURL(_ context.Context, data string) error { + q.mutex.RLock() + defer q.mutex.RUnlock() + + q.logoURL = data + return nil +} + +func (q *fakeQuerier) UpsertServiceBanner(_ context.Context, data string) error { + q.mutex.RLock() + defer q.mutex.RUnlock() + + q.serviceBanner = []byte(data) + return nil +} diff --git a/coderd/database/gen/fake/main.go b/coderd/database/gen/fake/main.go new file mode 100644 index 0000000000000..b74f5d18d7f7b --- /dev/null +++ b/coderd/database/gen/fake/main.go @@ -0,0 +1,199 @@ +package main + +import ( + "go/format" + "go/token" + "log" + "os" + + "github.com/dave/dst" + "github.com/dave/dst/decorator" + "github.com/dave/dst/decorator/resolver/goast" + "github.com/dave/dst/decorator/resolver/guess" + "golang.org/x/xerrors" +) + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + funcs, err := readStoreInterface() + if err != nil { + return err + } + funcByName := map[string]struct{}{} + for _, f := range funcs { + funcByName[f.Name] = struct{}{} + } + declByName := map[string]*dst.FuncDecl{} + + dbfake, err := os.ReadFile("./dbfake/dbfake.go") + if err != nil { + return xerrors.Errorf("read dbfake: %w", err) + } + + // Required to preserve imports! + f, err := decorator.NewDecoratorWithImports(token.NewFileSet(), "dbfake", goast.New()).Parse(dbfake) + if err != nil { + return xerrors.Errorf("parse dbfake: %w", err) + } + + for i := 0; i < len(f.Decls); i++ { + funcDecl, ok := f.Decls[i].(*dst.FuncDecl) + if !ok || funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 { + continue + } + // Check if the receiver is the struct we're interested in + starExpr, ok := funcDecl.Recv.List[0].Type.(*dst.StarExpr) + if !ok { + continue + } + ident, ok := starExpr.X.(*dst.Ident) + if !ok || ident.Name != "fakeQuerier" { + continue + } + if _, ok := funcByName[funcDecl.Name.Name]; !ok { + continue + } + declByName[funcDecl.Name.Name] = funcDecl + f.Decls = append(f.Decls[:i], f.Decls[i+1:]...) + i-- + } + + for _, fn := range funcs { + decl, ok := declByName[fn.Name] + if !ok { + // Not implemented! + decl = &dst.FuncDecl{ + Name: dst.NewIdent(fn.Name), + Type: &dst.FuncType{ + Func: true, + TypeParams: fn.Func.TypeParams, + Params: fn.Func.Params, + Results: fn.Func.Results, + Decs: fn.Func.Decs, + }, + Recv: &dst.FieldList{ + List: []*dst.Field{{ + Names: []*dst.Ident{dst.NewIdent("q")}, + Type: dst.NewIdent("*fakeQuerier"), + }}, + }, + Decs: dst.FuncDeclDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.EmptyLine, + After: dst.EmptyLine, + }, + }, + Body: &dst.BlockStmt{ + List: []dst.Stmt{ + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "panic", + }, + Args: []dst.Expr{ + &dst.BasicLit{ + Kind: token.STRING, + Value: "\"Not implemented\"", + }, + }, + }, + }, + }, + }, + } + } + f.Decls = append(f.Decls, decl) + } + + file, err := os.OpenFile("./dbfake/dbfake.go", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) + if err != nil { + return xerrors.Errorf("open dbfake: %w", err) + } + defer file.Close() + + // Required to preserve imports! + restorer := decorator.NewRestorerWithImports("dbfake", guess.New()) + restored, err := restorer.RestoreFile(f) + if err != nil { + return xerrors.Errorf("restore dbfake: %w", err) + } + err = format.Node(file, restorer.Fset, restored) + return err +} + +type storeMethod struct { + Name string + Func *dst.FuncType +} + +func readStoreInterface() ([]storeMethod, error) { + querier, err := os.ReadFile("./querier.go") + if err != nil { + return nil, xerrors.Errorf("read querier: %w", err) + } + f, err := decorator.Parse(querier) + if err != nil { + return nil, err + } + + var sqlcQuerier *dst.InterfaceType + for _, decl := range f.Decls { + genDecl, ok := decl.(*dst.GenDecl) + if !ok { + continue + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*dst.TypeSpec) + if !ok { + continue + } + if typeSpec.Name.Name != "sqlcQuerier" { + continue + } + sqlcQuerier, ok = typeSpec.Type.(*dst.InterfaceType) + if !ok { + return nil, xerrors.Errorf("unexpected sqlcQuerier type: %T", typeSpec.Type) + } + break + } + } + if sqlcQuerier == nil { + return nil, xerrors.Errorf("sqlcQuerier not found") + } + funcs := []storeMethod{} + for _, method := range sqlcQuerier.Methods.List { + funcType, ok := method.Type.(*dst.FuncType) + if !ok { + continue + } + + for _, t := range []*dst.FieldList{funcType.Params, funcType.Results} { + if t == nil { + continue + } + for _, f := range t.List { + ident, ok := f.Type.(*dst.Ident) + if !ok { + continue + } + if !ident.IsExported() { + continue + } + ident.Path = "github.com/coder/coder/coderd/database" + } + } + + funcs = append(funcs, storeMethod{ + Name: method.Names[0].Name, + Func: funcType, + }) + } + return funcs, nil +} diff --git a/coderd/database/generate.sh b/coderd/database/generate.sh index 5669f121a57e0..8eda82be03812 100755 --- a/coderd/database/generate.sh +++ b/coderd/database/generate.sh @@ -58,4 +58,8 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") # Generate enums (e.g. unique constraints). go run gen/enum/main.go + + # Generate the database fake! + go run gen/fake/main.go + go run golang.org/x/tools/cmd/goimports@latest -w ./dbfake/dbfake.go ) diff --git a/go.mod b/go.mod index 8af8ddee8cc10..6e9ecfe34ad22 100644 --- a/go.mod +++ b/go.mod @@ -357,6 +357,7 @@ require github.com/gobwas/httphead v0.1.0 require ( github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/dave/dst v0.27.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect diff --git a/go.sum b/go.sum index f8a24d4f20c53..b7e68714a6fd1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +4d63.com/gochecknoglobals v0.1.0/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= cdr.dev/slog v1.5.3 h1:Ry3RZLX6r1/n7Yud9K9Wz7h230VWKxl8m/COPmnWIyM= cdr.dev/slog v1.5.3/go.mod h1:vW6Q4gGoDZSb4Db2wxAZoUba/HRUpen1g0fCu06zrjQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -51,6 +52,8 @@ filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7 filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o= github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= +github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -59,6 +62,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= @@ -68,8 +73,11 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -86,6 +94,7 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= github.com/ammario/tlru v0.3.0 h1:yK8ESoFlEyz/BVVL8yZQKAUzJwFJR/j9EfxjnKxtR/Q= github.com/ammario/tlru v0.3.0/go.mod h1:aYzRFu0XLo4KavE9W8Lx7tzjkX+pAApz+NgcKYIFUBQ= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= @@ -105,6 +114,8 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloD github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= +github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= @@ -124,9 +135,12 @@ github.com/bep/golibsass v1.1.0 h1:pjtXr00IJZZaOdfryNa9wARTB3Q0BmxC3/V1KNcgyTw= github.com/bep/golibsass v1.1.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= +github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b h1:UJeNthMS3NHVtMFKMhzZNxdaXpYqQlbLrDRtVXorT7w= github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0= +github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= @@ -140,6 +154,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= @@ -153,6 +168,7 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -211,9 +227,13 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= +github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= +github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= @@ -245,6 +265,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -265,6 +287,7 @@ github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvD github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE= @@ -288,11 +311,14 @@ github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -325,10 +351,18 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -390,6 +424,16 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.43.0/go.mod h1:VIFlUqidx5ggxDfQagdvd9E67UjMXtTHBkBQ7sHoC5Q= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= @@ -443,12 +487,17 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.10.0 h1:ebSgKfMxynOdxw8QQuFOKMgomqeLGPqNLQox2bo42zg= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 h1:I6ITHEanAwjB0FvaxmGm8pKqmCLR7QIe05ZmO4QAXMw= @@ -459,6 +508,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= @@ -515,9 +565,14 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg= github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI= github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= @@ -528,6 +583,7 @@ github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2C github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= @@ -536,11 +592,13 @@ github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6k github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -548,6 +606,7 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= @@ -565,6 +624,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= +github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= github.com/kylecarbs/embedded-postgres v1.17.1-0.20220615202325-461532cecd3a h1:uOnis+HNE6e6eR17YlqzKk51GDahd7E/FacnZxS8h8w= github.com/kylecarbs/embedded-postgres v1.17.1-0.20220615202325-461532cecd3a/go.mod h1:0B+3bPsMvcNgR9nN+bdM2x9YaNYDnf3ksUqYp1OAub0= github.com/kylecarbs/go-httpstat v0.0.0-20220831233600-c91452099472 h1:KXbxoQY9tOxgacpw0vbHWfIb56Xuzgi0Oql5yr6RYaA= @@ -579,6 +640,9 @@ github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= +github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= +github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -588,10 +652,13 @@ github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= +github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -616,6 +683,7 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ= @@ -633,6 +701,8 @@ github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxn github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= +github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= @@ -642,6 +712,7 @@ github.com/miekg/dns v1.1.45 h1:g5fRIhm9nx7g8osrAvgb16QJfmyMsyOCb+J7LSv+Qzk= github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -661,8 +732,12 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= @@ -678,15 +753,21 @@ github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4Y github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.6.6 h1:U6+mJ80p3weR4oP+Z+Pb2EVkSbt1MUwweBbUcF1hVqQ= github.com/niklasfasching/go-org v1.6.6/go.mod h1:o3pMQpO9n6RNBXz2Oc2DiRkaVwjns0JElyKiG7yXwA4= +github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/open-policy-agent/opa v0.51.0 h1:2hS5xhos8HtkN+mgpqMhNJSFtn/1n/h3wh+AeTPJg6Q= github.com/open-policy-agent/opa v0.51.0/go.mod h1:OjmwLfXdeR7skSxrt8Yd3ScXTqPxyJn7GeTRJrcEerU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -699,8 +780,10 @@ github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.m github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= @@ -719,6 +802,7 @@ github.com/pkg/sftp v1.13.6-0.20221018182125-7da137aa03f0 h1:QJypP3NZEUt+ka49zyp github.com/pkg/sftp v1.13.6-0.20221018182125-7da137aa03f0/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -728,6 +812,7 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ= github.com/quasilyte/go-ruleguard/dsl v0.3.21 h1:vNkC6fC6qMLzCOGbnIHOd5ixUGgTbp3Z4fGnUgULlDA= github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= @@ -742,19 +827,27 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= +github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= +github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= @@ -766,6 +859,8 @@ github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -783,6 +878,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= @@ -790,6 +886,7 @@ github.com/swaggo/http-swagger/v2 v2.0.1 h1:mNOBLxDjSNwCKlMxcErjjvct/xhc9t2KIO48 github.com/swaggo/http-swagger/v2 v2.0.1/go.mod h1:XYhrQVIKz13CxuKD4p4kvpaRB4jJ1/MlfQXVOE+CX8Y= github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tabbed/pqtype v0.1.1 h1:PhEcb9JZ8jr7SUjJDFjRPxny0M8fkXZrxn/a9yQfoZg= github.com/tabbed/pqtype v0.1.1/go.mod h1:HLt2kLJPcUhODQkYn3mJkMHXVsuv3Z2n5NZEeKXL0Uk= @@ -811,6 +908,9 @@ github.com/tdewolff/parse/v2 v2.6.5 h1:lYvWBk55GkqKl0JJenGpmrgu/cPHQQ6/Mm1hBGswo github.com/tdewolff/parse/v2 v2.6.5/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs= github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= +github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= +github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0= github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= @@ -821,12 +921,17 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -856,8 +961,11 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -872,6 +980,7 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= @@ -906,6 +1015,7 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4= go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= @@ -1337,9 +1447,11 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1366,6 +1478,8 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg= inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=