From e3b62be22c44fcc87ec85f407175b14c0c7f277e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 09:38:20 +0100 Subject: [PATCH 1/8] fix: always return number of workspaces --- coderd/database/modelqueries.go | 11 +- coderd/database/querier.go | 420 ------------------------- coderd/database/queries.sql.go | 109 ++++--- coderd/database/queries/workspaces.sql | 59 +++- 4 files changed, 122 insertions(+), 477 deletions(-) delete mode 100644 coderd/database/querier.go diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 34d43ecd924bd..4f0c31f62d829 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -215,6 +215,9 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa // The name comment is for metric tracking query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filtered) rows, err := q.db.QueryContext(ctx, query, + arg.RequesterID, + arg.Offset, + arg.Limit, arg.Deleted, arg.Status, arg.OwnerID, @@ -228,9 +231,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa arg.LastUsedBefore, arg.LastUsedAfter, arg.UsingActive, - arg.RequesterID, - arg.Offset, - arg.Limit, ) if err != nil { return nil, err @@ -258,6 +258,11 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.TemplateName, &i.TemplateVersionID, &i.TemplateVersionName, + &i.Username, + &i.LatestBuildCompletedAt, + &i.LatestBuildCanceledAt, + &i.LatestBuildError, + &i.LatestBuildTransition, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/querier.go b/coderd/database/querier.go deleted file mode 100644 index 92ee81f85fd91..0000000000000 --- a/coderd/database/querier.go +++ /dev/null @@ -1,420 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.25.0 - -package database - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -type sqlcQuerier interface { - // Blocks until the lock is acquired. - // - // This must be called from within a transaction. The lock will be automatically - // released when the transaction ends. - AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error - // Acquires the lock for a single job that isn't started, completed, - // canceled, and that matches an array of provisioner types. - // - // SKIP LOCKED is used to jump over locked rows. This prevents - // multiple provisioners from acquiring the same jobs. See: - // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE - AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) - // Bumps the workspace deadline by the template's configured "activity_bump" - // duration (default 1h). If the workspace bump will cross an autostart - // threshold, then the bump is autostart + TTL. This is the deadline behavior if - // the workspace was to autostart from a stopped state. - // - // Max deadline is respected, and the deadline will never be bumped past it. - // The deadline will never decrease. - // We only bump if the template has an activity bump duration set. - // We only bump if the raw interval is positive and non-zero. - // We only bump if workspace shutdown is manual. - // We only bump when 5% of the deadline has elapsed. - ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error - // AllUserIDs returns all UserIDs regardless of user status or deletion. - AllUserIDs(ctx context.Context) ([]uuid.UUID, error) - // Archiving templates is a soft delete action, so is reversible. - // Archiving prevents the version from being used and discovered - // by listing. - // Only unused template versions will be archived, which are any versions not - // referenced by the latest build of a workspace. - ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) - BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error - CleanTailnetCoordinators(ctx context.Context) error - CleanTailnetLostPeers(ctx context.Context) error - CleanTailnetTunnels(ctx context.Context) error - DeleteAPIKeyByID(ctx context.Context, id string) error - DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error - DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error - DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - DeleteCoordinator(ctx context.Context, id uuid.UUID) error - DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error - DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error - DeleteGroupByID(ctx context.Context, id uuid.UUID) error - DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error - DeleteLicense(ctx context.Context, id int32) (int32, error) - DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error - DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error - DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error - // Delete provisioner daemons that have been created at least a week ago - // and have not connected to coderd since a week. - // A provisioner daemon with "zeroed" last_seen_at column indicates possible - // connectivity issues (no provisioner daemon activity since registration). - DeleteOldProvisionerDaemons(ctx context.Context) error - // If an agent hasn't connected in the last 7 days, we purge it's logs. - // Logs can take up a lot of space, so it's important we clean up frequently. - DeleteOldWorkspaceAgentLogs(ctx context.Context) error - DeleteOldWorkspaceAgentStats(ctx context.Context) error - DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error - DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) - DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) - DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error - DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) - DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) - DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error - FavoriteWorkspace(ctx context.Context, id uuid.UUID) error - GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) - // there is no unique constraint on empty token names - GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) - GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) - GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) - GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) - GetActiveUserCount(ctx context.Context) (int64, error) - GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) - GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) - // For PG Coordinator HTMLDebug - GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) - GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) - GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) - GetAppSecurityKey(ctx context.Context) (string, error) - GetApplicationName(ctx context.Context) (string, error) - // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided - // ID. - GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) - // This function returns roles for authorization purposes. Implied member roles - // are included. - GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) - GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) - GetDERPMeshKey(ctx context.Context) (string, error) - GetDefaultOrganization(ctx context.Context) (Organization, error) - GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) - GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) - GetDeploymentID(ctx context.Context) (string, error) - GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) - GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) - GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) - GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) - GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) - GetFileByID(ctx context.Context, id uuid.UUID) (File, error) - // Get all templates that use a file. - GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) - GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) - GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) - GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) - // If the group is a user made group, then we need to check the group_members table. - // If it is the "Everyone" group, then we need to check the organization_members table. - GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error) - GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) - GetHealthSettings(ctx context.Context) (string, error) - GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) - GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) - GetLastUpdateCheck(ctx context.Context) (string, error) - GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) - GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) - GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) - GetLicenseByID(ctx context.Context, id int32) (License, error) - GetLicenses(ctx context.Context) ([]License, error) - GetLogoURL(ctx context.Context) (string, error) - GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) - GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) - GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) - GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) - GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) - GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) - GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) - GetOAuthSigningKey(ctx context.Context) (string, error) - GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) - GetOrganizationByName(ctx context.Context, name string) (Organization, error) - GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) - GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error) - GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]OrganizationMember, error) - GetOrganizations(ctx context.Context) ([]Organization, error) - GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) - GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) - GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) - GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) - GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) - GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) - GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) - GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) - GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) - GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) - GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) - GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) - GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) - GetServiceBanner(ctx context.Context) (string, error) - GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) - GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) - GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) - GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) - GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) - // GetTemplateAppInsights returns the aggregate usage of each app in a given - // timeframe. The result can be filtered on template_ids, meaning only user data - // from workspaces based on those templates will be included. - GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) - GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) - GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) - GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) - GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) - GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) - // GetTemplateInsights has a granularity of 5 minutes where if a session/app was - // in use during a minute, we will add 5 minutes to the total usage for that - // session/app (per user). - GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) - // GetTemplateInsightsByInterval returns all intervals between start and end - // time, if end time is a partial interval, it will be included in the results and - // that interval will be shorter than a full one. If there is no data for a selected - // interval/template, it will be included in the results with 0 active users. - GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) - GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) - // GetTemplateParameterInsights does for each template in a given timeframe, - // look for the latest workspace build (for every workspace) that has been - // created in the timeframe and return the aggregate usage counts of parameter - // values. - GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) - GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) - GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) - GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) - GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) - GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) - GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) - GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) - GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) - GetTemplates(ctx context.Context) ([]Template, error) - GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) - GetUnexpiredLicenses(ctx context.Context) ([]License, error) - // GetUserActivityInsights returns the ranking with top active users. - // The result can be filtered on template_ids, meaning only user data from workspaces - // based on those templates will be included. - // Note: When selecting data from multiple templates or the entire deployment, - // be aware that it may lead to an increase in "usage" numbers (cumulative). In such cases, - // users may be counted multiple times for the same time interval if they have used multiple templates - // simultaneously. - GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) - GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) - GetUserByID(ctx context.Context, id uuid.UUID) (User, error) - GetUserCount(ctx context.Context) (int64, error) - // GetUserLatencyInsights returns the median and 95th percentile connection - // latency that users have experienced. The result can be filtered on - // template_ids, meaning only user data from workspaces based on those templates - // will be included. - GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) - GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) - GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) - GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) - GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) - // This will never return deleted users. - GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) - // This shouldn't check for deleted, because it's frequently used - // to look up references to actions. eg. a user could build a workspace - // for another user, then be deleted... we still want them to appear! - GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) - GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndOwnerByAuthTokenRow, error) - GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) - GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) - GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) - GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) - GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) - GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) - GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) - GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) - GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) - GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) - GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) - GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) - GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) - GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) - GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) - GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) - GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) - GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) - GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) - GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) - GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) - GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) - GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (GetWorkspaceByAgentIDRow, error) - GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) - GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) - GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) - GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) - // Finds a workspace proxy that has an access URL or app hostname that matches - // the provided hostname. This is to check if a hostname matches any workspace - // proxy. - // - // The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling - // this query. The scheme, port and path should be stripped. - // - GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) - GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) - GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) - GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) - GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) - GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) - GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) - GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) - GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) - GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) - GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) - GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) - InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) - // We use the organization_id as the id - // for simplicity since all users is - // every member of the org. - InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) - InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) - InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error - InsertDERPMeshKey(ctx context.Context, value string) error - InsertDeploymentID(ctx context.Context, value string) error - InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) - InsertFile(ctx context.Context, arg InsertFileParams) (File, error) - InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) - InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) - InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error - InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) - // Inserts any group by name that does not exist. All new groups are given - // a random uuid, are inserted into the same organization. They have the default - // values for avatar, display name, and quota allowance (all zero values). - // If the name conflicts, do nothing. - InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) - InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) - InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) - InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) - InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) - InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) - InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) - InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) - InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) - InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) - InsertTemplate(ctx context.Context, arg InsertTemplateParams) error - InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error - InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) - InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) - InsertUser(ctx context.Context, arg InsertUserParams) (User, error) - // InsertUserGroupsByName adds a user to all provided groups, if they exist. - InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error - InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) - InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) - InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) - InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) - InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) - InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error - InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) - InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) - InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error - InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) - InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error - InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error - InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error - InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) - InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) - InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) - ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) - RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) - RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error - RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error - // Non blocking lock. Returns true if the lock was acquired, false otherwise. - // - // This must be called from within a transaction. The lock will be automatically - // released when the transaction ends. - TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) - // This will always work regardless of the current state of the template version. - UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error - UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error - UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error - UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) - UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) - UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) - UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) - UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) - UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) - UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) - UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error - UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error - UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error - UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error - UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) - UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error - UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error - UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error - UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error - UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error - UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error - UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error - UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error - UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error - UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error - UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) - UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error - UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error - UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) - UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) - UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) - UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) - UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) - UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) - UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) - UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) - UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) - UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error - UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error - UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error - UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error - UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error - UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error - UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error - UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error - UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error - UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error - UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error - UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error - UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) - UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - // This allows editing the properties of a workspace proxy. - UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) - UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error - UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error - UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error - UpsertAppSecurityKey(ctx context.Context, value string) error - UpsertApplicationName(ctx context.Context, value string) error - // The default proxy is implied and not actually stored in the database. - // So we need to store it's configuration here for display purposes. - // The functional values are immutable and controlled implicitly. - UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error - UpsertHealthSettings(ctx context.Context, value string) error - UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error - UpsertLastUpdateCheck(ctx context.Context, value string) error - UpsertLogoURL(ctx context.Context, value string) error - UpsertOAuthSigningKey(ctx context.Context, value string) error - UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) - UpsertServiceBanner(ctx context.Context, value string) error - UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) - UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) - UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error - UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) - UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) - UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) - UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) -} - -var _ sqlcQuerier = (*sqlQuerier)(nil) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6ddf88be6ee24..cd89e18580257 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11771,12 +11771,17 @@ func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Conte } const getWorkspaces = `-- name: GetWorkspaces :many +WITH filtered_workspaces AS ( SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, COALESCE(template.name, 'unknown') as template_name, latest_build.template_version_id, latest_build.template_version_name, - COUNT(*) OVER () as count + users.username as username, + latest_build.completed_at as latest_build_completed_at, + latest_build.canceled_at as latest_build_canceled_at, + latest_build.error as latest_build_error, + latest_build.transition as latest_build_transition FROM workspaces JOIN @@ -11960,22 +11965,42 @@ WHERE END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter -ORDER BY - -- To ensure that 'favorite' workspaces show up first in the list only for their owner. - CASE WHEN workspaces.owner_id = $14 AND workspaces.favorite THEN 0 ELSE 1 END ASC, - (latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'start'::workspace_transition) DESC, - LOWER(users.username) ASC, - LOWER(workspaces.name) ASC -LIMIT - CASE - WHEN $16 :: integer > 0 THEN - $16 - END -OFFSET - $15 +), filtered_workspaces_order AS ( + SELECT + fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.template_name, fw.template_version_id, fw.template_version_name, fw.username, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition + FROM + filtered_workspaces fw + ORDER BY + -- To ensure that 'favorite' workspaces show up first in the list only for their owner. + CASE WHEN owner_id = $14 AND favorite THEN 0 ELSE 1 END ASC, + (latest_build_completed_at IS NOT NULL AND + latest_build_canceled_at IS NULL AND + latest_build_error IS NULL AND + latest_build_transition = 'start'::workspace_transition) DESC, + LOWER(username) ASC, + LOWER(name) ASC + LIMIT + CASE + WHEN $16 :: integer > 0 THEN + $16 + END + OFFSET + $15 +), total_count AS ( + SELECT + count(*) AS count + FROM + filtered_workspaces +) +SELECT + tc.count, + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.template_name, fwo.template_version_id, fwo.template_version_name, fwo.username, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition +FROM + total_count tc +LEFT JOIN + filtered_workspaces_order fwo +ON + true ` type GetWorkspacesParams struct { @@ -11998,25 +12023,30 @@ type GetWorkspacesParams struct { } type GetWorkspacesRow struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - Deleted bool `db:"deleted" json:"deleted"` - Name string `db:"name" json:"name"` - AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` - Ttl sql.NullInt64 `db:"ttl" json:"ttl"` - LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` - DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` - DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` - AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` - Favorite bool `db:"favorite" json:"favorite"` - TemplateName string `db:"template_name" json:"template_name"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` - Count int64 `db:"count" json:"count"` + Count int64 `db:"count" json:"count"` + ID uuid.NullUUID `db:"id" json:"id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` + TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` + Name sql.NullString `db:"name" json:"name"` + AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` + LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` + DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` + AutomaticUpdates NullAutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` + Favorite sql.NullBool `db:"favorite" json:"favorite"` + TemplateName sql.NullString `db:"template_name" json:"template_name"` + TemplateVersionID uuid.NullUUID `db:"template_version_id" json:"template_version_id"` + TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` + Username sql.NullString `db:"username" json:"username"` + LatestBuildCompletedAt sql.NullTime `db:"latest_build_completed_at" json:"latest_build_completed_at"` + LatestBuildCanceledAt sql.NullTime `db:"latest_build_canceled_at" json:"latest_build_canceled_at"` + LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"` + LatestBuildTransition NullWorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` } func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { @@ -12046,6 +12076,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) for rows.Next() { var i GetWorkspacesRow if err := rows.Scan( + &i.Count, &i.ID, &i.CreatedAt, &i.UpdatedAt, @@ -12064,7 +12095,11 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.TemplateName, &i.TemplateVersionID, &i.TemplateVersionName, - &i.Count, + &i.Username, + &i.LatestBuildCompletedAt, + &i.LatestBuildCanceledAt, + &i.LatestBuildError, + &i.LatestBuildTransition, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 592aefb1acce9..78c22a07de4d3 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -77,12 +77,17 @@ WHERE ); -- name: GetWorkspaces :many +WITH filtered_workspaces AS ( SELECT workspaces.*, COALESCE(template.name, 'unknown') as template_name, latest_build.template_version_id, latest_build.template_version_name, - COUNT(*) OVER () as count + users.username as username, + latest_build.completed_at as latest_build_completed_at, + latest_build.canceled_at as latest_build_canceled_at, + latest_build.error as latest_build_error, + latest_build.transition as latest_build_transition FROM workspaces JOIN @@ -266,22 +271,42 @@ WHERE END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter -ORDER BY - -- To ensure that 'favorite' workspaces show up first in the list only for their owner. - CASE WHEN workspaces.owner_id = @requester_id AND workspaces.favorite THEN 0 ELSE 1 END ASC, - (latest_build.completed_at IS NOT NULL AND - latest_build.canceled_at IS NULL AND - latest_build.error IS NULL AND - latest_build.transition = 'start'::workspace_transition) DESC, - LOWER(users.username) ASC, - LOWER(workspaces.name) ASC -LIMIT - CASE - WHEN @limit_ :: integer > 0 THEN - @limit_ - END -OFFSET - @offset_ +), filtered_workspaces_order AS ( + SELECT + fw.* + FROM + filtered_workspaces fw + ORDER BY + -- To ensure that 'favorite' workspaces show up first in the list only for their owner. + CASE WHEN owner_id = @requester_id AND favorite THEN 0 ELSE 1 END ASC, + (latest_build_completed_at IS NOT NULL AND + latest_build_canceled_at IS NULL AND + latest_build_error IS NULL AND + latest_build_transition = 'start'::workspace_transition) DESC, + LOWER(username) ASC, + LOWER(name) ASC + LIMIT + CASE + WHEN @limit_ :: integer > 0 THEN + @limit_ + END + OFFSET + @offset_ +), total_count AS ( + SELECT + count(*) AS count + FROM + filtered_workspaces +) +SELECT + tc.count, + fwo.* +FROM + total_count tc +LEFT JOIN + filtered_workspaces_order fwo +ON + true ; -- name: GetWorkspaceByOwnerIDAndName :one From e018e46706acc96a02b708da28d2ae241a5ceac3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 11:34:46 +0100 Subject: [PATCH 2/8] SQL --- coderd/database/dbmem/dbmem.go | 5 + coderd/database/modelqueries.go | 6 +- coderd/database/querier.go | 420 +++++++++++++++++++++++++ coderd/database/queries.sql.go | 93 ++++-- coderd/database/queries/workspaces.sql | 46 ++- coderd/workspaces.go | 11 +- 6 files changed, 538 insertions(+), 43 deletions(-) create mode 100644 coderd/database/querier.go diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3e6b0e1d15ab4..f76c7411482a8 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -389,6 +389,11 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac rows = append(rows, wr) } + + // Append a technical row with summary + rows = append(rows, database.GetWorkspacesRow{ + Count: count, + }) return rows } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 4f0c31f62d829..7094038b9f642 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -215,9 +215,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa // The name comment is for metric tracking query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filtered) rows, err := q.db.QueryContext(ctx, query, - arg.RequesterID, - arg.Offset, - arg.Limit, arg.Deleted, arg.Status, arg.OwnerID, @@ -231,6 +228,9 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa arg.LastUsedBefore, arg.LastUsedAfter, arg.UsingActive, + arg.RequesterID, + arg.Offset, + arg.Limit, ) if err != nil { return nil, err diff --git a/coderd/database/querier.go b/coderd/database/querier.go new file mode 100644 index 0000000000000..92ee81f85fd91 --- /dev/null +++ b/coderd/database/querier.go @@ -0,0 +1,420 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +type sqlcQuerier interface { + // Blocks until the lock is acquired. + // + // This must be called from within a transaction. The lock will be automatically + // released when the transaction ends. + AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error + // Acquires the lock for a single job that isn't started, completed, + // canceled, and that matches an array of provisioner types. + // + // SKIP LOCKED is used to jump over locked rows. This prevents + // multiple provisioners from acquiring the same jobs. See: + // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE + AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) + // Bumps the workspace deadline by the template's configured "activity_bump" + // duration (default 1h). If the workspace bump will cross an autostart + // threshold, then the bump is autostart + TTL. This is the deadline behavior if + // the workspace was to autostart from a stopped state. + // + // Max deadline is respected, and the deadline will never be bumped past it. + // The deadline will never decrease. + // We only bump if the template has an activity bump duration set. + // We only bump if the raw interval is positive and non-zero. + // We only bump if workspace shutdown is manual. + // We only bump when 5% of the deadline has elapsed. + ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error + // AllUserIDs returns all UserIDs regardless of user status or deletion. + AllUserIDs(ctx context.Context) ([]uuid.UUID, error) + // Archiving templates is a soft delete action, so is reversible. + // Archiving prevents the version from being used and discovered + // by listing. + // Only unused template versions will be archived, which are any versions not + // referenced by the latest build of a workspace. + ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) + BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error + CleanTailnetCoordinators(ctx context.Context) error + CleanTailnetLostPeers(ctx context.Context) error + CleanTailnetTunnels(ctx context.Context) error + DeleteAPIKeyByID(ctx context.Context, id string) error + DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error + DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error + DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error + DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error + DeleteCoordinator(ctx context.Context, id uuid.UUID) error + DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error + DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error + DeleteGroupByID(ctx context.Context, id uuid.UUID) error + DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error + DeleteLicense(ctx context.Context, id int32) (int32, error) + DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error + DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error + DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error + // Delete provisioner daemons that have been created at least a week ago + // and have not connected to coderd since a week. + // A provisioner daemon with "zeroed" last_seen_at column indicates possible + // connectivity issues (no provisioner daemon activity since registration). + DeleteOldProvisionerDaemons(ctx context.Context) error + // If an agent hasn't connected in the last 7 days, we purge it's logs. + // Logs can take up a lot of space, so it's important we clean up frequently. + DeleteOldWorkspaceAgentLogs(ctx context.Context) error + DeleteOldWorkspaceAgentStats(ctx context.Context) error + DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error + DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) + DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) + DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error + DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) + DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) + DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error + FavoriteWorkspace(ctx context.Context, id uuid.UUID) error + GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) + // there is no unique constraint on empty token names + GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) + GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) + GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) + GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) + GetActiveUserCount(ctx context.Context) (int64, error) + GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) + GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) + // For PG Coordinator HTMLDebug + GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) + GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) + GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) + GetAppSecurityKey(ctx context.Context) (string, error) + GetApplicationName(ctx context.Context) (string, error) + // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided + // ID. + GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) + // This function returns roles for authorization purposes. Implied member roles + // are included. + GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) + GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) + GetDERPMeshKey(ctx context.Context) (string, error) + GetDefaultOrganization(ctx context.Context) (Organization, error) + GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) + GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) + GetDeploymentID(ctx context.Context) (string, error) + GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) + GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) + GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) + GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) + GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) + GetFileByID(ctx context.Context, id uuid.UUID) (File, error) + // Get all templates that use a file. + GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) + GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) + GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) + GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) + // If the group is a user made group, then we need to check the group_members table. + // If it is the "Everyone" group, then we need to check the organization_members table. + GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error) + GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) + GetHealthSettings(ctx context.Context) (string, error) + GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error) + GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) + GetLastUpdateCheck(ctx context.Context) (string, error) + GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) + GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) + GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) + GetLicenseByID(ctx context.Context, id int32) (License, error) + GetLicenses(ctx context.Context) ([]License, error) + GetLogoURL(ctx context.Context) (string, error) + GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) + GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) + GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) + GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) + GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) + GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) + GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) + GetOAuthSigningKey(ctx context.Context) (string, error) + GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) + GetOrganizationByName(ctx context.Context, name string) (Organization, error) + GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) + GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error) + GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]OrganizationMember, error) + GetOrganizations(ctx context.Context) ([]Organization, error) + GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error) + GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) + GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) + GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) + GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) + GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) + GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) + GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) + GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) + GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) + GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) + GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) + GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) + GetServiceBanner(ctx context.Context) (string, error) + GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) + GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) + GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) + GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) + GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) + // GetTemplateAppInsights returns the aggregate usage of each app in a given + // timeframe. The result can be filtered on template_ids, meaning only user data + // from workspaces based on those templates will be included. + GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) + GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) + GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) + GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) + GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) + GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) + // GetTemplateInsights has a granularity of 5 minutes where if a session/app was + // in use during a minute, we will add 5 minutes to the total usage for that + // session/app (per user). + GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) + // GetTemplateInsightsByInterval returns all intervals between start and end + // time, if end time is a partial interval, it will be included in the results and + // that interval will be shorter than a full one. If there is no data for a selected + // interval/template, it will be included in the results with 0 active users. + GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) + GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) + // GetTemplateParameterInsights does for each template in a given timeframe, + // look for the latest workspace build (for every workspace) that has been + // created in the timeframe and return the aggregate usage counts of parameter + // values. + GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) + GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) + GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) + GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) + GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) + GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) + GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) + GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) + GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) + GetTemplates(ctx context.Context) ([]Template, error) + GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) + GetUnexpiredLicenses(ctx context.Context) ([]License, error) + // GetUserActivityInsights returns the ranking with top active users. + // The result can be filtered on template_ids, meaning only user data from workspaces + // based on those templates will be included. + // Note: When selecting data from multiple templates or the entire deployment, + // be aware that it may lead to an increase in "usage" numbers (cumulative). In such cases, + // users may be counted multiple times for the same time interval if they have used multiple templates + // simultaneously. + GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) + GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) + GetUserByID(ctx context.Context, id uuid.UUID) (User, error) + GetUserCount(ctx context.Context) (int64, error) + // GetUserLatencyInsights returns the median and 95th percentile connection + // latency that users have experienced. The result can be filtered on + // template_ids, meaning only user data from workspaces based on those templates + // will be included. + GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) + GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) + GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) + GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) + GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) + // This will never return deleted users. + GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) + // This shouldn't check for deleted, because it's frequently used + // to look up references to actions. eg. a user could build a workspace + // for another user, then be deleted... we still want them to appear! + GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) + GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndOwnerByAuthTokenRow, error) + GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) + GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) + GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) + GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) + GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) + GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) + GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) + GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) + GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) + GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) + GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) + GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) + GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) + GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) + GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) + GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) + GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) + GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) + GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (GetWorkspaceByAgentIDRow, error) + GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) + GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) + GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) + GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) + // Finds a workspace proxy that has an access URL or app hostname that matches + // the provided hostname. This is to check if a hostname matches any workspace + // proxy. + // + // The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling + // this query. The scheme, port and path should be stripped. + // + GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) + GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) + GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) + GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) + GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) + GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) + GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) + GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) + GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) + GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) + GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) + GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) + InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) + // We use the organization_id as the id + // for simplicity since all users is + // every member of the org. + InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) + InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) + InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error + InsertDERPMeshKey(ctx context.Context, value string) error + InsertDeploymentID(ctx context.Context, value string) error + InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) + InsertFile(ctx context.Context, arg InsertFileParams) (File, error) + InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) + InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) + InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error + InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) + // Inserts any group by name that does not exist. All new groups are given + // a random uuid, are inserted into the same organization. They have the default + // values for avatar, display name, and quota allowance (all zero values). + // If the name conflicts, do nothing. + InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) + InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) + InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) + InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) + InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) + InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) + InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) + InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) + InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) + InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) + InsertTemplate(ctx context.Context, arg InsertTemplateParams) error + InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error + InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) + InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) + InsertUser(ctx context.Context, arg InsertUserParams) (User, error) + // InsertUserGroupsByName adds a user to all provided groups, if they exist. + InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error + InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) + InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) + InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) + InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) + InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error + InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) + InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) + InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error + InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) + InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error + InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error + InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) + InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) + InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) + ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) + RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) + RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error + RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error + // Non blocking lock. Returns true if the lock was acquired, false otherwise. + // + // This must be called from within a transaction. The lock will be automatically + // released when the transaction ends. + TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) + // This will always work regardless of the current state of the template version. + UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error + UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error + UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error + UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) + UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) + UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) + UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) + UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) + UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) + UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) + UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error + UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error + UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error + UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error + UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) + UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error + UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error + UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error + UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error + UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error + UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error + UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error + UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error + UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error + UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error + UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) + UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error + UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error + UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) + UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) + UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) + UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) + UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) + UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) + UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) + UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) + UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error + UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error + UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error + UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error + UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error + UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error + UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error + UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error + UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error + UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error + UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error + UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error + UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) + UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error + // This allows editing the properties of a workspace proxy. + UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) + UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error + UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error + UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error + UpsertAppSecurityKey(ctx context.Context, value string) error + UpsertApplicationName(ctx context.Context, value string) error + // The default proxy is implied and not actually stored in the database. + // So we need to store it's configuration here for display purposes. + // The functional values are immutable and controlled implicitly. + UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error + UpsertHealthSettings(ctx context.Context, value string) error + UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error + UpsertLastUpdateCheck(ctx context.Context, value string) error + UpsertLogoURL(ctx context.Context, value string) error + UpsertOAuthSigningKey(ctx context.Context, value string) error + UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) + UpsertServiceBanner(ctx context.Context, value string) error + UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) + UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) + UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error + UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) + UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) + UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) + UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) +} + +var _ sqlcQuerier = (*sqlQuerier)(nil) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index cd89e18580257..1e6d2441cd35e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11986,6 +11986,39 @@ WHERE END OFFSET $15 +), filtered_workspaces_order_with_summary AS ( + SELECT + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.template_name, fwo.template_version_id, fwo.template_version_name, fwo.username, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition + FROM + filtered_workspaces_order fwo + -- Always return a technical summary row with total count of workspaces. + -- It is used to present the correct count if pagination goes beyond the offset. + UNION ALL + SELECT + '00000000-0000-0000-0000-000000000000'::uuid, -- id + '0001-01-01 00:00:00+00'::timestamp, -- created_at + '0001-01-01 00:00:00+00'::timestamp, -- updated_at + '00000000-0000-0000-0000-000000000000'::uuid, -- owner_id + '00000000-0000-0000-0000-000000000000'::uuid, -- organization_id + '00000000-0000-0000-0000-000000000000'::uuid, -- template_id + false, -- deleted + '**TECHNICAL_ROW**', -- name + '', -- autostart_schedule + 0, -- ttl + '0001-01-01 00:00:00+00'::timestamp, -- last_used_at + '0001-01-01 00:00:00+00'::timestamp, -- dormant_at + '0001-01-01 00:00:00+00'::timestamp, -- deleting_at + 'never'::automatic_updates, -- automatic_updates + false, -- favorite + -- Extra columns added to ` + "`" + `filtered_workspaces` + "`" + ` + '', -- template_name + '00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id + '', -- template_version_name + '', -- username + '0001-01-01 00:00:00+00'::timestamp, -- latest_build_completed_at, + '0001-01-01 00:00:00+00'::timestamp, -- latest_build_canceled_at, + '', -- latest_build_error + 'start'::workspace_transition -- latest_build_transition ), total_count AS ( SELECT count(*) AS count @@ -11993,14 +12026,12 @@ WHERE filtered_workspaces ) SELECT - tc.count, - fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.template_name, fwo.template_version_id, fwo.template_version_name, fwo.username, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition + fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.template_name, fwos.template_version_id, fwos.template_version_name, fwos.username, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, + tc.count FROM + filtered_workspaces_order_with_summary fwos +CROSS JOIN total_count tc -LEFT JOIN - filtered_workspaces_order fwo -ON - true ` type GetWorkspacesParams struct { @@ -12023,30 +12054,30 @@ type GetWorkspacesParams struct { } type GetWorkspacesRow struct { - Count int64 `db:"count" json:"count"` - ID uuid.NullUUID `db:"id" json:"id"` - CreatedAt sql.NullTime `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` - OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` - TemplateID uuid.NullUUID `db:"template_id" json:"template_id"` - Deleted sql.NullBool `db:"deleted" json:"deleted"` - Name sql.NullString `db:"name" json:"name"` - AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` - Ttl sql.NullInt64 `db:"ttl" json:"ttl"` - LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"` - DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` - DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` - AutomaticUpdates NullAutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` - Favorite sql.NullBool `db:"favorite" json:"favorite"` - TemplateName sql.NullString `db:"template_name" json:"template_name"` - TemplateVersionID uuid.NullUUID `db:"template_version_id" json:"template_version_id"` - TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` - Username sql.NullString `db:"username" json:"username"` - LatestBuildCompletedAt sql.NullTime `db:"latest_build_completed_at" json:"latest_build_completed_at"` - LatestBuildCanceledAt sql.NullTime `db:"latest_build_canceled_at" json:"latest_build_canceled_at"` - LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"` - LatestBuildTransition NullWorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + Deleted bool `db:"deleted" json:"deleted"` + Name string `db:"name" json:"name"` + AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` + DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` + AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` + Favorite bool `db:"favorite" json:"favorite"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` + Username string `db:"username" json:"username"` + LatestBuildCompletedAt sql.NullTime `db:"latest_build_completed_at" json:"latest_build_completed_at"` + LatestBuildCanceledAt sql.NullTime `db:"latest_build_canceled_at" json:"latest_build_canceled_at"` + LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"` + LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` + Count int64 `db:"count" json:"count"` } func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { @@ -12076,7 +12107,6 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) for rows.Next() { var i GetWorkspacesRow if err := rows.Scan( - &i.Count, &i.ID, &i.CreatedAt, &i.UpdatedAt, @@ -12100,6 +12130,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.LatestBuildCanceledAt, &i.LatestBuildError, &i.LatestBuildTransition, + &i.Count, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 78c22a07de4d3..787dc59ae1a56 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -292,6 +292,39 @@ WHERE END OFFSET @offset_ +), filtered_workspaces_order_with_summary AS ( + SELECT + fwo.* + FROM + filtered_workspaces_order fwo + -- Always return a technical summary row with total count of workspaces. + -- It is used to present the correct count if pagination goes beyond the offset. + UNION ALL + SELECT + '00000000-0000-0000-0000-000000000000'::uuid, -- id + '0001-01-01 00:00:00+00'::timestamp, -- created_at + '0001-01-01 00:00:00+00'::timestamp, -- updated_at + '00000000-0000-0000-0000-000000000000'::uuid, -- owner_id + '00000000-0000-0000-0000-000000000000'::uuid, -- organization_id + '00000000-0000-0000-0000-000000000000'::uuid, -- template_id + false, -- deleted + '**TECHNICAL_ROW**', -- name + '', -- autostart_schedule + 0, -- ttl + '0001-01-01 00:00:00+00'::timestamp, -- last_used_at + '0001-01-01 00:00:00+00'::timestamp, -- dormant_at + '0001-01-01 00:00:00+00'::timestamp, -- deleting_at + 'never'::automatic_updates, -- automatic_updates + false, -- favorite + -- Extra columns added to `filtered_workspaces` + '', -- template_name + '00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id + '', -- template_version_name + '', -- username + '0001-01-01 00:00:00+00'::timestamp, -- latest_build_completed_at, + '0001-01-01 00:00:00+00'::timestamp, -- latest_build_canceled_at, + '', -- latest_build_error + 'start'::workspace_transition -- latest_build_transition ), total_count AS ( SELECT count(*) AS count @@ -299,15 +332,12 @@ WHERE filtered_workspaces ) SELECT - tc.count, - fwo.* + fwos.*, + tc.count FROM - total_count tc -LEFT JOIN - filtered_workspaces_order fwo -ON - true -; + filtered_workspaces_order_with_summary fwos +CROSS JOIN + total_count tc; -- name: GetWorkspaceByOwnerIDAndName :one SELECT diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0ab1e5ee41660..baf337609d2f5 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -182,12 +182,21 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { return } if len(workspaceRows) == 0 { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspaces.", + Detail: "Workspace summary row is missing.", + }) + return + } + if len(workspaceRows) == 1 { httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{ Workspaces: []codersdk.Workspace{}, - Count: 0, + Count: int(workspaceRows[0].Count), }) return } + // Skip technical summary row + workspaceRows = workspaceRows[:len(workspaceRows)-1] workspaces := database.ConvertWorkspaceRows(workspaceRows) From 2faf060e7dbbfba7255f4763394df511d95daf28 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 11:46:11 +0100 Subject: [PATCH 3/8] bad merge --- cli/user_delete_test.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/cli/user_delete_test.go b/cli/user_delete_test.go index 9ee546ca7a925..58b02bb506a1f 100644 --- a/cli/user_delete_test.go +++ b/cli/user_delete_test.go @@ -78,37 +78,6 @@ func TestUserDelete(t *testing.T) { pty.ExpectMatch("coolin") }) - t.Run("UserID", func(t *testing.T) { - t.Parallel() - ctx := context.Background() - client := coderdtest.New(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin()) - - pw, err := cryptorand.String(16) - require.NoError(t, err) - - user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "colin5@coder.com", - Username: "coolin", - Password: pw, - UserLoginType: codersdk.LoginTypePassword, - OrganizationID: owner.OrganizationID, - DisableLogin: false, - }) - require.NoError(t, err) - - inv, root := clitest.New(t, "users", "delete", user.ID.String()) - clitest.SetupConfig(t, userAdmin, root) - pty := ptytest.New(t).Attach(inv) - errC := make(chan error) - go func() { - errC <- inv.Run() - }() - require.NoError(t, <-errC) - pty.ExpectMatch("coolin") - }) - // TODO: reenable this test case. Fetching users without perms returns a // "user "testuser@coder.com" must be a member of at least one organization" // error. From dcff0c03d6d9294adbe08c833eb7ca31265cd6cd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 12:34:24 +0100 Subject: [PATCH 4/8] WIP: update code references --- coderd/database/dbauthz/dbauthz.go | 4 ++++ coderd/database/dbmem/dbmem.go | 13 +++++++++++++ coderd/database/dbmetrics/dbmetrics.go | 7 +++++++ coderd/database/modelqueries.go | 9 +++++++++ coderd/prometheusmetrics/prometheusmetrics.go | 2 +- coderd/telemetry/telemetry.go | 2 +- coderd/templates.go | 2 +- coderd/users.go | 2 +- 8 files changed, 37 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 135703bb0ba71..866bc38228261 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3438,3 +3438,7 @@ func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersP // GetUsers is authenticated. return q.GetUsers(ctx, arg) } + +func (q *querier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + return q.db.GetWorkspacesWithoutSummary(ctx, arg) +} diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index f76c7411482a8..5d9f86285ed20 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -393,6 +393,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac // Append a technical row with summary rows = append(rows, database.GetWorkspacesRow{ Count: count, + Name: "*TECHNICAL_ROW*", }) return rows } @@ -5089,6 +5090,18 @@ func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspa return workspaceRows, err } +func (q *FakeQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err + } + + workspaceRows, err := q.GetWorkspaces(ctx, arg) + if err != nil { + return nil, err + } + return workspaceRows[:len(workspaceRows)-1], err +} + func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index efbf01848020e..78b61f0612361 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -2261,3 +2261,10 @@ func (m metricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUs m.queryLatencies.WithLabelValues("GetAuthorizedUsers").Observe(time.Since(start).Seconds()) return r0, r1 } + +func (m metricsStore) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + start := time.Now() + workspaces, err := m.s.GetWorkspacesWithoutSummary(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspacesWithoutSummary").Observe(time.Since(start).Seconds()) + return workspaces, err +} diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 7094038b9f642..69900b1f7b4e8 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -194,6 +194,7 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([ type workspaceQuerier interface { GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) + GetWorkspacesWithoutSummary(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) } // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. @@ -278,6 +279,14 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa return items, nil } +func (q *sqlQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { + rows, err := q.GetWorkspaces(ctx, arg) + if err != nil { + return nil, err + } + return rows[:len(rows)-1], nil +} + type userQuerier interface { GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, prepared rbac.PreparedAuthorized) ([]GetUsersRow, error) } diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index e1928fec5fa15..11b9cb91a0114 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -229,7 +229,7 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis timer := prometheus.NewTimer(metricsCollectorAgents) derpMap := derpMapFn() - workspaceRows, err := db.GetWorkspaces(ctx, database.GetWorkspacesParams{ + workspaceRows, err := db.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{ AgentInactiveDisconnectTimeoutSeconds: int64(agentInactiveDisconnectTimeout.Seconds()), }) if err != nil { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index a70c0c7d2658e..696ea335addbb 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -375,7 +375,7 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { return nil }) eg.Go(func() error { - workspaceRows, err := r.options.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{}) + workspaceRows, err := r.options.Database.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{}) if err != nil { return xerrors.Errorf("get workspaces: %w", err) } diff --git a/coderd/templates.go b/coderd/templates.go index d7c578bd639e5..fb8c4fa235fa6 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -70,7 +70,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) { // This is just to get the workspace count, so we use a system context to // return ALL workspaces. Not just workspaces the user can view. // nolint:gocritic - workspaces, err := api.Database.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{ + workspaces, err := api.Database.GetWorkspacesWithoutSummary(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{ TemplateIDs: []uuid.UUID{template.ID}, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/coderd/users.go b/coderd/users.go index cbc9a75059210..7496d9e5d69c2 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -517,7 +517,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) { return } - workspaces, err := api.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{ + workspaces, err := api.Database.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{ OwnerID: user.ID, }) if err != nil { From 752ba7e25240a1c0f9905053d580f6d381fec9b2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 14:47:56 +0100 Subject: [PATCH 5/8] dbmock --- coderd/database/dbmock/dbmock.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 5bcca0b1a22b4..ce2a042d97843 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2881,6 +2881,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaces", reflect.TypeOf((*MockStore)(nil).GetWorkspaces), arg0, arg1) } +// GetWorkspaces mocks base method. +func (m *MockStore) GetWorkspacesWithoutSummary(arg0 context.Context, arg1 database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspacesWithoutSummary", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspacesRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaces indicates an expected call of GetWorkspaces. +func (mr *MockStoreMockRecorder) GetWorkspacesWithoutSummary(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesWithoutSummary", reflect.TypeOf((*MockStore)(nil).GetWorkspacesWithoutSummary), arg0, arg1) +} + // GetWorkspacesEligibleForTransition mocks base method. func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.Workspace, error) { m.ctrl.T.Helper() From b989773dee89822a2ee4d9a1380dd43c433f6d1c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 14:54:14 +0100 Subject: [PATCH 6/8] dbmock --- coderd/database/dbauthz/dbauthz.go | 8 ++++---- coderd/database/dbmem/dbmem.go | 24 +++++++++++----------- coderd/database/dbmetrics/dbmetrics.go | 14 ++++++------- coderd/database/dbmock/dbmock.go | 28 +++++++++++++------------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 866bc38228261..184618b827c5e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3432,13 +3432,13 @@ func (q *querier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetW return q.GetWorkspaces(ctx, arg) } +func (q *querier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + return q.db.GetWorkspacesWithoutSummary(ctx, arg) +} + // GetAuthorizedUsers is not required for dbauthz since GetUsers is already // authenticated. func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { // GetUsers is authenticated. return q.GetUsers(ctx, arg) } - -func (q *querier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - return q.db.GetWorkspacesWithoutSummary(ctx, arg) -} diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5d9f86285ed20..18a373ae259ec 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5090,18 +5090,6 @@ func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspa return workspaceRows, err } -func (q *FakeQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err - } - - workspaceRows, err := q.GetWorkspaces(ctx, arg) - if err != nil { - return nil, err - } - return workspaceRows[:len(workspaceRows)-1], err -} - func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -8304,6 +8292,18 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil } +func (q *FakeQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err + } + + workspaceRows, err := q.GetWorkspaces(ctx, arg) + if err != nil { + return nil, err + } + return workspaceRows[:len(workspaceRows)-1], err +} + func (q *FakeQuerier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { if err := validateDatabaseType(arg); err != nil { return nil, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 78b61f0612361..2532916cc622d 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -2255,16 +2255,16 @@ func (m metricsStore) GetAuthorizedWorkspaces(ctx context.Context, arg database. return workspaces, err } -func (m metricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { - start := time.Now() - r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared) - m.queryLatencies.WithLabelValues("GetAuthorizedUsers").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m metricsStore) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { start := time.Now() workspaces, err := m.s.GetWorkspacesWithoutSummary(ctx, arg) m.queryLatencies.WithLabelValues("GetWorkspacesWithoutSummary").Observe(time.Since(start).Seconds()) return workspaces, err } + +func (m metricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { + start := time.Now() + r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared) + m.queryLatencies.WithLabelValues("GetAuthorizedUsers").Observe(time.Since(start).Seconds()) + return r0, r1 +} diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index ce2a042d97843..f6380917f8830 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2881,34 +2881,34 @@ func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaces", reflect.TypeOf((*MockStore)(nil).GetWorkspaces), arg0, arg1) } -// GetWorkspaces mocks base method. -func (m *MockStore) GetWorkspacesWithoutSummary(arg0 context.Context, arg1 database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { +// GetWorkspacesEligibleForTransition mocks base method. +func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.Workspace, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspacesWithoutSummary", arg0, arg1) - ret0, _ := ret[0].([]database.GetWorkspacesRow) + ret := m.ctrl.Call(m, "GetWorkspacesEligibleForTransition", arg0, arg1) + ret0, _ := ret[0].([]database.Workspace) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaces indicates an expected call of GetWorkspaces. -func (mr *MockStoreMockRecorder) GetWorkspacesWithoutSummary(arg0, arg1 any) *gomock.Call { +// GetWorkspacesEligibleForTransition indicates an expected call of GetWorkspacesEligibleForTransition. +func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesWithoutSummary", reflect.TypeOf((*MockStore)(nil).GetWorkspacesWithoutSummary), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), arg0, arg1) } -// GetWorkspacesEligibleForTransition mocks base method. -func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.Workspace, error) { +// GetWorkspacesWithoutSummary mocks base method. +func (m *MockStore) GetWorkspacesWithoutSummary(arg0 context.Context, arg1 database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspacesEligibleForTransition", arg0, arg1) - ret0, _ := ret[0].([]database.Workspace) + ret := m.ctrl.Call(m, "GetWorkspacesWithoutSummary", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspacesRow) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspacesEligibleForTransition indicates an expected call of GetWorkspacesEligibleForTransition. -func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 any) *gomock.Call { +// GetWorkspacesWithoutSummary indicates an expected call of GetWorkspacesWithoutSummary. +func (mr *MockStoreMockRecorder) GetWorkspacesWithoutSummary(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesWithoutSummary", reflect.TypeOf((*MockStore)(nil).GetWorkspacesWithoutSummary), arg0, arg1) } // InTx mocks base method. From df4e12734bcbd92d1de616c4ed0fd1d796ee0cee Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 16:12:42 +0100 Subject: [PATCH 7/8] The other way round --- coderd/database/dbauthz/dbauthz.go | 20 +- coderd/database/dbmem/dbmem.go | 535 +++++++++--------- coderd/database/dbmetrics/dbmetrics.go | 20 +- coderd/database/dbmock/dbmock.go | 14 +- coderd/database/gentest/modelqueries_test.go | 6 +- coderd/database/modelqueries.go | 20 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 224 ++++---- coderd/database/queries/workspaces.sql | 2 +- coderd/prometheusmetrics/prometheusmetrics.go | 2 +- coderd/telemetry/telemetry.go | 2 +- coderd/templates.go | 2 +- coderd/users.go | 2 +- 13 files changed, 436 insertions(+), 415 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 184618b827c5e..055a8a423bc78 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2214,18 +2214,14 @@ func (q *querier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, return q.db.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIds) } -func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceWorkspace.Type) - if err != nil { - return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) - } - return q.db.GetAuthorizedWorkspaces(ctx, arg, prep) -} - func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) { return q.db.GetWorkspacesEligibleForTransition(ctx, now) } +func (q *querier) GetWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams) ([]database.GetWorkspacesWithSummaryRow, error) { + return q.db.GetWorkspacesWithSummary(ctx, arg) +} + func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { return insert(q.log, q.auth, rbac.ResourceAPIKey.WithOwner(arg.UserID.String()), @@ -3432,8 +3428,12 @@ func (q *querier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetW return q.GetWorkspaces(ctx, arg) } -func (q *querier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - return q.db.GetWorkspacesWithoutSummary(ctx, arg) +func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { + prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceWorkspace.Type) + if err != nil { + return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err) + } + return q.db.GetAuthorizedWorkspaces(ctx, arg, prep) } // GetAuthorizedUsers is not required for dbauthz since GetUsers is already diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 18a373ae259ec..eccbb79819515 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -345,10 +345,10 @@ func mapAgentStatus(dbAgent database.WorkspaceAgent, agentInactiveDisconnectTime return status } -func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspaces []database.Workspace, count int64) []database.GetWorkspacesRow { - rows := make([]database.GetWorkspacesRow, 0, len(workspaces)) +func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspaces []database.Workspace, count int64) []database.GetWorkspacesWithSummaryRow { + rows := make([]database.GetWorkspacesWithSummaryRow, 0, len(workspaces)) for _, w := range workspaces { - wr := database.GetWorkspacesRow{ + wr := database.GetWorkspacesWithSummaryRow{ ID: w.ID, CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, @@ -391,7 +391,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac } // Append a technical row with summary - rows = append(rows, database.GetWorkspacesRow{ + rows = append(rows, database.GetWorkspacesWithSummaryRow{ Count: count, Name: "*TECHNICAL_ROW*", }) @@ -5080,16 +5080,6 @@ func (q *FakeQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(_ context.Contex return resp, nil } -func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err - } - - // A nil auth filter means no auth filter. - workspaceRows, err := q.GetAuthorizedWorkspaces(ctx, arg, nil) - return workspaceRows, err -} - func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -5142,6 +5132,264 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no return workspaces, nil } +func (q *FakeQuerier) GetWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams) ([]database.GetWorkspacesWithSummaryRow, error) { + return q.getAuthorizedWorkspacesWithSummary(ctx, arg, nil) +} + +func (q *FakeQuerier) getAuthorizedWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesWithSummaryRow, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaces := make([]database.Workspace, 0) + for _, workspace := range q.workspaces { + if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { + continue + } + + if arg.OwnerUsername != "" { + owner, err := q.getUserByIDNoLock(workspace.OwnerID) + if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { + continue + } + } + + if arg.TemplateName != "" { + template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) + if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { + continue + } + } + + if arg.UsingActive.Valid { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) + } + + template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) + if err != nil { + return nil, xerrors.Errorf("get template: %w", err) + } + + updated := build.TemplateVersionID == template.ActiveVersionID + if arg.UsingActive.Bool != updated { + continue + } + } + + if !arg.Deleted && workspace.Deleted { + continue + } + + if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { + continue + } + + if !arg.LastUsedBefore.IsZero() { + if workspace.LastUsedAt.After(arg.LastUsedBefore) { + continue + } + } + + if !arg.LastUsedAfter.IsZero() { + if workspace.LastUsedAt.Before(arg.LastUsedAfter) { + continue + } + } + + 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.WorkspaceStatusStarting: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionStart + case database.WorkspaceStatusStopping: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusDeleting: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionDelete + + case "started": + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart + case database.WorkspaceStatusDeleted: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionDelete + case database.WorkspaceStatusStopped: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusRunning: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart + default: + statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status) + } + if !statusMatch { + continue + } + } + + 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 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 + } + } + + if arg.Dormant && !workspace.DormantAt.Valid { + continue + } + + if len(arg.TemplateIDs) > 0 { + match := false + for _, id := range arg.TemplateIDs { + if workspace.TemplateID == id { + match = true + break + } + } + if !match { + continue + } + } + + // If the filter exists, ensure the object is authorized. + if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { + continue + } + workspaces = append(workspaces, workspace) + } + + // 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 + } + + preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} + preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} + preloadedUsers := map[uuid.UUID]database.User{} + + 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) + } + + 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) + } + + 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) + } + } + + sort.Slice(workspaces, func(i, j int) bool { + w1 := workspaces[i] + w2 := workspaces[j] + + // Order by: favorite first + if arg.RequesterID == w1.OwnerID && w1.Favorite { + return true + } + if arg.RequesterID == w2.OwnerID && w2.Favorite { + return false + } + + // Order by: running + w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) + w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) + + if w1IsRunning && !w2IsRunning { + return true + } + + if !w1IsRunning && w2IsRunning { + return false + } + + // Order by: usernames + if strings.Compare(preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username) < 0 { + return true + } + + // Order by: workspace names + return strings.Compare(w1.Name, w2.Name) < 0 + }) + + beforePageCount := len(workspaces) + + if arg.Offset > 0 { + if int(arg.Offset) > len(workspaces) { + return []database.GetWorkspacesWithSummaryRow{}, nil + } + workspaces = workspaces[arg.Offset:] + } + if arg.Limit > 0 { + if int(arg.Limit) > len(workspaces) { + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil + } + workspaces = workspaces[:arg.Limit] + } + + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil +} + func (q *FakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { if err := validateDatabaseType(arg); err != nil { return database.APIKey{}, err @@ -8046,262 +8294,25 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } } - workspaces := make([]database.Workspace, 0) - for _, workspace := range q.workspaces { - if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { - continue - } - - if arg.OwnerUsername != "" { - owner, err := q.getUserByIDNoLock(workspace.OwnerID) - if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { - continue - } - } - - if arg.TemplateName != "" { - template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) - if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { - continue - } - } - - if arg.UsingActive.Valid { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return nil, xerrors.Errorf("get latest build: %w", err) - } - - template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) - if err != nil { - return nil, xerrors.Errorf("get template: %w", err) - } - - updated := build.TemplateVersionID == template.ActiveVersionID - if arg.UsingActive.Bool != updated { - continue - } - } - - if !arg.Deleted && workspace.Deleted { - continue - } - - if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { - continue - } - - if !arg.LastUsedBefore.IsZero() { - if workspace.LastUsedAt.After(arg.LastUsedBefore) { - continue - } - } - - if !arg.LastUsedAfter.IsZero() { - if workspace.LastUsedAt.Before(arg.LastUsedAfter) { - continue - } - } - - 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.WorkspaceStatusStarting: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionStart - case database.WorkspaceStatusStopping: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusDeleting: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionDelete - - case "started": - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStart - case database.WorkspaceStatusDeleted: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionDelete - case database.WorkspaceStatusStopped: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusRunning: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStart - default: - statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status) - } - if !statusMatch { - continue - } - } - - 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 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 - } - } - - if arg.Dormant && !workspace.DormantAt.Valid { - continue - } - - if len(arg.TemplateIDs) > 0 { - match := false - for _, id := range arg.TemplateIDs { - if workspace.TemplateID == id { - match = true - break - } - } - if !match { - continue - } - } - - // If the filter exists, ensure the object is authorized. - if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { - continue - } - workspaces = append(workspaces, workspace) - } - - // 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 - } - - preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} - preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} - preloadedUsers := map[uuid.UUID]database.User{} - - 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) - } - - 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) - } - - 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) - } - } - - sort.Slice(workspaces, func(i, j int) bool { - w1 := workspaces[i] - w2 := workspaces[j] - - // Order by: favorite first - if arg.RequesterID == w1.OwnerID && w1.Favorite { - return true - } - if arg.RequesterID == w2.OwnerID && w2.Favorite { - return false - } - - // Order by: running - w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) - w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) - - if w1IsRunning && !w2IsRunning { - return true - } - - if !w1IsRunning && w2IsRunning { - return false - } - - // Order by: usernames - if strings.Compare(preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username) < 0 { - return true - } - - // Order by: workspace names - return strings.Compare(w1.Name, w2.Name) < 0 - }) - - beforePageCount := len(workspaces) - - if arg.Offset > 0 { - if int(arg.Offset) > len(workspaces) { - return []database.GetWorkspacesRow{}, nil - } - workspaces = workspaces[arg.Offset:] + workspaceRows, err := q.getAuthorizedWorkspacesWithSummary(ctx, database.GetWorkspacesWithSummaryParams(arg), prepared) + if err != nil { + return nil, err } - if arg.Limit > 0 { - if int(arg.Limit) > len(workspaces) { - return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil - } - workspaces = workspaces[:arg.Limit] + workspaceRows = workspaceRows[:len(workspaceRows)-1] + rows := make([]database.GetWorkspacesRow, 0, len(workspaceRows)) + for _, r := range workspaceRows { + rows = append(rows, database.GetWorkspacesRow(r)) } - - return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil + return rows, nil } -func (q *FakeQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { +func (q *FakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { if err := validateDatabaseType(arg); err != nil { return nil, err } - workspaceRows, err := q.GetWorkspaces(ctx, arg) - if err != nil { - return nil, err - } - return workspaceRows[:len(workspaceRows)-1], err + // A nil auth filter means no auth filter. + return q.GetAuthorizedWorkspaces(ctx, arg, nil) } func (q *FakeQuerier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 2532916cc622d..32a87ae4d9bef 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1369,13 +1369,6 @@ func (m metricsStore) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Cont return r0, r1 } -func (m metricsStore) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { - start := time.Now() - workspaces, err := m.s.GetWorkspaces(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspaces").Observe(time.Since(start).Seconds()) - return workspaces, err -} - func (m metricsStore) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) { start := time.Now() workspaces, err := m.s.GetWorkspacesEligibleForTransition(ctx, now) @@ -1383,6 +1376,13 @@ func (m metricsStore) GetWorkspacesEligibleForTransition(ctx context.Context, no return workspaces, err } +func (m metricsStore) GetWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams) ([]database.GetWorkspacesWithSummaryRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspacesWithSummary(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspacesWithSummary").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { start := time.Now() key, err := m.s.InsertAPIKey(ctx, arg) @@ -2255,10 +2255,10 @@ func (m metricsStore) GetAuthorizedWorkspaces(ctx context.Context, arg database. return workspaces, err } -func (m metricsStore) GetWorkspacesWithoutSummary(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { +func (m metricsStore) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { start := time.Now() - workspaces, err := m.s.GetWorkspacesWithoutSummary(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspacesWithoutSummary").Observe(time.Since(start).Seconds()) + workspaces, err := m.s.GetWorkspaces(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaces").Observe(time.Since(start).Seconds()) return workspaces, err } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index f6380917f8830..d65c775eccb9d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2896,19 +2896,19 @@ func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), arg0, arg1) } -// GetWorkspacesWithoutSummary mocks base method. -func (m *MockStore) GetWorkspacesWithoutSummary(arg0 context.Context, arg1 database.GetWorkspacesParams) ([]database.GetWorkspacesRow, error) { +// GetWorkspacesWithSummary mocks base method. +func (m *MockStore) GetWorkspacesWithSummary(arg0 context.Context, arg1 database.GetWorkspacesWithSummaryParams) ([]database.GetWorkspacesWithSummaryRow, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspacesWithoutSummary", arg0, arg1) - ret0, _ := ret[0].([]database.GetWorkspacesRow) + ret := m.ctrl.Call(m, "GetWorkspacesWithSummary", arg0, arg1) + ret0, _ := ret[0].([]database.GetWorkspacesWithSummaryRow) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspacesWithoutSummary indicates an expected call of GetWorkspacesWithoutSummary. -func (mr *MockStoreMockRecorder) GetWorkspacesWithoutSummary(arg0, arg1 any) *gomock.Call { +// GetWorkspacesWithSummary indicates an expected call of GetWorkspacesWithSummary. +func (mr *MockStoreMockRecorder) GetWorkspacesWithSummary(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesWithoutSummary", reflect.TypeOf((*MockStore)(nil).GetWorkspacesWithoutSummary), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesWithSummary", reflect.TypeOf((*MockStore)(nil).GetWorkspacesWithSummary), arg0, arg1) } // InTx mocks base method. diff --git a/coderd/database/gentest/modelqueries_test.go b/coderd/database/gentest/modelqueries_test.go index 52a99b54405ec..148c6de69acb1 100644 --- a/coderd/database/gentest/modelqueries_test.go +++ b/coderd/database/gentest/modelqueries_test.go @@ -23,9 +23,9 @@ func TestCustomQueriesSyncedRowScan(t *testing.T) { t.Parallel() funcsToTrack := map[string]string{ - "GetTemplatesWithFilter": "GetAuthorizedTemplates", - "GetWorkspaces": "GetAuthorizedWorkspaces", - "GetUsers": "GetAuthorizedUsers", + "GetTemplatesWithFilter": "GetAuthorizedTemplates", + "GetWorkspacesWithSummary": "GetAuthorizedWorkspaces", + "GetUsers": "GetAuthorizedUsers", } // Scan custom diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 69900b1f7b4e8..2ec4101e847e6 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -194,9 +194,12 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([ type workspaceQuerier interface { GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) - GetWorkspacesWithoutSummary(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) + GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) } +type GetWorkspacesParams GetWorkspacesWithSummaryParams +type GetWorkspacesRow GetWorkspacesWithSummaryRow + // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. // This code is copied from `GetWorkspaces` and adds the authorized filter WHERE // clause. @@ -208,7 +211,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa // In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the // authorizedFilter between the end of the where clause and those statements. - filtered, err := insertAuthorizedFilter(getWorkspaces, fmt.Sprintf(" AND %s", authorizedFilter)) + filtered, err := insertAuthorizedFilter(getWorkspacesWithSummary, fmt.Sprintf(" AND %s", authorizedFilter)) if err != nil { return nil, xerrors.Errorf("insert authorized filter: %w", err) } @@ -276,15 +279,22 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa if err := rows.Err(); err != nil { return nil, err } + + items = items[:len(items)-1] // Remove summary row return items, nil } -func (q *sqlQuerier) GetWorkspacesWithoutSummary(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { - rows, err := q.GetWorkspaces(ctx, arg) +func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { + rows, err := q.GetWorkspacesWithSummary(ctx, GetWorkspacesWithSummaryParams(arg)) if err != nil { return nil, err } - return rows[:len(rows)-1], nil + rows = rows[:len(rows)-1] + translated := make([]GetWorkspacesRow, 0, len(rows)) + for _, r := range rows { + translated = append(translated, GetWorkspacesRow(r)) + } + return translated, nil } type userQuerier interface { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 92ee81f85fd91..9955f5f325fb6 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -275,8 +275,8 @@ type sqlcQuerier interface { GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) - GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) + GetWorkspacesWithSummary(ctx context.Context, arg GetWorkspacesWithSummaryParams) ([]GetWorkspacesWithSummaryRow, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) // We use the organization_id as the id // for simplicity since all users is diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 1e6d2441cd35e..07619e12e16c1 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11770,7 +11770,112 @@ func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Conte return items, nil } -const getWorkspaces = `-- name: GetWorkspaces :many +const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many +SELECT + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite +FROM + workspaces +LEFT JOIN + workspace_builds ON workspace_builds.workspace_id = workspaces.id +INNER JOIN + provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id +INNER JOIN + templates ON workspaces.template_id = templates.id +WHERE + workspace_builds.build_number = ( + SELECT + MAX(build_number) + FROM + workspace_builds + WHERE + workspace_builds.workspace_id = workspaces.id + ) AND + + ( + -- If the workspace build was a start transition, the workspace is + -- potentially eligible for autostop if it's past the deadline. The + -- deadline is computed at build time upon success and is bumped based + -- on activity (up the max deadline if set). We don't need to check + -- license here since that's done when the values are written to the build. + ( + workspace_builds.transition = 'start'::workspace_transition AND + workspace_builds.deadline IS NOT NULL AND + workspace_builds.deadline < $1 :: timestamptz + ) OR + + -- If the workspace build was a stop transition, the workspace is + -- potentially eligible for autostart if it has a schedule set. The + -- caller must check if the template allows autostart in a license-aware + -- fashion as we cannot check it here. + ( + workspace_builds.transition = 'stop'::workspace_transition AND + workspaces.autostart_schedule IS NOT NULL + ) OR + + -- If the workspace's most recent job resulted in an error + -- it may be eligible for failed stop. + ( + provisioner_jobs.error IS NOT NULL AND + provisioner_jobs.error != '' AND + workspace_builds.transition = 'start'::workspace_transition + ) OR + + -- If the workspace's template has an inactivity_ttl set + -- it may be eligible for dormancy. + ( + templates.time_til_dormant > 0 AND + workspaces.dormant_at IS NULL + ) OR + + -- If the workspace's template has a time_til_dormant_autodelete set + -- and the workspace is already dormant. + ( + templates.time_til_dormant_autodelete > 0 AND + workspaces.dormant_at IS NOT NULL + ) + ) AND workspaces.deleted = 'false' +` + +func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForTransition, now) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Workspace + for rows.Next() { + var i Workspace + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.OrganizationID, + &i.TemplateID, + &i.Deleted, + &i.Name, + &i.AutostartSchedule, + &i.Ttl, + &i.LastUsedAt, + &i.DormantAt, + &i.DeletingAt, + &i.AutomaticUpdates, + &i.Favorite, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getWorkspacesWithSummary = `-- name: GetWorkspacesWithSummary :many WITH filtered_workspaces AS ( SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, @@ -12034,7 +12139,7 @@ CROSS JOIN total_count tc ` -type GetWorkspacesParams struct { +type GetWorkspacesWithSummaryParams struct { Deleted bool `db:"deleted" json:"deleted"` Status string `db:"status" json:"status"` OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` @@ -12053,7 +12158,7 @@ type GetWorkspacesParams struct { Limit int32 `db:"limit_" json:"limit_"` } -type GetWorkspacesRow struct { +type GetWorkspacesWithSummaryRow struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -12080,8 +12185,8 @@ type GetWorkspacesRow struct { Count int64 `db:"count" json:"count"` } -func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaces, +func (q *sqlQuerier) GetWorkspacesWithSummary(ctx context.Context, arg GetWorkspacesWithSummaryParams) ([]GetWorkspacesWithSummaryRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesWithSummary, arg.Deleted, arg.Status, arg.OwnerID, @@ -12103,9 +12208,9 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) return nil, err } defer rows.Close() - var items []GetWorkspacesRow + var items []GetWorkspacesWithSummaryRow for rows.Next() { - var i GetWorkspacesRow + var i GetWorkspacesWithSummaryRow if err := rows.Scan( &i.ID, &i.CreatedAt, @@ -12145,111 +12250,6 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) return items, nil } -const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many -SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite -FROM - workspaces -LEFT JOIN - workspace_builds ON workspace_builds.workspace_id = workspaces.id -INNER JOIN - provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id -INNER JOIN - templates ON workspaces.template_id = templates.id -WHERE - workspace_builds.build_number = ( - SELECT - MAX(build_number) - FROM - workspace_builds - WHERE - workspace_builds.workspace_id = workspaces.id - ) AND - - ( - -- If the workspace build was a start transition, the workspace is - -- potentially eligible for autostop if it's past the deadline. The - -- deadline is computed at build time upon success and is bumped based - -- on activity (up the max deadline if set). We don't need to check - -- license here since that's done when the values are written to the build. - ( - workspace_builds.transition = 'start'::workspace_transition AND - workspace_builds.deadline IS NOT NULL AND - workspace_builds.deadline < $1 :: timestamptz - ) OR - - -- If the workspace build was a stop transition, the workspace is - -- potentially eligible for autostart if it has a schedule set. The - -- caller must check if the template allows autostart in a license-aware - -- fashion as we cannot check it here. - ( - workspace_builds.transition = 'stop'::workspace_transition AND - workspaces.autostart_schedule IS NOT NULL - ) OR - - -- If the workspace's most recent job resulted in an error - -- it may be eligible for failed stop. - ( - provisioner_jobs.error IS NOT NULL AND - provisioner_jobs.error != '' AND - workspace_builds.transition = 'start'::workspace_transition - ) OR - - -- If the workspace's template has an inactivity_ttl set - -- it may be eligible for dormancy. - ( - templates.time_til_dormant > 0 AND - workspaces.dormant_at IS NULL - ) OR - - -- If the workspace's template has a time_til_dormant_autodelete set - -- and the workspace is already dormant. - ( - templates.time_til_dormant_autodelete > 0 AND - workspaces.dormant_at IS NOT NULL - ) - ) AND workspaces.deleted = 'false' -` - -func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]Workspace, error) { - rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForTransition, now) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Workspace - for rows.Next() { - var i Workspace - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OwnerID, - &i.OrganizationID, - &i.TemplateID, - &i.Deleted, - &i.Name, - &i.AutostartSchedule, - &i.Ttl, - &i.LastUsedAt, - &i.DormantAt, - &i.DeletingAt, - &i.AutomaticUpdates, - &i.Favorite, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const insertWorkspace = `-- name: InsertWorkspace :one INSERT INTO workspaces ( diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 787dc59ae1a56..18583e67d4cda 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -76,7 +76,7 @@ WHERE ) ); --- name: GetWorkspaces :many +-- name: GetWorkspacesWithSummary :many WITH filtered_workspaces AS ( SELECT workspaces.*, diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index 11b9cb91a0114..e1928fec5fa15 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -229,7 +229,7 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis timer := prometheus.NewTimer(metricsCollectorAgents) derpMap := derpMapFn() - workspaceRows, err := db.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{ + workspaceRows, err := db.GetWorkspaces(ctx, database.GetWorkspacesParams{ AgentInactiveDisconnectTimeoutSeconds: int64(agentInactiveDisconnectTimeout.Seconds()), }) if err != nil { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 696ea335addbb..a70c0c7d2658e 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -375,7 +375,7 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { return nil }) eg.Go(func() error { - workspaceRows, err := r.options.Database.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{}) + workspaceRows, err := r.options.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{}) if err != nil { return xerrors.Errorf("get workspaces: %w", err) } diff --git a/coderd/templates.go b/coderd/templates.go index fb8c4fa235fa6..d7c578bd639e5 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -70,7 +70,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) { // This is just to get the workspace count, so we use a system context to // return ALL workspaces. Not just workspaces the user can view. // nolint:gocritic - workspaces, err := api.Database.GetWorkspacesWithoutSummary(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{ + workspaces, err := api.Database.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{ TemplateIDs: []uuid.UUID{template.ID}, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/coderd/users.go b/coderd/users.go index 7496d9e5d69c2..cbc9a75059210 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -517,7 +517,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) { return } - workspaces, err := api.Database.GetWorkspacesWithoutSummary(ctx, database.GetWorkspacesParams{ + workspaces, err := api.Database.GetWorkspaces(ctx, database.GetWorkspacesParams{ OwnerID: user.ID, }) if err != nil { From 63615166896a46d12e71dff7b735f2e7ad670208 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 1 Mar 2024 17:20:34 +0100 Subject: [PATCH 8/8] fmt --- coderd/database/dbmem/dbmem.go | 508 ++++++++++++++++---------------- coderd/database/modelqueries.go | 6 +- 2 files changed, 258 insertions(+), 256 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index eccbb79819515..2df269db1053c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -773,6 +773,260 @@ func tagsSubset(m1, m2 map[string]string) bool { // default tags when no tag is specified for a provisioner or job var tagsUntagged = provisionersdk.MutateTags(uuid.Nil, nil) +func (q *FakeQuerier) getAuthorizedWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesWithSummaryRow, error) { + if err := validateDatabaseType(arg); err != nil { + return nil, err + } + + q.mutex.RLock() + defer q.mutex.RUnlock() + + workspaces := make([]database.Workspace, 0) + for _, workspace := range q.workspaces { + if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { + continue + } + + if arg.OwnerUsername != "" { + owner, err := q.getUserByIDNoLock(workspace.OwnerID) + if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { + continue + } + } + + if arg.TemplateName != "" { + template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) + if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { + continue + } + } + + if arg.UsingActive.Valid { + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) + } + + template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) + if err != nil { + return nil, xerrors.Errorf("get template: %w", err) + } + + updated := build.TemplateVersionID == template.ActiveVersionID + if arg.UsingActive.Bool != updated { + continue + } + } + + if !arg.Deleted && workspace.Deleted { + continue + } + + if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { + continue + } + + if !arg.LastUsedBefore.IsZero() { + if workspace.LastUsedAt.After(arg.LastUsedBefore) { + continue + } + } + + if !arg.LastUsedAfter.IsZero() { + if workspace.LastUsedAt.Before(arg.LastUsedAfter) { + continue + } + } + + 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.WorkspaceStatusStarting: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionStart + case database.WorkspaceStatusStopping: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusDeleting: + statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && + build.Transition == database.WorkspaceTransitionDelete + + case "started": + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart + case database.WorkspaceStatusDeleted: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionDelete + case database.WorkspaceStatusStopped: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStop + case database.WorkspaceStatusRunning: + statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && + build.Transition == database.WorkspaceTransitionStart + default: + statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status) + } + if !statusMatch { + continue + } + } + + 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 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 + } + } + + if arg.Dormant && !workspace.DormantAt.Valid { + continue + } + + if len(arg.TemplateIDs) > 0 { + match := false + for _, id := range arg.TemplateIDs { + if workspace.TemplateID == id { + match = true + break + } + } + if !match { + continue + } + } + + // If the filter exists, ensure the object is authorized. + if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { + continue + } + workspaces = append(workspaces, workspace) + } + + // 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 + } + + preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} + preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} + preloadedUsers := map[uuid.UUID]database.User{} + + 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) + } + + 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) + } + + 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) + } + } + + sort.Slice(workspaces, func(i, j int) bool { + w1 := workspaces[i] + w2 := workspaces[j] + + // Order by: favorite first + if arg.RequesterID == w1.OwnerID && w1.Favorite { + return true + } + if arg.RequesterID == w2.OwnerID && w2.Favorite { + return false + } + + // Order by: running + w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) + w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) + + if w1IsRunning && !w2IsRunning { + return true + } + + if !w1IsRunning && w2IsRunning { + return false + } + + // Order by: usernames + if strings.Compare(preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username) < 0 { + return true + } + + // Order by: workspace names + return strings.Compare(w1.Name, w2.Name) < 0 + }) + + beforePageCount := len(workspaces) + + if arg.Offset > 0 { + if int(arg.Offset) > len(workspaces) { + return []database.GetWorkspacesWithSummaryRow{}, nil + } + workspaces = workspaces[arg.Offset:] + } + if arg.Limit > 0 { + if int(arg.Limit) > len(workspaces) { + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil + } + workspaces = workspaces[:arg.Limit] + } + + return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -5136,260 +5390,6 @@ func (q *FakeQuerier) GetWorkspacesWithSummary(ctx context.Context, arg database return q.getAuthorizedWorkspacesWithSummary(ctx, arg, nil) } -func (q *FakeQuerier) getAuthorizedWorkspacesWithSummary(ctx context.Context, arg database.GetWorkspacesWithSummaryParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesWithSummaryRow, error) { - if err := validateDatabaseType(arg); err != nil { - return nil, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - workspaces := make([]database.Workspace, 0) - for _, workspace := range q.workspaces { - if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { - continue - } - - if arg.OwnerUsername != "" { - owner, err := q.getUserByIDNoLock(workspace.OwnerID) - if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { - continue - } - } - - if arg.TemplateName != "" { - template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) - if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { - continue - } - } - - if arg.UsingActive.Valid { - build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) - if err != nil { - return nil, xerrors.Errorf("get latest build: %w", err) - } - - template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID) - if err != nil { - return nil, xerrors.Errorf("get template: %w", err) - } - - updated := build.TemplateVersionID == template.ActiveVersionID - if arg.UsingActive.Bool != updated { - continue - } - } - - if !arg.Deleted && workspace.Deleted { - continue - } - - if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { - continue - } - - if !arg.LastUsedBefore.IsZero() { - if workspace.LastUsedAt.After(arg.LastUsedBefore) { - continue - } - } - - if !arg.LastUsedAfter.IsZero() { - if workspace.LastUsedAt.Before(arg.LastUsedAfter) { - continue - } - } - - 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.WorkspaceStatusStarting: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionStart - case database.WorkspaceStatusStopping: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusDeleting: - statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning && - build.Transition == database.WorkspaceTransitionDelete - - case "started": - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStart - case database.WorkspaceStatusDeleted: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionDelete - case database.WorkspaceStatusStopped: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStop - case database.WorkspaceStatusRunning: - statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded && - build.Transition == database.WorkspaceTransitionStart - default: - statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status) - } - if !statusMatch { - continue - } - } - - 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 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 - } - } - - if arg.Dormant && !workspace.DormantAt.Valid { - continue - } - - if len(arg.TemplateIDs) > 0 { - match := false - for _, id := range arg.TemplateIDs { - if workspace.TemplateID == id { - match = true - break - } - } - if !match { - continue - } - } - - // If the filter exists, ensure the object is authorized. - if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil { - continue - } - workspaces = append(workspaces, workspace) - } - - // 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 - } - - preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{} - preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{} - preloadedUsers := map[uuid.UUID]database.User{} - - 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) - } - - 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) - } - - 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) - } - } - - sort.Slice(workspaces, func(i, j int) bool { - w1 := workspaces[i] - w2 := workspaces[j] - - // Order by: favorite first - if arg.RequesterID == w1.OwnerID && w1.Favorite { - return true - } - if arg.RequesterID == w2.OwnerID && w2.Favorite { - return false - } - - // Order by: running - w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID]) - w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID]) - - if w1IsRunning && !w2IsRunning { - return true - } - - if !w1IsRunning && w2IsRunning { - return false - } - - // Order by: usernames - if strings.Compare(preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username) < 0 { - return true - } - - // Order by: workspace names - return strings.Compare(w1.Name, w2.Name) < 0 - }) - - beforePageCount := len(workspaces) - - if arg.Offset > 0 { - if int(arg.Offset) > len(workspaces) { - return []database.GetWorkspacesWithSummaryRow{}, nil - } - workspaces = workspaces[arg.Offset:] - } - if arg.Limit > 0 { - if int(arg.Limit) > len(workspaces) { - return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil - } - workspaces = workspaces[:arg.Limit] - } - - return q.convertToWorkspaceRowsNoLock(ctx, workspaces, int64(beforePageCount)), nil -} - func (q *FakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) { if err := validateDatabaseType(arg); err != nil { return database.APIKey{}, err diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 2ec4101e847e6..20bca49b49318 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -197,8 +197,10 @@ type workspaceQuerier interface { GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) } -type GetWorkspacesParams GetWorkspacesWithSummaryParams -type GetWorkspacesRow GetWorkspacesWithSummaryRow +type ( + GetWorkspacesParams GetWorkspacesWithSummaryParams + GetWorkspacesRow GetWorkspacesWithSummaryRow +) // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. // This code is copied from `GetWorkspaces` and adds the authorized filter WHERE