Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-09 12:10:06 +00:00
parent 529d3153bc
commit 963e09aecf
52 changed files with 626 additions and 570 deletions

View File

@ -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.'),
});
});
}

View File

@ -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"
>
<gl-tab
:query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
@click="setScope($options.ENVIRONMENTS_SCOPE.AVAILABLE)"
:query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
@click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
>
<template #title>
<span>{{ $options.i18n.available }}</span>
<span>{{ $options.i18n.active }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
{{ availableCount }}
{{ activeCount }}
</gl-badge>
</template>
</gl-tab>

View File

@ -111,7 +111,6 @@ export default {
<kubernetes-status-bar
:cluster-health-status="clusterHealthStatus"
:configuration="k8sAccessConfiguration"
:namespace="namespace"
:environment-name="environmentName"
:flux-resource-path="fluxResourcePath"
class="gl-mb-3" />

View File

@ -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) {

View File

@ -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',
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,6 @@
query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
folder(environment: $environment, scope: $scope, search: $search) @client {
availableCount
activeCount
environments
stoppedCount
}

View File

@ -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',

View File

@ -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,
});
},

View File

@ -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 ||

View File

@ -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 }

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
c8dbdeb4ffcb7f5dc1c719a09a1f6c41188f584c80331a4482542a873d3ad12d

View File

@ -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);

View File

@ -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

View File

@ -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: <your_access_token>" \

View File

@ -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`. |

View File

@ -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:

View File

@ -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).

View File

@ -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

View File

@ -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`.

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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 () => {

View File

@ -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,

View File

@ -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 },
);

View File

@ -122,7 +122,6 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
expect(findKubernetesStatusBar().props()).toEqual({
clusterHealthStatus: 'success',
configuration,
namespace: kubernetesNamespace,
environmentName: resolvedEnvironment.name,
fluxResourcePath: fluxResourcePathMock,
});

View File

@ -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();
});

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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