diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 9971d3bf7f8..4331260db99 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -61,8 +61,14 @@ import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync'; import DiffsFileTree from './diffs_file_tree.vue'; import getMRCodequalityAndSecurityReports from './graphql/get_mr_codequality_and_security_reports.query.graphql'; +export const FINDINGS_STATUS_PARSED = 'PARSED'; +export const FINDINGS_STATUS_ERROR = 'ERROR'; +export const FINDINGS_POLL_INTERVAL = 1000; + export default { name: 'DiffsApp', + FINDINGS_STATUS_PARSED, + FINDINGS_STATUS_ERROR, components: { DiffsFileTree, FindingsDrawer, @@ -145,6 +151,7 @@ export default { apollo: { getMRCodequalityAndSecurityReports: { query: getMRCodequalityAndSecurityReports, + pollInterval: FINDINGS_POLL_INTERVAL, variables() { return { fullPath: this.projectPath, iid: this.iid }; }, @@ -154,23 +161,37 @@ export default { return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable); }, update(data) { - if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) { - this.$store.commit( - 'diffs/SET_CODEQUALITY_DATA', - sortFindingsByFile( - data.project.mergeRequest.codequalityReportsComparer.report.newErrors, - ), + const codeQualityBoolean = Boolean(this.endpointCodequality); + const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {}; + + if ( + (sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) && + /* Checking for newErrors instead of a status indicator is a workaround that + needs to be adjusted once https://gitlab.com/gitlab-org/gitlab/-/issues/429527 is resolved. */ + (!codeQualityBoolean || codequalityReportsComparer?.report?.newErrors.length > 0) + ) { + this.getMRCodequalityAndSecurityReportStopPolling( + this.$apollo.queries.getMRCodequalityAndSecurityReports, ); } - if (data.project?.mergeRequest?.sastReport?.report) { - this.$store.commit('diffs/SET_SAST_DATA', data.project.mergeRequest.sastReport.report); + if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) { + this.fetchScannerFindingsError(); + } + + if (codequalityReportsComparer?.report?.newErrors) { + this.$store.commit( + 'diffs/SET_CODEQUALITY_DATA', + sortFindingsByFile(codequalityReportsComparer.report.newErrors), + ); + } + + if (sastReport?.report) { + this.$store.commit('diffs/SET_SAST_DATA', sastReport.report); } }, error() { - createAlert({ - message: __('Something went wrong fetching the Scanner Findings. Please try again!'), - }); + this.fetchScannerFindingsError(); }, }, }, @@ -410,6 +431,11 @@ export default { closeDrawer() { this.setDrawer({}); }, + fetchScannerFindingsError() { + createAlert({ + message: __('Something went wrong fetching the Scanner Findings. Please try again.'), + }); + }, subscribeToEvents() { notesEventHub.$once('fetchDiffData', this.fetchData); notesEventHub.$on('refetchDiffData', this.refetchDiffData); @@ -419,6 +445,9 @@ export default { diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); }, + getMRCodequalityAndSecurityReportStopPolling(query) { + query.stopPolling(); + }, unsubscribeFromEvents() { diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); @@ -494,7 +523,7 @@ export default { }) .catch(() => { createAlert({ - message: __('Something went wrong on our end. Please try again!'), + message: __('Something went wrong on our end. Please try again.'), }); }); } @@ -511,7 +540,7 @@ export default { }) .catch(() => { createAlert({ - message: __('Something went wrong on our end. Please try again!'), + message: __('Something went wrong on our end. Please try again.'), }); }); } diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index fd5fcb12cc5..4e8b75536a4 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -78,7 +78,7 @@ export default { newEnvironmentButtonLabel: s__('Environments|New environment'), reviewAppButtonLabel: s__('Environments|Enable review apps'), cleanUpEnvsButtonLabel: s__('Environments|Clean up environments'), - available: __('Available'), + active: __('Active'), stopped: __('Stopped'), prevPage: __('Go to previous page'), nextPage: __('Go to next page'), @@ -97,9 +97,7 @@ export default { isStopStaleEnvModalVisible: false, page: parseInt(page, 10), pageInfo: {}, - scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope) - ? scope - : ENVIRONMENTS_SCOPE.AVAILABLE, + scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope) ? scope : ENVIRONMENTS_SCOPE.ACTIVE, environmentToDelete: {}, environmentToRollback: {}, environmentToStop: {}, @@ -133,14 +131,14 @@ export default { hasSearch() { return Boolean(this.search); }, - availableCount() { - return this.environmentApp?.availableCount; + activeCount() { + return this.environmentApp?.activeCount ?? 0; }, stoppedCount() { - return this.environmentApp?.stoppedCount; + return this.environmentApp?.stoppedCount ?? 0; }, hasAnyEnvironment() { - return this.availableCount > 0 || this.stoppedCount > 0; + return this.activeCount > 0 || this.stoppedCount > 0; }, showContent() { return this.hasAnyEnvironment || this.hasSearch; @@ -278,13 +276,13 @@ export default { @primary="showCleanUpEnvsModal" > diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue index 550568ffaaa..36cce29d624 100644 --- a/app/assets/javascripts/environments/components/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue @@ -111,7 +111,6 @@ export default { diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue index c603d83db9c..8ecb61711ce 100644 --- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue +++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue @@ -37,11 +37,6 @@ export default { required: true, type: String, }, - namespace: { - required: false, - type: String, - default: '', - }, fluxResourcePath: { required: false, type: String, @@ -54,14 +49,12 @@ export default { variables() { return { configuration: this.configuration, - namespace: this.namespace, - environmentName: this.environmentName.toLowerCase(), fluxResourcePath: this.fluxResourcePath, }; }, skip() { return Boolean( - !this.namespace || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE), + !this.fluxResourcePath || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE), ); }, error(err) { @@ -73,17 +66,12 @@ export default { variables() { return { configuration: this.configuration, - namespace: this.namespace, - environmentName: this.environmentName.toLowerCase(), fluxResourcePath: this.fluxResourcePath, }; }, skip() { return Boolean( - !this.namespace || - this.$apollo.queries.fluxKustomizationStatus.loading || - this.hasKustomizations || - this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE), + !this.fluxResourcePath || this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE), ); }, error(err) { diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js index 7214454c45c..e97720312b0 100644 --- a/app/assets/javascripts/environments/constants.js +++ b/app/assets/javascripts/environments/constants.js @@ -42,12 +42,12 @@ export const CANARY_STATUS = { export const CANARY_UPDATE_MODAL = 'confirm-canary-change'; export const ENVIRONMENTS_SCOPE = { - AVAILABLE: 'available', + ACTIVE: 'active', STOPPED: 'stopped', }; export const ENVIRONMENT_COUNT_BY_SCOPE = { - [ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount', + [ENVIRONMENTS_SCOPE.ACTIVE]: 'activeCount', [ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount', }; diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql index 7a50ded7d6c..ef5a8194dca 100644 --- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql @@ -1,6 +1,6 @@ query getEnvironmentApp($page: Int, $scope: String, $search: String) { environmentApp(page: $page, scope: $scope, search: $search) @client { - availableCount + activeCount stoppedCount environments reviewApp diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql index 544232dafd7..042bdc1992d 100644 --- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql @@ -1,15 +1,6 @@ -query getFluxHelmReleaseStatusQuery( - $configuration: LocalConfiguration - $namespace: String - $environmentName: String - $fluxResourcePath: String -) { - fluxHelmReleaseStatus( - configuration: $configuration - namespace: $namespace - environmentName: $environmentName - fluxResourcePath: $fluxResourcePath - ) @client { +query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) { + fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath) + @client { message status type diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql index 2884f95355e..458b8a4d9db 100644 --- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql @@ -1,15 +1,9 @@ query getFluxHelmKustomizationStatusQuery( $configuration: LocalConfiguration - $namespace: String - $environmentName: String $fluxResourcePath: String ) { - fluxKustomizationStatus( - configuration: $configuration - namespace: $namespace - environmentName: $environmentName - fluxResourcePath: $fluxResourcePath - ) @client { + fluxKustomizationStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath) + @client { message status type diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql index c662acb8f93..ac6a68e450c 100644 --- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql @@ -1,6 +1,6 @@ query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) { folder(environment: $environment, scope: $scope, search: $search) @client { - availableCount + activeCount environments stoppedCount } diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js index 9752a3a6634..4427b8ff2ef 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/base.js +++ b/app/assets/javascripts/environments/graphql/resolvers/base.js @@ -47,7 +47,7 @@ export const baseQueries = (endpoint) => ({ }); return { - availableCount: res.data.available_count, + activeCount: res.data.active_count, environments: res.data.environments.map(mapNestedEnvironment), reviewApp: { ...convertObjectPropsToCamelCase(res.data.review_app), @@ -61,7 +61,7 @@ export const baseQueries = (endpoint) => ({ }, folder(_, { environment: { folderPath }, scope, search }) { return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({ - availableCount: res.data.available_count, + activeCount: res.data.active_count, environments: res.data.environments.map(mapEnvironment), stoppedCount: res.data.stopped_count, __typename: 'LocalEnvironmentFolder', diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js index 627737276db..5cb5db5d752 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/flux.js +++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js @@ -18,14 +18,8 @@ const handleClusterError = (err) => { throw error; }; -const buildFluxResourceUrl = ({ - basePath, - namespace, - apiVersion, - resourceType, - environmentName = '', -}) => { - return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`; +const buildFluxResourceUrl = ({ basePath, namespace, apiVersion, resourceType }) => { + return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`; }; const buildFluxResourceWatchPath = ({ namespace, apiVersion, resourceType }) => { @@ -57,17 +51,22 @@ const watchFluxResource = ({ watchPath, resourceName, query, variables, field, c }); }; -const getFluxResourceStatus = ({ url, watchPath, query, variables, field, client }) => { +const getFluxResourceStatus = ({ query, variables, field, resourceType, client }) => { const { headers } = variables.configuration; const withCredentials = true; + const url = `${variables.configuration.basePath}/apis/${variables.fluxResourcePath}`; return axios .get(url, { withCredentials, headers }) .then((res) => { const fluxData = res?.data; const resourceName = fluxData?.metadata?.name; + const namespace = fluxData?.metadata?.namespace; + const apiVersion = fluxData?.apiVersion; if (gon.features?.k8sWatchApi && resourceName) { + const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType }); + watchFluxResource({ watchPath, resourceName, @@ -111,67 +110,21 @@ const getFluxResources = (configuration, url) => { }; export default { - fluxKustomizationStatus( - _, - { configuration, namespace, environmentName, fluxResourcePath = '' }, - { client }, - ) { - const watchPath = buildFluxResourceWatchPath({ - namespace, - apiVersion: kustomizationsApiVersion, - resourceType: KUSTOMIZATIONS_RESOURCE_TYPE, - }); - let url; - - if (fluxResourcePath) { - url = `${configuration.basePath}/apis/${fluxResourcePath}`; - } else { - url = buildFluxResourceUrl({ - basePath: configuration.basePath, - resourceType: KUSTOMIZATIONS_RESOURCE_TYPE, - apiVersion: kustomizationsApiVersion, - namespace, - environmentName, - }); - } + fluxKustomizationStatus(_, { configuration, fluxResourcePath }, { client }) { return getFluxResourceStatus({ - url, - watchPath, query: fluxKustomizationStatusQuery, - variables: { configuration, namespace, environmentName, fluxResourcePath }, + variables: { configuration, fluxResourcePath }, field: kustomizationField, + resourceType: KUSTOMIZATIONS_RESOURCE_TYPE, client, }); }, - fluxHelmReleaseStatus( - _, - { configuration, namespace, environmentName, fluxResourcePath }, - { client }, - ) { - const watchPath = buildFluxResourceWatchPath({ - namespace, - apiVersion: helmReleasesApiVersion, - resourceType: HELM_RELEASES_RESOURCE_TYPE, - }); - let url; - - if (fluxResourcePath) { - url = `${configuration.basePath}/apis/${fluxResourcePath}`; - } else { - url = buildFluxResourceUrl({ - basePath: configuration.basePath, - resourceType: HELM_RELEASES_RESOURCE_TYPE, - apiVersion: helmReleasesApiVersion, - namespace, - environmentName, - }); - } + fluxHelmReleaseStatus(_, { configuration, fluxResourcePath }, { client }) { return getFluxResourceStatus({ - url, - watchPath, query: fluxHelmReleaseStatusQuery, - variables: { configuration, namespace, environmentName, fluxResourcePath }, + variables: { configuration, fluxResourcePath }, field: helmReleaseField, + resourceType: HELM_RELEASES_RESOURCE_TYPE, client, }); }, diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue index 5426f3965b3..76a73093206 100644 --- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue +++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue @@ -36,6 +36,11 @@ export default { return this.workItemType.toUpperCase().split(' ').join('_'); }, iconName() { + // TODO Delete this conditional once we have an `issue-type-epic` icon + if (this.workItemIconName === 'issue-type-epic') { + return 'epic'; + } + return ( this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon || diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb index 773ef2bddca..17a79f83a78 100644 --- a/app/controllers/jira_connect/subscriptions_controller.rb +++ b/app/controllers/jira_connect/subscriptions_controller.rb @@ -48,7 +48,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController def destroy subscription = current_jira_installation.subscriptions.find(params[:id]) - if !jira_user&.site_admin? + if !jira_user&.jira_admin? render json: { error: 'forbidden' }, status: :forbidden elsif subscription.destroy render json: { success: true } diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index efb8c63252b..4b2749dc716 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,6 +2,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController MIN_SEARCH_LENGTH = 3 + ACTIVE_STATES = %i[available stopping].freeze + SCOPES_TO_STATES = { "active" => ACTIVE_STATES, "stopped" => %i[stopped] }.freeze include ProductAnalyticsTracking include KasCookie @@ -35,7 +37,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do - @environments = search_environments.with_state(params[:scope] || :available) + states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES) + @environments = search_environments.with_state(states) + environments_count_by_state = search_environments.count_by_state Gitlab::PollingInterval.set_header(response, interval: 3_000) @@ -44,6 +48,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController review_app: serialize_review_app, can_stop_stale_environments: can?(current_user, :stop_environment, @project), available_count: environments_count_by_state[:available], + active_count: environments_count_by_state[:available] + environments_count_by_state[:stopping], stopped_count: environments_count_by_state[:stopped] } end @@ -58,14 +63,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do + states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES) folder_environments = search_environments(type: params[:id]) - @environments = folder_environments.with_state(params[:scope] || :available) + @environments = folder_environments.with_state(states) .order(:name) render json: { environments: serialize_environments(request, response), available_count: folder_environments.available.count, + active_count: folder_environments.active.count, stopped_count: folder_environments.stopped.count } end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index ec4fbdec73a..8d4f50de75e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -32,6 +32,8 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord ], remove_with: '16.5', remove_after: '2023-09-22' ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22' + ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21' + INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ 'Admin Area > Settings > Metrics and profiling > Metrics - Grafana' @@ -91,7 +93,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize - serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize # See https://gitlab.com/gitlab-org/gitlab/-/issues/300916 serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize @@ -303,8 +304,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :repository_storages, presence: true - validate :check_repository_storages validate :check_repository_storages_weighted validates :auto_devops_domain, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index a5ed402aa9a..00b093c8ac3 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -159,7 +159,6 @@ module ApplicationSettingImplementation recaptcha_enabled: false, repository_checks_enabled: true, repository_storages_weighted: { 'default' => 100 }, - repository_storages: ['default'], require_admin_approval_after_user_signup: true, require_two_factor_authentication: false, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], @@ -434,10 +433,6 @@ module ApplicationSettingImplementation read_attribute(:asset_proxy_whitelist) end - def repository_storages - Array(read_attribute(:repository_storages)) - end - def commit_email_hostname super.presence || self.class.default_commit_email_hostname end @@ -645,12 +640,6 @@ module ApplicationSettingImplementation self.uuid = SecureRandom.uuid end - def check_repository_storages - invalid = repository_storages - Gitlab.config.repositories.storages.keys - errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless - invalid.empty? - end - def coerce_repository_storages_weighted repository_storages_weighted.transform_values!(&:to_i) end diff --git a/app/models/environment.rb b/app/models/environment.rb index c3066bbca0a..4f76fae24eb 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -91,6 +91,7 @@ class Environment < ApplicationRecord delegate :auto_rollback_enabled?, to: :project scope :available, -> { with_state(:available) } + scope :active, -> { with_state(:available, :stopping) } scope :stopped, -> { with_state(:stopped) } scope :order_by_last_deployed_at, -> do diff --git a/app/services/ci/catalog/resources/validate_service.rb b/app/services/ci/catalog/resources/validate_service.rb index 186b1699d03..0e842fb7405 100644 --- a/app/services/ci/catalog/resources/validate_service.rb +++ b/app/services/ci/catalog/resources/validate_service.rb @@ -44,7 +44,7 @@ module Ci return if Ci::Catalog::ComponentsProject.new(project).fetch_component_paths(ref, limit: MINIMUM_AMOUNT_OF_COMPONENTS).any? - errors << 'Project must contain components' + errors << 'Project must contain components. Ensure you are using the correct directory structure' end def project_has_readme? diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb index d5ab3800dcf..f537da5c091 100644 --- a/app/services/jira_connect_subscriptions/create_service.rb +++ b/app/services/jira_connect_subscriptions/create_service.rb @@ -11,7 +11,7 @@ module JiraConnectSubscriptions return error(s_('JiraConnect|Could not fetch user information from Jira. ' \ 'Check the permissions in Jira and try again.'), 403) elsif !can_administer_jira? - return error(s_('JiraConnect|The Jira user is not a site administrator. ' \ + return error(s_('JiraConnect|The Jira user is not a site or organization administrator. ' \ 'Check the permissions in Jira and try again.'), 403) end @@ -25,7 +25,7 @@ module JiraConnectSubscriptions private def can_administer_jira? - params[:jira_user]&.site_admin? + params[:jira_user]&.jira_admin? end def create_subscription diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 79bc07103fd..b453098e27a 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -83,18 +83,9 @@ module Members end def add_members - @members = if Feature.enabled?(:invitations_member_role_id, source) - creator_service.add_members( - source, invites, params[:access_level], **create_params - ) - else - source.add_members( - invites, - params[:access_level], - expires_at: params[:expires_at], - current_user: current_user - ) - end + @members = creator_service.add_members( + source, invites, params[:access_level], **create_params + ) members.each { |member| process_result(member) } end diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index 0e105ca3575..38c9e6d60a7 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -53,7 +53,7 @@ module Releases if project.catalog_resource && release.valid? response = Ci::Catalog::Resources::ReleaseService.new(release).execute - return error(response.message) if response.error? + return error(response.message, 422) if response.error? end release.save! diff --git a/config/feature_flags/development/explain_code_vertex_ai.yml b/config/feature_flags/development/explain_code_vertex_ai.yml deleted file mode 100644 index 4eb4d64ed30..00000000000 --- a/config/feature_flags/development/explain_code_vertex_ai.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: explain_code_vertex_ai -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125292 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416907 -milestone: '16.2' -type: development -group: group::source code -default_enabled: false diff --git a/config/feature_flags/development/invitations_member_role_id.yml b/config/feature_flags/development/invitations_member_role_id.yml deleted file mode 100644 index ccb319e4e35..00000000000 --- a/config/feature_flags/development/invitations_member_role_id.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: invitations_member_role_id -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134100 -rollout_issue_url: -milestone: '16.6' -type: development -group: group::authorization -default_enabled: false diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb index 7f0196197b9..40d959c1ba0 100644 --- a/config/initializers/postgresql_cte.rb +++ b/config/initializers/postgresql_cte.rb @@ -40,6 +40,8 @@ module ActiveRecord::Querying delegate :with, to: :all end +# Rails 7.1 defines #with method. +# Therefore, this file can be either simplified or completely removed. module ActiveRecord class Relation # WithChain objects act as placeholder for queries in which #with does not have any parameter. @@ -51,21 +53,21 @@ module ActiveRecord # Returns a new relation expressing WITH RECURSIVE def recursive(*args) - @scope.with_values += args + @scope.with_values_ += args @scope.recursive_value = true @scope.extend(Gitlab::Database::ReadOnlyRelation) @scope end end - def with_values - @values[:with] || [] + def with_values_ + @values[:with_values] || [] end - def with_values=(values) + def with_values_=(values) raise ImmutableRelation if @loaded - @values[:with] = values + @values[:with_values] = values end def recursive_value=(value) @@ -92,7 +94,7 @@ module ActiveRecord if opts == :chain WithChain.new(self) else - self.with_values += [opts] + rest + self.with_values_ += [opts] + rest self end end @@ -100,13 +102,13 @@ module ActiveRecord def build_arel(aliases = nil) arel = super - build_with(arel) if @values[:with] + build_with(arel) if @values[:with_values] arel end def build_with(arel) - with_statements = with_values.flat_map do |with_value| + with_statements = with_values_.flat_map do |with_value| case with_value when String with_value diff --git a/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb b/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb new file mode 100644 index 00000000000..8916a1e9729 --- /dev/null +++ b/db/post_migrate/20231101130230_remove_in_product_marketing_emails_campaign_column.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class RemoveInProductMarketingEmailsCampaignColumn < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '16.6' + + TARGET_TABLE = :in_product_marketing_emails + UNIQUE_INDEX_NAME = :index_in_product_marketing_emails_on_user_campaign + CONSTRAINT_NAME = :in_product_marketing_emails_track_and_series_or_campaign + TRACK_AND_SERIES_NOT_NULL_CONSTRAINT = 'track IS NOT NULL AND series IS NOT NULL AND campaign IS NULL' + CAMPAIGN_NOT_NULL_CONSTRAINT = 'track IS NULL AND series IS NULL AND campaign IS NOT NULL' + + def up + with_lock_retries do + remove_column :in_product_marketing_emails, :campaign, if_exists: true + end + end + + def down + with_lock_retries do + add_column :in_product_marketing_emails, :campaign, :text, if_not_exists: true + end + + add_text_limit :in_product_marketing_emails, :campaign, 255 + + add_concurrent_index TARGET_TABLE, [:user_id, :campaign], unique: true, name: UNIQUE_INDEX_NAME + add_check_constraint TARGET_TABLE, + "(#{TRACK_AND_SERIES_NOT_NULL_CONSTRAINT}) OR (#{CAMPAIGN_NOT_NULL_CONSTRAINT})", + CONSTRAINT_NAME + end +end diff --git a/db/schema_migrations/20231101130230 b/db/schema_migrations/20231101130230 new file mode 100644 index 00000000000..8fa382d7033 --- /dev/null +++ b/db/schema_migrations/20231101130230 @@ -0,0 +1 @@ +c8dbdeb4ffcb7f5dc1c719a09a1f6c41188f584c80331a4482542a873d3ad12d \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 04f3f70feb8..870ae4727f1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17462,10 +17462,7 @@ CREATE TABLE in_product_marketing_emails ( track smallint, series smallint, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - campaign text, - CONSTRAINT check_9d8b29f74f CHECK ((char_length(campaign) <= 255)), - CONSTRAINT in_product_marketing_emails_track_and_series_or_campaign CHECK ((((track IS NOT NULL) AND (series IS NOT NULL) AND (campaign IS NULL)) OR ((track IS NULL) AND (series IS NULL) AND (campaign IS NOT NULL)))) + updated_at timestamp with time zone NOT NULL ); CREATE SEQUENCE in_product_marketing_emails_id_seq @@ -32915,8 +32912,6 @@ CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree ( CREATE INDEX index_in_product_marketing_emails_on_track_series_id_clicked ON in_product_marketing_emails USING btree (track, series, id, cta_clicked_at); -CREATE UNIQUE INDEX index_in_product_marketing_emails_on_user_campaign ON in_product_marketing_emails USING btree (user_id, campaign); - CREATE INDEX index_in_product_marketing_emails_on_user_id ON in_product_marketing_emails USING btree (user_id); CREATE UNIQUE INDEX index_in_product_marketing_emails_on_user_track_series ON in_product_marketing_emails USING btree (user_id, track, series); diff --git a/doc/administration/settings/jira_cloud_app.md b/doc/administration/settings/jira_cloud_app.md index 0b9def383d5..8ff2a9acdb8 100644 --- a/doc/administration/settings/jira_cloud_app.md +++ b/doc/administration/settings/jira_cloud_app.md @@ -50,14 +50,25 @@ To create an OAuth application on your self-managed instance: ## Jira user requirements -You must ensure that the Jira user that is used to setup the GitLab for Jira Cloud app is a member of the Site Administrators (`site-admins`) group in your -[Atlassian organization](https://admin.atlassian.com): +> Support for the `org-admins` group [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420687) in GitLab 16.6. -1. If you don't have a `site-admins` group in your Atlassian organization, [create the group](https://support.atlassian.com/user-management/docs/create-groups/). -1. If not already a member, [add your Jira user as a member](https://support.atlassian.com/user-management/docs/edit-a-group/) of the `site-admins` group. +In your [Atlassian organization](https://admin.atlassian.com), you must ensure that the Jira user that is used to set up the GitLab for Jira Cloud app is a member of +either: -If you have customized your global permissions in Jira, you might also need to grant the -[`Browse users and groups` permission](https://confluence.atlassian.com/jirakb/unable-to-browse-for-users-and-groups-120521888.html) to the Jira user. +- The Organization Administrators (`org-admins`) group. Newer Atlassian organizations are using + [centralized user management](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Centralized-user-management-content), + which contains the `org-admins` group. Existing Atlassian organizations are being migrated to centralized user management. + If available, you should use the `org-admins` group to indicate which Jira users can manage the GitLab for Jira app. Alternatively you can use the + `site-admins` group. +- The Site Administrators (`site-admins`) group. The `site-admins` group was used under + [original user management](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Original-user-management-content). + +If necessary: + +1. [Create your preferred group](https://support.atlassian.com/user-management/docs/create-groups/). +1. [Edit the group](https://support.atlassian.com/user-management/docs/edit-a-group/) to add your Jira user as a member of it. +1. If you customized your global permissions in Jira, you might also need to grant the + [`Browse users and groups` permission](https://confluence.atlassian.com/jirakb/unable-to-browse-for-users-and-groups-120521888.html) to the Jira user. ## Connect the GitLab for Jira Cloud app diff --git a/doc/api/invitations.md b/doc/api/invitations.md index 9c209f04d65..0bf38b6e616 100644 --- a/doc/api/invitations.md +++ b/doc/api/invitations.md @@ -43,7 +43,7 @@ POST /projects/:id/invitations | `access_level` | integer | yes | A valid access level | | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | | `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). | -| `member_role_id` **(ULTIMATE ALL)** | integer | no | Assigns the new member to the provided custom role. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134100) in GitLab 16.6 under a feature flag `invitations_member_role_id`. | +| `member_role_id` **(ULTIMATE ALL)** | integer | no | Assigns the new member to the provided custom role. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134100) in GitLab 16.6. | ```shell curl --request POST --header "PRIVATE-TOKEN: " \ diff --git a/doc/api/settings.md b/doc/api/settings.md index 0a74afe2abf..c8899eeb3e6 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -216,7 +216,6 @@ Example response: "container_registry_token_expire_delay": 5, "decompress_archive_file_timeout": 210, "package_registry_cleanup_policies_worker_capacity": 2, - "repository_storages": ["default"], "plantuml_enabled": false, "plantuml_url": null, "diagramsnet_enabled": true, @@ -537,7 +536,6 @@ listed in the descriptions of the relevant settings. | `repository_checks_enabled` | boolean | no | GitLab periodically runs `git fsck` in all project and wiki repositories to look for silent disk corruption issues. | | `repository_size_limit` **(PREMIUM ALL)** | integer | no | Size limit per repository (MB) | | `repository_storages_weighted` | hash of strings to integers | no | (GitLab 13.1 and later) Hash of names of taken from `gitlab.yml` to [weights](../administration/repository_storage_paths.md#configure-where-new-repositories-are-stored). New projects are created in one of these stores, chosen by a weighted random selection. | -| `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. | | `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../administration/moderate_users.md) by an administrator. | | `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. | | `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-Administrator users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction.[Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot select levels that are set as `default_project_visibility` and `default_group_visibility`. | diff --git a/doc/ci/environments/kubernetes_dashboard.md b/doc/ci/environments/kubernetes_dashboard.md index 4062fbee25e..42fa560ad76 100644 --- a/doc/ci/environments/kubernetes_dashboard.md +++ b/doc/ci/environments/kubernetes_dashboard.md @@ -83,8 +83,7 @@ You can review the sync status of your Flux deployments from a dashboard. To display the deployment status, your dashboard must be able to retrieve the `Kustomization` and `HelmRelease` resources, which requires a namespace to be configured for the environment. -By default, GitLab searches the `Kustomization` and `HelmRelease` resources for the name of the project slug. -You can specify the resource names with the **Flux resource** dropdown list in the environment settings. +GitLab searches the `Kustomization` and `HelmRelease` resources specified by the **Flux resource** dropdown list in the environment settings. A dashboard displays one of the following status badges: diff --git a/doc/development/ai_architecture.md b/doc/development/ai_architecture.md index f03ffa748fa..a41d17887ca 100644 --- a/doc/development/ai_architecture.md +++ b/doc/development/ai_architecture.md @@ -77,7 +77,7 @@ A [draft MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122035) has b The index function has been updated to improve search quality. This was tested locally by setting the `ivfflat.probes` value to `10` with the following SQL command: ```ruby -Embedding::TanukiBotMvc.connection.execute("SET ivfflat.probes = 10") +::Embedding::Vertex::GitlabDocumentation.connection.execute("SET ivfflat.probes = 10") ``` Setting the `probes` value for indexing improves results, as per the neighbor [documentation](https://github.com/ankane/neighbor#indexing). diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 9bef2f2ab10..d05249f3d3f 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -78,7 +78,7 @@ end ### Do not use SaaS-only features for functionality in CE -`Gitlab::Saas.feature_vailable?` must not appear in CE. +`Gitlab::Saas.feature_available?` must not appear in CE. See [extending CE with EE guide](#extend-ce-features-with-ee-backend-code). ### SaaS-only features in tests diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index d1f1d28663e..d26f2193124 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -544,3 +544,9 @@ generates the codes. For example: 1. Select General. 1. Select Date & Time. 1. Enable Set Automatically. If it's already enabled, disable it, wait a few seconds, and re-enable. + +### Error: "Permission denied (publickey)" when regenerating recovery codes + +If you receive a `Permission denied (publickey)` error when attempting to [generate new recovery codes using an SSH key](#generate-new-recovery-codes-using-ssh) +and you are using a non-default SSH key pair file path, +you might need to [manually register your private SSH key](../../ssh.md#configure-ssh-to-point-to-a-different-directory) using `ssh-agent`. diff --git a/lib/atlassian/jira_connect/jira_user.rb b/lib/atlassian/jira_connect/jira_user.rb index 57ceb8fdf13..051165474af 100644 --- a/lib/atlassian/jira_connect/jira_user.rb +++ b/lib/atlassian/jira_connect/jira_user.rb @@ -3,15 +3,17 @@ module Atlassian module JiraConnect class JiraUser + ADMIN_GROUPS = %w[site-admins org-admins].freeze + def initialize(data) @data = data end - def site_admin? + def jira_admin? groups = @data.dig('groups', 'items') return false unless groups - groups.any? { |g| g['name'] == 'site-admins' } + groups.any? { |group| ADMIN_GROUPS.include?(group['name']) } end end end diff --git a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb index 11f1e62e8b9..d1edb739b85 100644 --- a/lib/gitlab/database/migration_helpers/convert_to_bigint.rb +++ b/lib/gitlab/database/migration_helpers/convert_to_bigint.rb @@ -4,12 +4,16 @@ module Gitlab module Database module MigrationHelpers module ConvertToBigint - # This helper is extracted for the purpose of - # https://gitlab.com/gitlab-org/gitlab/-/issues/392815 - # so that we can test all combinations just once, - # and simplify migration tests. - # - # Once we are done with the PK conversions we can remove this. + INDEX_OPTIONS_MAP = { + unique: :unique, + order: :orders, + opclass: :opclasses, + where: :where, + type: :type, + using: :using, + comment: :comment + }.freeze + def com_or_dev_or_test_but_not_jh? return true if Gitlab.dev_or_test_env? @@ -29,6 +33,78 @@ module Gitlab column.sql_type == 'bigint' && temp_column.sql_type == 'integer' end + + def add_bigint_column_indexes(table_name, int_column_name) + bigint_column_name = convert_to_bigint_column(int_column_name) + + unless column_exists?(table_name.to_s, bigint_column_name) + raise "Bigint column '#{bigint_column_name}' does not exist on #{table_name}" + end + + indexes(table_name).each do |i| + next unless Array(i.columns).join(' ').match?(/\b#{int_column_name}\b/) + + create_bigint_index(table_name, i, int_column_name, bigint_column_name) + end + end + + # default 'index_name' method is not used because this method can be reused while swapping/dropping the indexes + def bigint_index_name(int_column_index_name) + # First 20 digits of the hash is chosen to make sure it fits the 63 chars limit + digest = Digest::SHA256.hexdigest(int_column_index_name).first(20) + "bigint_idx_#{digest}" + end + + private + + def create_bigint_index(table_name, index_definition, int_column_name, bigint_column_name) + index_attributes = index_definition.as_json + index_options = INDEX_OPTIONS_MAP + .transform_values { |key| index_attributes[key.to_s] } + .select { |_, v| v.present? } + + bigint_index_options = create_bigint_options( + index_options, + index_definition.name, + int_column_name, + bigint_column_name + ) + + add_concurrent_index( + table_name, + bigint_index_columns(int_column_name, bigint_column_name, index_definition.columns), + name: bigint_index_options.delete(:name), + ** bigint_index_options + ) + end + + def bigint_index_columns(int_column_name, bigint_column_name, int_index_columns) + if int_index_columns.is_a?(String) + int_index_columns.gsub(/\b#{int_column_name}\b/, bigint_column_name) + else + int_index_columns.map do |column| + column == int_column_name.to_s ? bigint_column_name : column + end + end + end + + def create_bigint_options(index_options, int_index_name, int_column_name, bigint_column_name) + index_options[:name] = bigint_index_name(int_index_name) + index_options[:where]&.gsub!(/\b#{int_column_name}\b/, bigint_column_name) + + # ordering on multiple columns will return a Hash instead of string + index_options[:order] = + if index_options[:order].is_a?(Hash) + index_options[:order].to_h do |column, order| + column = bigint_column_name if column == int_column_name + [column, order] + end + else + index_options[:order]&.gsub(/\b#{int_column_name}\b/, bigint_column_name) + end + + index_options.select { |_, v| v.present? } + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 31facc5689b..dbe4be7456b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21509,15 +21509,9 @@ msgstr "" msgid "Geo|Remove %{siteType} site" msgstr "" -msgid "Geo|Remove entry" -msgstr "" - msgid "Geo|Remove site" msgstr "" -msgid "Geo|Remove tracking database entry" -msgstr "" - msgid "Geo|Removing a Geo site stops the synchronization to and from that site. Are you sure?" msgstr "" @@ -21689,9 +21683,6 @@ msgstr "" msgid "Geo|Time in seconds" msgstr "" -msgid "Geo|Tracking database entry will be removed. Are you sure?" -msgstr "" - msgid "Geo|Tuning settings" msgstr "" @@ -26948,7 +26939,7 @@ msgstr "" msgid "JiraConnect|Tell us what you think!" msgstr "" -msgid "JiraConnect|The Jira user is not a site administrator. Check the permissions in Jira and try again." +msgid "JiraConnect|The Jira user is not a site or organization administrator. Check the permissions in Jira and try again." msgstr "" msgid "JiraConnect|We would love to learn more about your experience with the GitLab for Jira Cloud App." @@ -45549,7 +45540,7 @@ msgstr "" msgid "Something went wrong" msgstr "" -msgid "Something went wrong fetching the Scanner Findings. Please try again!" +msgid "Something went wrong fetching the Scanner Findings. Please try again." msgstr "" msgid "Something went wrong on our end" diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 3a2c7f0ac7b..0a54f5923f2 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -34,16 +34,16 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery describe 'with one available environment' do let!(:environment) { create(:environment, project: project, state: :available) } - it 'shows "Available" and "Stopped" tab with links' do + it 'shows "Active" and "Stopped" tab with links' do visit_environments(project) - expect(page).to have_link(_('Available')) + expect(page).to have_link(_('Active')) expect(page).to have_link(_('Stopped')) end - describe 'in available tab page' do + describe 'in active tab page' do it 'shows one environment' do - visit_environments(project, scope: 'available') + visit_environments(project, scope: 'active') expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) end @@ -56,7 +56,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery end it 'renders second page of pipelines' do - visit_environments(project, scope: 'available') + visit_environments(project, scope: 'active') find('.page-link.next-page-item').click wait_for_requests @@ -85,7 +85,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery end it 'shows one environment without error' do - visit_environments(project, scope: 'available') + visit_environments(project, scope: 'active') expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) end @@ -95,9 +95,9 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery describe 'with one stopped environment' do let!(:environment) { create(:environment, project: project, state: :stopped) } - describe 'in available tab page' do + describe 'in active tab page' do it 'shows no environments' do - visit_environments(project, scope: 'available') + visit_environments(project, scope: 'active') expect(page).to have_content(s_('Environments|Get started with environments')) end @@ -122,7 +122,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery it 'does not show environments and tabs' do expect(page).to have_content(s_('Environments|Get started with environments')) - expect(page).not_to have_link(_('Available')) + expect(page).not_to have_link(_('Active')) expect(page).not_to have_link(_('Stopped')) end end @@ -142,7 +142,7 @@ RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery it 'shows environments names and counters' do expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) - expect(page).to have_link("#{_('Available')} 1") + expect(page).to have_link("#{_('Active')} 1") expect(page).to have_link("#{_('Stopped')} 0") end diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js index 8c02a07994b..5ac949e77b6 100644 --- a/spec/frontend/environments/environments_app_spec.js +++ b/spec/frontend/environments/environments_app_spec.js @@ -71,7 +71,7 @@ describe('~/environments/components/environments_app.vue', () => { previousPage: 1, __typename: 'LocalPageInfo', }, - location = '?scope=available&page=2&search=prod', + location = '?scope=active&page=2&search=prod', }) => { setWindowLocation(location); environmentAppMock.mockReturnValue(environmentsApp); @@ -96,7 +96,7 @@ describe('~/environments/components/environments_app.vue', () => { paginationMock = jest.fn(); }); - it('should request available environments if the scope is invalid', async () => { + it('should request active environments if the scope is invalid', async () => { await createWrapperWithMocked({ environmentsApp: resolvedEnvironmentsApp, folder: resolvedFolder, @@ -105,7 +105,7 @@ describe('~/environments/components/environments_app.vue', () => { expect(environmentAppMock).toHaveBeenCalledWith( expect.anything(), - expect.objectContaining({ scope: 'available', page: 2 }), + expect.objectContaining({ scope: 'active', page: 2 }), expect.anything(), expect.anything(), ); @@ -225,16 +225,16 @@ describe('~/environments/components/environments_app.vue', () => { }); describe('tabs', () => { - it('should show tabs for available and stopped environmets', async () => { + it('should show tabs for active and stopped environmets', async () => { await createWrapperWithMocked({ environmentsApp: resolvedEnvironmentsApp, folder: resolvedFolder, }); - const [available, stopped] = wrapper.findAllByRole('tab').wrappers; + const [active, stopped] = wrapper.findAllByRole('tab').wrappers; - expect(available.text()).toContain(__('Available')); - expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount.toString()); + expect(active.text()).toContain(__('Active')); + expect(active.text()).toContain(resolvedEnvironmentsApp.activeCount.toString()); expect(stopped.text()).toContain(__('Stopped')); expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount.toString()); }); @@ -379,7 +379,7 @@ describe('~/environments/components/environments_app.vue', () => { next.trigger('click'); await nextTick(); - expect(window.location.search).toBe('?scope=available&page=3&search=prod'); + expect(window.location.search).toBe('?scope=active&page=3&search=prod'); }); }); @@ -406,7 +406,7 @@ describe('~/environments/components/environments_app.vue', () => { await waitForDebounce(); - expect(window.location.search).toBe('?scope=available&page=1&search=hello'); + expect(window.location.search).toBe('?scope=active&page=1&search=hello'); }); it('should query for the entered parameter', async () => { diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index b80b8508e8d..7d354566761 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -267,12 +267,12 @@ export const environmentsApp = { '{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}', }, can_stop_stale_environments: true, - available_count: 4, + active_count: 4, stopped_count: 0, }; export const resolvedEnvironmentsApp = { - availableCount: 4, + activeCount: 4, environments: [ { name: 'review', @@ -535,7 +535,7 @@ export const folder = { has_opened_alert: false, }, ], - available_count: 2, + active_count: 2, stopped_count: 0, }; @@ -704,7 +704,7 @@ export const resolvedEnvironment = { }; export const resolvedFolder = { - availableCount: 2, + activeCount: 2, environments: [ { id: 42, diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js index ea733c6e0e8..526c98b55b3 100644 --- a/spec/frontend/environments/graphql/resolvers/flux_spec.js +++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js @@ -15,8 +15,6 @@ describe('~/frontend/environments/graphql/resolvers', () => { headers: { 'GitLab-Agent-Id': '1' }, }, }; - const namespace = 'default'; - const environmentName = 'my-environment'; beforeEach(() => { mockResolvers = resolvers(); @@ -29,34 +27,14 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('fluxKustomizationStatus', () => { const client = { writeQuery: jest.fn() }; - const endpoint = `${configuration.basePath}/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations/${environmentName}`; const fluxResourcePath = 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app'; - const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`; + const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`; describe('when k8sWatchApi feature is disabled', () => { - it('should request Flux Kustomizations for the provided namespace via the Kubernetes API if the fluxResourcePath is not specified', async () => { - mock - .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers }) - .reply(HTTP_STATUS_OK, { - status: { conditions: fluxKustomizationsMock }, - }); - - const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus( - null, - { - configuration, - namespace, - environmentName, - }, - { client }, - ); - - expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock); - }); it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => { mock - .onGet(endpointWithFluxResourcePath, { + .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers, }) @@ -68,8 +46,6 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, fluxResourcePath, }, { client }, @@ -87,8 +63,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); @@ -108,6 +83,8 @@ describe('~/frontend/environments/graphql/resolvers', () => { } }); const resourceName = 'custom-resource'; + const resourceNamespace = 'custom-namespace'; + const apiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1'; beforeEach(() => { gon.features = { k8sWatchApi: true }; @@ -120,7 +97,8 @@ describe('~/frontend/environments/graphql/resolvers', () => { mock .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers }) .reply(HTTP_STATUS_OK, { - metadata: { name: resourceName }, + apiVersion, + metadata: { name: resourceName, namespace: resourceNamespace }, status: { conditions: fluxKustomizationsMock }, }); }); @@ -129,14 +107,13 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); expect(mockKustomizationStatusFn).toHaveBeenCalledWith( - `/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations`, + `/apis/${apiVersion}/namespaces/${resourceNamespace}/kustomizations`, { watch: true, fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`, @@ -149,8 +126,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); @@ -168,8 +144,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); @@ -181,34 +156,14 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('fluxHelmReleaseStatus', () => { const client = { writeQuery: jest.fn() }; - const endpoint = `${configuration.basePath}/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases/${environmentName}`; const fluxResourcePath = 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app'; - const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`; + const endpoint = `${configuration.basePath}/apis/${fluxResourcePath}`; describe('when k8sWatchApi feature is disabled', () => { - it('should request Flux Helm Releases via the Kubernetes API', async () => { - mock - .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers }) - .reply(HTTP_STATUS_OK, { - status: { conditions: fluxKustomizationsMock }, - }); - - const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus( - null, - { - configuration, - namespace, - environmentName, - }, - { client }, - ); - - expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock); - }); it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => { mock - .onGet(endpointWithFluxResourcePath, { + .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers, }) @@ -220,8 +175,6 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, fluxResourcePath, }, { client }, @@ -239,8 +192,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); @@ -260,6 +212,8 @@ describe('~/frontend/environments/graphql/resolvers', () => { } }); const resourceName = 'custom-resource'; + const resourceNamespace = 'custom-namespace'; + const apiVersion = 'helm.toolkit.fluxcd.io/v2beta1'; beforeEach(() => { gon.features = { k8sWatchApi: true }; @@ -272,7 +226,8 @@ describe('~/frontend/environments/graphql/resolvers', () => { mock .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers }) .reply(HTTP_STATUS_OK, { - metadata: { name: resourceName }, + apiVersion, + metadata: { name: resourceName, namespace: resourceNamespace }, status: { conditions: fluxKustomizationsMock }, }); }); @@ -281,14 +236,13 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); expect(mockHelmReleaseStatusFn).toHaveBeenCalledWith( - `/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases`, + `/apis/${apiVersion}/namespaces/${resourceNamespace}/helmreleases`, { watch: true, fieldSelector: `metadata.name=${decodeURIComponent(resourceName)}`, @@ -301,8 +255,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); @@ -320,8 +273,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { null, { configuration, - namespace, - environmentName, + fluxResourcePath, }, { client }, ); diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js index 9756859f71e..12689df586f 100644 --- a/spec/frontend/environments/kubernetes_overview_spec.js +++ b/spec/frontend/environments/kubernetes_overview_spec.js @@ -122,7 +122,6 @@ describe('~/environments/components/kubernetes_overview.vue', () => { expect(findKubernetesStatusBar().props()).toEqual({ clusterHealthStatus: 'success', configuration, - namespace: kubernetesNamespace, environmentName: resolvedEnvironment.name, fluxResourcePath: fluxResourcePathMock, }); diff --git a/spec/frontend/environments/kubernetes_status_bar_spec.js b/spec/frontend/environments/kubernetes_status_bar_spec.js index 5dec7ca5aac..dcd628354e1 100644 --- a/spec/frontend/environments/kubernetes_status_bar_spec.js +++ b/spec/frontend/environments/kubernetes_status_bar_spec.js @@ -49,7 +49,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { const createWrapper = ({ apolloProvider = createApolloProvider(), clusterHealthStatus = '', - namespace = '', fluxResourcePath = '', } = {}) => { wrapper = shallowMountExtended(KubernetesStatusBar, { @@ -57,7 +56,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { clusterHealthStatus, configuration, environmentName, - namespace, fluxResourcePath, }, apolloProvider, @@ -88,7 +86,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { }); describe('sync badge', () => { - describe('when no namespace is provided', () => { + describe('when no flux resource path is provided', () => { beforeEach(() => { createWrapper(); }); @@ -104,7 +102,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { }); describe('when flux resource path is provided', () => { - const namespace = 'my-namespace'; let fluxResourcePath; describe('if the provided resource is a Kustomization', () => { @@ -112,7 +109,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { fluxResourcePath = 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app'; - createWrapper({ namespace, fluxResourcePath }); + createWrapper({ fluxResourcePath }); }); it('requests the Kustomization resource status', () => { @@ -120,8 +117,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { {}, expect.objectContaining({ configuration, - namespace, - environmentName, fluxResourcePath, }), expect.any(Object), @@ -139,7 +134,7 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { fluxResourcePath = 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app'; - createWrapper({ namespace, fluxResourcePath }); + createWrapper({ fluxResourcePath }); }); it('requests the HelmRelease resource status', () => { @@ -147,8 +142,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { {}, expect.objectContaining({ configuration, - namespace, - environmentName, fluxResourcePath, }), expect.any(Object), @@ -160,30 +153,6 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled(); }); }); - }); - - describe('when namespace is provided', () => { - describe('with no Flux resources found', () => { - beforeEach(() => { - createWrapper({ namespace: 'my-namespace' }); - }); - - it('requests Kustomizations', () => { - expect(fluxKustomizationStatusQuery).toHaveBeenCalled(); - }); - - it('requests HelmReleases when there were no Kustomizations found', async () => { - await waitForPromises(); - - expect(fluxHelmReleaseStatusQuery).toHaveBeenCalled(); - }); - - it('renders sync status as Unavailable when no Kustomizations and HelmReleases found', async () => { - await waitForPromises(); - - expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable')); - }); - }); describe('with Flux Kustomizations available', () => { const createApolloProviderWithKustomizations = ({ @@ -202,63 +171,11 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { it("doesn't request HelmReleases when the Kustomizations were found", async () => { createWrapper({ apolloProvider: createApolloProviderWithKustomizations(), - namespace: 'my-namespace', }); await waitForPromises(); expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled(); }); - - it.each` - status | type | badgeType - ${'True'} | ${'Stalled'} | ${'stalled'} - ${'True'} | ${'Reconciling'} | ${'reconciling'} - ${'True'} | ${'Ready'} | ${'reconciled'} - ${'False'} | ${'Ready'} | ${'failed'} - ${'True'} | ${'Unknown'} | ${'unknown'} - `( - 'renders $badgeType when status is $status and type is $type', - async ({ status, type, badgeType }) => { - createWrapper({ - apolloProvider: createApolloProviderWithKustomizations({ - result: { status, type, message: '' }, - }), - namespace: 'my-namespace', - }); - await waitForPromises(); - - const badge = SYNC_STATUS_BADGES[badgeType]; - - expect(findSyncBadge().text()).toBe(badge.text); - expect(findSyncBadge().props()).toMatchObject({ - icon: badge.icon, - variant: badge.variant, - }); - }, - ); - - it.each` - status | type | message | popoverTitle | popoverText - ${'True'} | ${'Stalled'} | ${'stalled reason'} | ${s__('Deployment|Flux sync stalled')} | ${'stalled reason'} - ${'True'} | ${'Reconciling'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciling')} - ${'True'} | ${'Ready'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciled successfully')} - ${'False'} | ${'Ready'} | ${'failed reason'} | ${s__('Deployment|Flux sync failed')} | ${'failed reason'} - ${'True'} | ${'Unknown'} | ${''} | ${s__('Deployment|Flux sync status is unknown')} | ${s__('Deployment|Unable to detect state. %{linkStart}How are states detected?%{linkEnd}')} - `( - 'renders correct popover text when status is $status and type is $type', - async ({ status, type, message, popoverTitle, popoverText }) => { - createWrapper({ - apolloProvider: createApolloProviderWithKustomizations({ - result: { status, type, message }, - }), - namespace: 'my-namespace', - }); - await waitForPromises(); - - expect(findPopover().text()).toMatchInterpolatedText(popoverText); - expect(findPopover().props('title')).toBe(popoverTitle); - }, - ); }); describe('when Flux API errored', () => { @@ -277,7 +194,8 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => { beforeEach(async () => { createWrapper({ apolloProvider: createApolloProviderWithErrors(), - namespace: 'my-namespace', + fluxResourcePath: + 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app', }); await waitForPromises(); }); diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb index f7597579e7a..a692d76da77 100644 --- a/spec/lib/atlassian/jira_connect/client_spec.rb +++ b/spec/lib/atlassian/jira_connect/client_spec.rb @@ -447,38 +447,77 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d end describe '#user_info' do - let(:account_id) { '12345' } - let(:response_body) do - { - groups: { - items: [ - { name: 'site-admins' } - ] - } - }.to_json - end + context 'when user is a site administrator' do + let(:account_id) { '12345' } + let(:response_body) do + { + groups: { + items: [ + { name: 'site-admins' } + ] + } + }.to_json + end - before do - stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups") - .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' }) - end + before do + stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups") + .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' }) + end - context 'with a successful response' do - let(:response_status) { 200 } + context 'with a successful response' do + let(:response_status) { 200 } - it 'returns a JiraUser instance' do - jira_user = client.user_info(account_id) + it 'returns a JiraUser instance' do + jira_user = client.user_info(account_id) - expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser) - expect(jira_user).to be_site_admin + expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser) + expect(jira_user).to be_jira_admin + end + end + + context 'with a failed response' do + let(:response_status) { 401 } + + it 'returns nil' do + expect(client.user_info(account_id)).to be_nil + end end end - context 'with a failed response' do - let(:response_status) { 401 } + context 'when user is an organization administrator' do + let(:account_id) { '12345' } + let(:response_body) do + { + groups: { + items: [ + { name: 'org-admins' } + ] + } + }.to_json + end - it 'returns nil' do - expect(client.user_info(account_id)).to be_nil + before do + stub_full_request("https://gitlab-test.atlassian.net/rest/api/3/user?accountId=#{account_id}&expand=groups") + .to_return(status: response_status, body: response_body, headers: { 'Content-Type': 'application/json' }) + end + + context 'with a successful response' do + let(:response_status) { 200 } + + it 'returns a JiraUser instance' do + jira_user = client.user_info(account_id) + + expect(jira_user).to be_a(Atlassian::JiraConnect::JiraUser) + expect(jira_user).to be_jira_admin + end + end + + context 'with a failed response' do + let(:response_status) { 401 } + + it 'returns nil' do + expect(client.user_info(account_id)).to be_nil + end end end end diff --git a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb index 1ff157b51d4..b0384a37746 100644 --- a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_category: :database do let(:migration) do Class - .new + .new(Gitlab::Database::Migration[2.1]) .include(described_class) .include(Gitlab::Database::MigrationHelpers) .new @@ -73,4 +73,135 @@ RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_cate expect(migration.columns_swapped?(:test_table, :id)).to eq(false) end end + + describe '#add_bigint_column_indexes' do + let(:connection) { migration.connection } + + let(:table_name) { '_test_table_bigint_indexes' } + let(:int_column) { 'token' } + let(:bigint_column) { 'token_convert_to_bigint' } + + subject(:add_bigint_column_indexes) { migration.add_bigint_column_indexes(table_name, int_column) } + + before do + connection.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS public.#{table_name} ( + name varchar(40), + #{int_column} integer + ); + SQL + + allow(migration).to receive(:transaction_open?).and_return(false) + allow(migration).to receive(:disable_statement_timeout).and_call_original + end + + after do + connection.execute("DROP TABLE IF EXISTS #{table_name}") + end + + context 'without corresponding bigint column' do + let(:error_msg) { "Bigint column '#{bigint_column}' does not exist on #{table_name}" } + + it { expect { subject }.to raise_error(RuntimeError, error_msg) } + end + + context 'with corresponding bigint column' do + let(:indexes) { connection.indexes(table_name) } + let(:int_column_indexes) { indexes.select { |i| i.columns.include?(int_column) } } + let(:bigint_column_indexes) { indexes.select { |i| i.columns.include?(bigint_column) } } + + before do + connection.execute("ALTER TABLE #{table_name} ADD COLUMN #{bigint_column} bigint") + end + + context 'without the integer column index' do + it 'does not create new bigint index' do + expect(int_column_indexes).to be_empty + + add_bigint_column_indexes + + expect(bigint_column_indexes).to be_empty + end + end + + context 'with integer column indexes' do + let(:bigint_index_name) { ->(int_index_name) { migration.bigint_index_name(int_index_name) } } + let(:expected_bigint_indexes) do + [ + { + name: bigint_index_name.call("hash_idx_#{table_name}"), + column: [bigint_column], + using: 'hash' + }, + { + name: bigint_index_name.call("idx_#{table_name}"), + column: [bigint_column], + using: 'btree' + }, + { + name: bigint_index_name.call("idx_#{table_name}_combined"), + column: "#{bigint_column}, lower((name)::text)", + where: "(#{bigint_column} IS NOT NULL)", + using: 'btree' + }, + { + name: bigint_index_name.call("idx_#{table_name}_functional"), + column: "#{bigint_column}, lower((name)::text)", + using: 'btree' + }, + { + name: bigint_index_name.call("idx_#{table_name}_ordered"), + column: [bigint_column], + order: 'DESC NULLS LAST', + using: 'btree' + }, + { + name: bigint_index_name.call("idx_#{table_name}_ordered_multiple"), + column: [bigint_column, 'name'], + order: { bigint_column => 'DESC NULLS LAST', 'name' => 'desc' }, + using: 'btree' + }, + { + name: bigint_index_name.call("idx_#{table_name}_partial"), + column: [bigint_column], + where: "(#{bigint_column} IS NOT NULL)", + using: 'btree' + }, + { + name: bigint_index_name.call("uniq_idx_#{table_name}"), + column: [bigint_column], + unique: true, + using: 'btree' + } + ] + end + + before do + connection.execute(<<~SQL) + CREATE INDEX "hash_idx_#{table_name}" ON #{table_name} USING hash (#{int_column}); + CREATE INDEX "idx_#{table_name}" ON #{table_name} USING btree (#{int_column}); + CREATE INDEX "idx_#{table_name}_combined" ON #{table_name} USING btree (#{int_column}, lower((name)::text)) WHERE (#{int_column} IS NOT NULL); + CREATE INDEX "idx_#{table_name}_functional" ON #{table_name} USING btree (#{int_column}, lower((name)::text)); + CREATE INDEX "idx_#{table_name}_ordered" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST); + CREATE INDEX "idx_#{table_name}_ordered_multiple" ON #{table_name} USING btree (#{int_column} DESC NULLS LAST, name DESC); + CREATE INDEX "idx_#{table_name}_partial" ON #{table_name} USING btree (#{int_column}) WHERE (#{int_column} IS NOT NULL); + CREATE UNIQUE INDEX "uniq_idx_#{table_name}" ON #{table_name} USING btree (#{int_column}); + SQL + end + + it 'creates appropriate bigint indexes' do + expected_bigint_indexes.each do |bigint_index| + expect(migration).to receive(:add_concurrent_index).with( + table_name, + bigint_index[:column], + name: bigint_index[:name], + ** bigint_index.except(:name, :column) + ) + end + + add_bigint_column_indexes + end + end + end + end end diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb index 64c4e92f80b..8f676d20c22 100644 --- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -4,11 +4,6 @@ require 'fast_spec_helper' RSpec.describe Gitlab::HealthChecks::GitalyCheck do let(:result_class) { Gitlab::HealthChecks::Result } - let(:repository_storages) { ['default'] } - - before do - allow(described_class).to receive(:repository_storages) { repository_storages } - end describe '#readiness' do subject { described_class.readiness } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f2f79311fdf..a2d6c60fbd0 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -822,15 +822,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do subject { setting } end - # Upgraded databases will have this sort of content - context 'repository_storages is a String, not an Array' do - before do - described_class.where(id: setting.id).update_all(repository_storages: 'default') - end - - it { expect(setting.repository_storages).to eq(['default']) } - end - context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do @@ -867,31 +858,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do end end - context 'repository storages' do - before do - storages = { - 'custom1' => 'tmp/tests/custom_repositories_1', - 'custom2' => 'tmp/tests/custom_repositories_2', - 'custom3' => 'tmp/tests/custom_repositories_3' - - } - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - end - - describe 'inclusion' do - it { is_expected.to allow_value('custom1').for(:repository_storages) } - it { is_expected.to allow_value(%w[custom2 custom3]).for(:repository_storages) } - it { is_expected.not_to allow_value('alternative').for(:repository_storages) } - it { is_expected.not_to allow_value(%w[alternative custom1]).for(:repository_storages) } - end - - describe 'presence' do - it { is_expected.not_to allow_value([]).for(:repository_storages) } - it { is_expected.not_to allow_value("").for(:repository_storages) } - it { is_expected.not_to allow_value(nil).for(:repository_storages) } - end - end - context 'housekeeping settings' do it { is_expected.not_to allow_value(0).for(:housekeeping_optimize_repository_period) } end diff --git a/spec/services/ci/catalog/resources/release_service_spec.rb b/spec/services/ci/catalog/resources/release_service_spec.rb index 1901485d402..60cd6cb5f96 100644 --- a/spec/services/ci/catalog/resources/release_service_spec.rb +++ b/spec/services/ci/catalog/resources/release_service_spec.rb @@ -31,7 +31,9 @@ RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeli expect(Ci::Catalog::Resources::Version.count).to be(0) expect(response).to be_error - expect(response.message).to eq('Project must have a description, Project must contain components') + expect(response.message).to eq( + 'Project must have a description, ' \ + 'Project must contain components. Ensure you are using the correct directory structure') end end diff --git a/spec/services/ci/catalog/resources/validate_service_spec.rb b/spec/services/ci/catalog/resources/validate_service_spec.rb index 597451db486..39ab758d78d 100644 --- a/spec/services/ci/catalog/resources/validate_service_spec.rb +++ b/spec/services/ci/catalog/resources/validate_service_spec.rb @@ -19,7 +19,9 @@ RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipel response = described_class.new(project, project.default_branch).execute expect(response.message).to eq( - 'Project must have a README, Project must have a description, Project must contain components') + 'Project must have a README, ' \ + 'Project must have a description, ' \ + 'Project must contain components. Ensure you are using the correct directory structure') end end @@ -37,7 +39,9 @@ RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipel project = create(:project, :small_repo, description: 'project with no README and no components') response = described_class.new(project, project.default_branch).execute - expect(response.message).to eq('Project must have a README, Project must contain components') + expect(response.message).to eq( + 'Project must have a README, ' \ + 'Project must contain components. Ensure you are using the correct directory structure') end end @@ -46,7 +50,9 @@ RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipel project = create(:project, :repository) response = described_class.new(project, project.default_branch).execute - expect(response.message).to eq('Project must have a description, Project must contain components') + expect(response.message).to eq( + 'Project must have a description, ' \ + 'Project must contain components. Ensure you are using the correct directory structure') end end @@ -74,7 +80,8 @@ RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipel project = create(:project, :readme, description: 'project with no README and no components') response = described_class.new(project, project.default_branch).execute - expect(response.message).to eq('Project must contain components') + expect(response.message).to eq( + 'Project must contain components. Ensure you are using the correct directory structure') end end end diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb index f9d3954b84c..2296d0fbfed 100644 --- a/spec/services/jira_connect_subscriptions/create_service_spec.rb +++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ let(:path) { group.full_path } let(:params) { { namespace_path: path, jira_user: jira_user } } - let(:jira_user) { double(:JiraUser, site_admin?: true) } + let(:jira_user) { double(:JiraUser, jira_admin?: true) } subject { described_class.new(installation, current_user, params).execute } @@ -29,11 +29,11 @@ RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integ end context 'remote user does not have access' do - let(:jira_user) { double(site_admin?: false) } + let(:jira_user) { double(jira_admin?: false) } it_behaves_like 'a failed execution', http_status: 403, - message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.' + message: 'The Jira user is not a site or organization administrator. Check the permissions in Jira and try again.' end context 'remote user cannot be retrieved' do diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index b28d7549fbb..3504f00412c 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -71,7 +71,10 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) - expect(result[:message]).to eq('Project must have a description, Project must contain components') + expect(result[:http_status]).to eq(422) + expect(result[:message]).to eq( + 'Project must have a description, ' \ + 'Project must contain components. Ensure you are using the correct directory structure') end end end @@ -105,6 +108,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(403) end end @@ -140,6 +144,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio it 'raises an error and does not update the release' do result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(409) expect(project.releases.find_by(tag: tag_name).description).to eq(description) end end @@ -151,6 +156,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(400) expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}") end @@ -160,6 +166,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(400) expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}") end end @@ -245,6 +252,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(400) expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_title}") end @@ -261,6 +269,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio result = service.execute expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(400) expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}") end end diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml index bb2802ba6d2..b4532ca7d7a 100644 --- a/spec/support/helpers/database/duplicate_indexes.yml +++ b/spec/support/helpers/database/duplicate_indexes.yml @@ -2,255 +2,253 @@ # It maps table_name to {index1: array_of_duplicate_indexes, index2: array_of_duplicate_indexes, ... } abuse_reports: idx_abuse_reports_user_id_status_and_category: - - index_abuse_reports_on_user_id + - index_abuse_reports_on_user_id alert_management_http_integrations: index_http_integrations_on_project_and_endpoint: - - index_alert_management_http_integrations_on_project_id + - index_alert_management_http_integrations_on_project_id approval_project_rules_users: index_approval_project_rules_users_1: - - index_approval_project_rules_users_on_approval_project_rule_id + - index_approval_project_rules_users_on_approval_project_rule_id approvals: index_approvals_on_merge_request_id_and_created_at: - - index_approvals_on_merge_request_id + - index_approvals_on_merge_request_id board_group_recent_visits: index_board_group_recent_visits_on_user_group_and_board: - - index_board_group_recent_visits_on_user_id + - index_board_group_recent_visits_on_user_id board_project_recent_visits: index_board_project_recent_visits_on_user_project_and_board: - - index_board_project_recent_visits_on_user_id + - index_board_project_recent_visits_on_user_id board_user_preferences: index_board_user_preferences_on_user_id_and_board_id: - - index_board_user_preferences_on_user_id + - index_board_user_preferences_on_user_id boards_epic_board_recent_visits: index_epic_board_recent_visits_on_user_group_and_board: - - index_boards_epic_board_recent_visits_on_user_id + - index_boards_epic_board_recent_visits_on_user_id boards_epic_user_preferences: index_boards_epic_user_preferences_on_board_user_epic_unique: - - index_boards_epic_user_preferences_on_board_id + - index_boards_epic_user_preferences_on_board_id bulk_import_batch_trackers: i_bulk_import_trackers_id_batch_number: - - index_bulk_import_batch_trackers_on_tracker_id + - index_bulk_import_batch_trackers_on_tracker_id bulk_import_export_batches: i_bulk_import_export_batches_id_batch_number: - - index_bulk_import_export_batches_on_export_id + - index_bulk_import_export_batches_on_export_id ci_job_artifacts: index_ci_job_artifacts_on_id_project_id_and_created_at: - - index_ci_job_artifacts_on_project_id + - index_ci_job_artifacts_on_project_id index_ci_job_artifacts_on_id_project_id_and_file_type: - - index_ci_job_artifacts_on_project_id + - index_ci_job_artifacts_on_project_id index_ci_job_artifacts_on_project_id_and_id: - - index_ci_job_artifacts_on_project_id + - index_ci_job_artifacts_on_project_id ci_pipeline_artifacts: index_ci_pipeline_artifacts_on_pipeline_id_and_file_type: - - index_ci_pipeline_artifacts_on_pipeline_id + - index_ci_pipeline_artifacts_on_pipeline_id ci_stages: index_ci_stages_on_pipeline_id_and_name: - - index_ci_stages_on_pipeline_id + - index_ci_stages_on_pipeline_id index_ci_stages_on_pipeline_id_and_position: - - index_ci_stages_on_pipeline_id + - index_ci_stages_on_pipeline_id index_ci_stages_on_pipeline_id_convert_to_bigint_and_name: - - index_ci_stages_on_pipeline_id_convert_to_bigint + - index_ci_stages_on_pipeline_id_convert_to_bigint index_ci_stages_on_pipeline_id_convert_to_bigint_and_position: - - index_ci_stages_on_pipeline_id_convert_to_bigint + - index_ci_stages_on_pipeline_id_convert_to_bigint dast_site_tokens: index_dast_site_token_on_project_id_and_url: - - index_dast_site_tokens_on_project_id + - index_dast_site_tokens_on_project_id design_management_designs: index_design_management_designs_on_iid_and_project_id: - - index_design_management_designs_on_project_id + - index_design_management_designs_on_project_id design_management_designs_versions: design_management_designs_versions_uniqueness: - - index_design_management_designs_versions_on_design_id + - index_design_management_designs_versions_on_design_id error_tracking_errors: index_et_errors_on_project_id_and_status_and_id: - - index_error_tracking_errors_on_project_id + - index_error_tracking_errors_on_project_id index_et_errors_on_project_id_and_status_events_count_id_desc: - - index_error_tracking_errors_on_project_id + - index_error_tracking_errors_on_project_id index_et_errors_on_project_id_and_status_first_seen_at_id_desc: - - index_error_tracking_errors_on_project_id + - index_error_tracking_errors_on_project_id index_et_errors_on_project_id_and_status_last_seen_at_id_desc: - - index_error_tracking_errors_on_project_id + - index_error_tracking_errors_on_project_id geo_node_namespace_links: index_geo_node_namespace_links_on_geo_node_id_and_namespace_id: - - index_geo_node_namespace_links_on_geo_node_id + - index_geo_node_namespace_links_on_geo_node_id in_product_marketing_emails: - index_in_product_marketing_emails_on_user_campaign: - - index_in_product_marketing_emails_on_user_id index_in_product_marketing_emails_on_user_track_series: - - index_in_product_marketing_emails_on_user_id + - index_in_product_marketing_emails_on_user_id incident_management_oncall_participants: index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id: - - index_inc_mgmnt_oncall_participants_on_oncall_user_id + - index_inc_mgmnt_oncall_participants_on_oncall_user_id incident_management_oncall_schedules: index_im_oncall_schedules_on_project_id_and_iid: - - index_incident_management_oncall_schedules_on_project_id + - index_incident_management_oncall_schedules_on_project_id instance_audit_events_streaming_headers: idx_instance_external_audit_event_destination_id_key_uniq: - - idx_headers_instance_external_audit_event_destination_id + - idx_headers_instance_external_audit_event_destination_id issue_links: index_issue_links_on_source_id_and_target_id: - - index_issue_links_on_source_id + - index_issue_links_on_source_id issues: index_issues_on_author_id_and_id_and_created_at: - - index_issues_on_author_id + - index_issues_on_author_id jira_connect_subscriptions: idx_jira_connect_subscriptions_on_installation_id_namespace_id: - - idx_jira_connect_subscriptions_on_installation_id + - idx_jira_connect_subscriptions_on_installation_id list_user_preferences: index_list_user_preferences_on_user_id_and_list_id: - - index_list_user_preferences_on_user_id + - index_list_user_preferences_on_user_id member_tasks: index_member_tasks_on_member_id_and_project_id: - - index_member_tasks_on_member_id + - index_member_tasks_on_member_id members: index_members_on_member_namespace_id_compound: - - index_members_on_member_namespace_id + - index_members_on_member_namespace_id merge_request_assignees: index_merge_request_assignees_on_merge_request_id_and_user_id: - - index_merge_request_assignees_on_merge_request_id + - index_merge_request_assignees_on_merge_request_id merge_requests: index_merge_requests_on_author_id_and_created_at: - - index_merge_requests_on_author_id + - index_merge_requests_on_author_id index_merge_requests_on_author_id_and_id: - - index_merge_requests_on_author_id + - index_merge_requests_on_author_id index_merge_requests_on_author_id_and_target_project_id: - - index_merge_requests_on_author_id + - index_merge_requests_on_author_id ml_candidate_params: index_ml_candidate_params_on_candidate_id_on_name: - - index_ml_candidate_params_on_candidate_id + - index_ml_candidate_params_on_candidate_id ml_candidates: index_ml_candidates_on_project_id_on_internal_id: - - index_ml_candidates_on_project_id + - index_ml_candidates_on_project_id ml_model_versions: index_ml_model_versions_on_project_id_and_model_id_and_version: - - index_ml_model_versions_on_project_id + - index_ml_model_versions_on_project_id ml_models: index_ml_models_on_project_id_and_name: - - index_ml_models_on_project_id + - index_ml_models_on_project_id p_ci_runner_machine_builds: index_p_ci_runner_machine_builds_on_runner_machine_id: - - index_ci_runner_machine_builds_on_runner_machine_id + - index_ci_runner_machine_builds_on_runner_machine_id packages_debian_group_distributions: uniq_pkgs_debian_group_distributions_group_id_and_codename: - - index_packages_debian_group_distributions_on_group_id + - index_packages_debian_group_distributions_on_group_id uniq_pkgs_debian_group_distributions_group_id_and_suite: - - index_packages_debian_group_distributions_on_group_id + - index_packages_debian_group_distributions_on_group_id packages_debian_project_distributions: uniq_pkgs_debian_project_distributions_project_id_and_codename: - - index_packages_debian_project_distributions_on_project_id + - index_packages_debian_project_distributions_on_project_id uniq_pkgs_debian_project_distributions_project_id_and_suite: - - index_packages_debian_project_distributions_on_project_id + - index_packages_debian_project_distributions_on_project_id packages_tags: index_packages_tags_on_package_id_and_updated_at: - - index_packages_tags_on_package_id + - index_packages_tags_on_package_id pages_domains: index_pages_domains_on_project_id_and_enabled_until: - - index_pages_domains_on_project_id + - index_pages_domains_on_project_id index_pages_domains_on_verified_at_and_enabled_until: - - index_pages_domains_on_verified_at + - index_pages_domains_on_verified_at personal_access_tokens: index_pat_on_user_id_and_expires_at: - - index_personal_access_tokens_on_user_id + - index_personal_access_tokens_on_user_id pm_affected_packages: i_affected_packages_unique_for_upsert: - - index_pm_affected_packages_on_pm_advisory_id + - index_pm_affected_packages_on_pm_advisory_id pm_package_version_licenses: i_pm_package_version_licenses_join_ids: - - index_pm_package_version_licenses_on_pm_package_version_id + - index_pm_package_version_licenses_on_pm_package_version_id pm_package_versions: i_pm_package_versions_on_package_id_and_version: - - index_pm_package_versions_on_pm_package_id + - index_pm_package_versions_on_pm_package_id project_compliance_standards_adherence: u_project_compliance_standards_adherence_for_reporting: - - index_project_compliance_standards_adherence_on_project_id + - index_project_compliance_standards_adherence_on_project_id project_relation_exports: index_project_export_job_relation: - - index_project_relation_exports_on_project_export_job_id + - index_project_relation_exports_on_project_export_job_id project_repositories: index_project_repositories_on_shard_id_and_project_id: - - index_project_repositories_on_shard_id + - index_project_repositories_on_shard_id project_topics: index_project_topics_on_project_id_and_topic_id: - - index_project_topics_on_project_id + - index_project_topics_on_project_id projects: index_projects_api_path_id_desc: - - index_on_projects_path + - index_on_projects_path index_projects_on_path_and_id: - - index_on_projects_path + - index_on_projects_path protected_environments: index_protected_environments_on_project_id_and_name: - - index_protected_environments_on_project_id + - index_protected_environments_on_project_id protected_tags: index_protected_tags_on_project_id_and_name: - - index_protected_tags_on_project_id + - index_protected_tags_on_project_id related_epic_links: index_related_epic_links_on_source_id_and_target_id: - - index_related_epic_links_on_source_id + - index_related_epic_links_on_source_id requirements_management_test_reports: idx_test_reports_on_issue_id_created_at_and_id: - - index_requirements_management_test_reports_on_issue_id + - index_requirements_management_test_reports_on_issue_id sbom_component_versions: index_sbom_component_versions_on_component_id_and_version: - - index_sbom_component_versions_on_component_id + - index_sbom_component_versions_on_component_id sbom_occurrences: index_sbom_occurrences_for_input_file_path_search: - - index_sbom_occurrences_on_project_id_component_id - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id_component_id + - index_sbom_occurrences_on_project_id idx_sbom_occurrences_on_project_id_and_source_id: - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id index_sbom_occurrences_on_project_id_and_id: - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id index_sbom_occurrences_on_project_id_component_id: - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id index_sbom_occurrences_on_project_id_and_component_id_and_id: - - index_sbom_occurrences_on_project_id_component_id - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id_component_id + - index_sbom_occurrences_on_project_id index_sbom_occurrences_on_project_id_and_package_manager: - - index_sbom_occurrences_on_project_id + - index_sbom_occurrences_on_project_id search_namespace_index_assignments: index_search_namespace_index_assignments_uniqueness_index_type: - - index_search_namespace_index_assignments_on_namespace_id + - index_search_namespace_index_assignments_on_namespace_id index_search_namespace_index_assignments_uniqueness_on_index_id: - - index_search_namespace_index_assignments_on_namespace_id + - index_search_namespace_index_assignments_on_namespace_id sprints: sequence_is_unique_per_iterations_cadence_id: - - index_sprints_iterations_cadence_id + - index_sprints_iterations_cadence_id taggings: taggings_idx: - - index_taggings_on_tag_id + - index_taggings_on_tag_id term_agreements: term_agreements_unique_index: - - index_term_agreements_on_user_id + - index_term_agreements_on_user_id todos: index_todos_on_author_id_and_created_at: - - index_todos_on_author_id + - index_todos_on_author_id user_callouts: index_user_callouts_on_user_id_and_feature_name: - - index_user_callouts_on_user_id + - index_user_callouts_on_user_id users: index_users_on_state_and_user_type: - - index_users_on_state + - index_users_on_state vulnerabilities: index_vulnerabilities_project_id_state_severity_default_branch: - - index_vulnerabilities_on_project_id_and_state_and_severity + - index_vulnerabilities_on_project_id_and_state_and_severity vulnerability_external_issue_links: idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue: - - index_vulnerability_external_issue_links_on_vulnerability_id + - index_vulnerability_external_issue_links_on_vulnerability_id vulnerability_finding_links: finding_link_name_url_idx: - - finding_links_on_vulnerability_occurrence_id + - finding_links_on_vulnerability_occurrence_id vulnerability_finding_signatures: idx_vuln_signatures_uniqueness_signature_sha: - - index_vulnerability_finding_signatures_on_finding_id + - index_vulnerability_finding_signatures_on_finding_id vulnerability_flags: index_vulnerability_flags_on_unique_columns: - - index_vulnerability_flags_on_vulnerability_occurrence_id + - index_vulnerability_flags_on_vulnerability_occurrence_id web_hook_logs: index_web_hook_logs_on_web_hook_id_and_created_at: - - index_web_hook_logs_part_on_web_hook_id + - index_web_hook_logs_part_on_web_hook_id web_hooks: index_web_hooks_on_project_id_recent_failures: - - index_web_hooks_on_project_id + - index_web_hooks_on_project_id work_item_hierarchy_restrictions: index_work_item_hierarchy_restrictions_on_parent_and_child: - - index_work_item_hierarchy_restrictions_on_parent_type_id + - index_work_item_hierarchy_restrictions_on_parent_type_id