Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
73fe31a692
commit
256c5ea115
|
|
@ -463,7 +463,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
|
|||
/doc/administration/pages/ @ashrafkhamis
|
||||
/doc/administration/polling.md @axil
|
||||
/doc/administration/postgresql/ @aqualls
|
||||
/doc/administration/postgresql/multiple_databases.md @jglassman1
|
||||
/doc/administration/postgresql/multiple_databases.md @lciutacu
|
||||
/doc/administration/raketasks/ @axil
|
||||
/doc/administration/raketasks/ldap.md @jglassman1
|
||||
/doc/administration/raketasks/praefect.md @eread
|
||||
|
|
@ -710,7 +710,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
|
|||
/doc/development/contributing/ @sselhorn
|
||||
/doc/development/database/ @aqualls
|
||||
/doc/development/database/filtering_by_label.md @msedlakjakubowski
|
||||
/doc/development/database/multiple_databases.md @jglassman1
|
||||
/doc/development/database/multiple_databases.md @lciutacu
|
||||
/doc/development/database_review.md @aqualls
|
||||
/doc/development/developing_with_solargraph.md @aqualls
|
||||
/doc/development/development_processes.md @sselhorn
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<!--
|
||||
Merging into stable branches is reserved for GitLab patch releases
|
||||
https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases
|
||||
Merging into stable branches in canonical projects is reserved for
|
||||
GitLab patch releases https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases
|
||||
|
||||
If you're backporting a security fix, please refer to the security merge request
|
||||
template https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md.
|
||||
Security backport merge requests should not be opened on this project.
|
||||
-->
|
||||
|
||||
## What does this MR do and why?
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
37fd51c54395a06dc39322606f9a3e4ba2dafa3d
|
||||
ac7304c0ce981bf97dca39ac1017b31860c043b5
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export default {
|
|||
<gl-badge
|
||||
v-gl-tooltip
|
||||
variant="warning"
|
||||
size="sm"
|
||||
class="gl-mr-2"
|
||||
:title="__('Pending comments are hidden until you submit your review.')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default {
|
|||
defaultFetchOptions: {
|
||||
exclude_internal: true,
|
||||
active: true,
|
||||
order_by: 'similarity',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink } from '@gitlab/ui';
|
||||
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink, GlAlert } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
|
@ -17,6 +17,7 @@ export default {
|
|||
GlModal,
|
||||
GlToggle,
|
||||
GlLink,
|
||||
GlAlert,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
|
|
@ -27,6 +28,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
modalId: 'delete-self-monitor-modal',
|
||||
showDeprecationNotice: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -49,6 +51,20 @@ export default {
|
|||
selfMonitorProjectFullUrl() {
|
||||
return `${getBaseURL()}/${this.projectPath}`;
|
||||
},
|
||||
selfMonitoringDeprecationNotice() {
|
||||
return sprintf(
|
||||
s__(
|
||||
'SelfMonitoring|Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}.',
|
||||
),
|
||||
{
|
||||
deprecation: `<a href="${this.deprecationPath}">`,
|
||||
removal: `<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909">`,
|
||||
opstrace: `<a href="https://gitlab.com/groups/gitlab-org/-/epics/6976">`,
|
||||
link_end: `</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
selfMonitoringFormText() {
|
||||
if (this.projectCreated) {
|
||||
return sprintf(
|
||||
|
|
@ -70,6 +86,9 @@ export default {
|
|||
helpDocsPath() {
|
||||
return helpPagePath('administration/monitoring/gitlab_self_monitoring_project/index');
|
||||
},
|
||||
deprecationPath() {
|
||||
return helpPagePath('update/deprecations.md', { anchor: 'gitlab-self-monitoring-project' });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selfMonitorEnabled() {
|
||||
|
|
@ -123,6 +142,9 @@ export default {
|
|||
viewSelfMonitorProject() {
|
||||
visitUrl(this.selfMonitorProjectFullUrl);
|
||||
},
|
||||
hideDeprecationNotice() {
|
||||
this.showDeprecationNotice = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -140,6 +162,16 @@ export default {
|
|||
<gl-link :href="helpDocsPath">{{ __('Learn more.') }}</gl-link>
|
||||
</p>
|
||||
</div>
|
||||
<gl-alert
|
||||
v-if="showDeprecationNotice"
|
||||
class="gl-mb-3"
|
||||
:title="s__('SelfMonitoring|Deprecation notice')"
|
||||
:dismissible="true"
|
||||
variant="danger"
|
||||
@dismiss="hideDeprecationNotice"
|
||||
>
|
||||
<div v-safe-html="selfMonitoringDeprecationNotice"></div>
|
||||
</gl-alert>
|
||||
<div class="settings-content">
|
||||
<form name="self-monitoring-form">
|
||||
<p ref="selfMonitoringFormText" v-safe-html="selfMonitoringFormText"></p>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ module Mutations
|
|||
|
||||
def resolve(project_path:, target_branch:)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
return respond(nil, ['Feature flag is disabled']) unless Feature.enabled?(:synchronize_fork, project)
|
||||
|
||||
details_resolver = Resolvers::Projects::ForkDetailsResolver.new(object: project, context: context, field: nil)
|
||||
details = details_resolver.resolve(ref: target_branch)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ module NavHelper
|
|||
def super_sidebar_supported?
|
||||
return true if @nav.nil?
|
||||
|
||||
%w(your_work project group).include?(@nav)
|
||||
%w(your_work project group profile).include?(@nav)
|
||||
end
|
||||
|
||||
def get_header_links
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ module SidebarsHelper
|
|||
when 'group'
|
||||
context = group_sidebar_context(group, user, **context_adds)
|
||||
Sidebars::Groups::SuperSidebarPanel.new(context)
|
||||
when 'profile'
|
||||
context = Sidebars::Context.new(current_user: user, container: user, **context_adds)
|
||||
Sidebars::UserSettings::Panel.new(context)
|
||||
else
|
||||
context = your_work_sidebar_context(user, **context_adds)
|
||||
Sidebars::YourWork::Panel.new(context)
|
||||
|
|
|
|||
|
|
@ -1,169 +1 @@
|
|||
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(current_user), 'aria-label': _('User settings') }
|
||||
.nav-sidebar-inner-scroll
|
||||
.context-header
|
||||
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 32, alt: current_user.name, class: 'gl-mr-3 js-sidebar-user-avatar', avatar_options: { data: { testid: 'sidebar-user-avatar' } })
|
||||
%span.sidebar-context-title= _('User Settings')
|
||||
%ul.sidebar-top-level-items
|
||||
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
|
||||
= link_to profile_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('profile')
|
||||
%span.nav-item-name
|
||||
= _('Profile')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Profile')
|
||||
= nav_link(controller: [:accounts, :two_factor_auths]) do
|
||||
= link_to profile_account_path, data: { qa_selector: 'profile_account_link' } do
|
||||
.nav-icon-container
|
||||
= sprite_icon('account')
|
||||
%span.nav-item-name
|
||||
= _('Account')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_account_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Account')
|
||||
|
||||
= render_if_exists 'layouts/nav/sidebar/profile_billing_link'
|
||||
= nav_link(controller: 'oauth/applications') do
|
||||
= link_to applications_profile_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('applications')
|
||||
%span.nav-item-name
|
||||
= _('Applications')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to applications_profile_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Applications')
|
||||
= nav_link(controller: :chat_names) do
|
||||
= link_to profile_chat_names_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('comment')
|
||||
%span.nav-item-name
|
||||
= _('Chat')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_chat_names_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Chat')
|
||||
- unless Gitlab::CurrentSettings.personal_access_tokens_disabled?
|
||||
= nav_link(controller: :personal_access_tokens) do
|
||||
= link_to profile_personal_access_tokens_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('token')
|
||||
%span.nav-item-name
|
||||
= _('Access Tokens')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_personal_access_tokens_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Access Tokens')
|
||||
= nav_link(controller: :emails) do
|
||||
= link_to profile_emails_path, data: { qa_selector: 'profile_emails_link' } do
|
||||
.nav-icon-container
|
||||
= sprite_icon('mail')
|
||||
%span.nav-item-name
|
||||
= _('Emails')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :emails, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_emails_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Emails')
|
||||
- if current_user.allow_password_authentication?
|
||||
= nav_link(controller: :passwords) do
|
||||
= link_to edit_profile_password_path , data: { qa_selector: 'profile_password_link' } do
|
||||
.nav-icon-container
|
||||
= sprite_icon('lock')
|
||||
%span.nav-item-name
|
||||
= _('Password')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to edit_profile_password_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Password')
|
||||
= nav_link(controller: :notifications) do
|
||||
= link_to profile_notifications_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('notifications')
|
||||
%span.nav-item-name
|
||||
= _('Notifications')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_notifications_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Notifications')
|
||||
= nav_link(controller: :keys) do
|
||||
= link_to profile_keys_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('key')
|
||||
%span.nav-item-name
|
||||
= _('SSH Keys')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :keys, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_keys_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('SSH Keys')
|
||||
= nav_link(controller: :gpg_keys) do
|
||||
= link_to profile_gpg_keys_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('key')
|
||||
%span.nav-item-name
|
||||
= _('GPG Keys')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_gpg_keys_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('GPG Keys')
|
||||
= nav_link(controller: :preferences) do
|
||||
= link_to profile_preferences_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('preferences')
|
||||
%span.nav-item-name
|
||||
= _('Preferences')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_preferences_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Preferences')
|
||||
- if saved_replies_enabled?
|
||||
= nav_link(controller: :saved_replies) do
|
||||
= link_to profile_saved_replies_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('symlink')
|
||||
%span.nav-item-name
|
||||
= _('Saved Replies')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :saved_replies, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_saved_replies_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Saved Replies')
|
||||
= nav_link(controller: :active_sessions) do
|
||||
= link_to profile_active_sessions_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('monitor-lines')
|
||||
%span.nav-item-name
|
||||
= _('Active Sessions')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to profile_active_sessions_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Active Sessions')
|
||||
= nav_link(path: 'profiles#audit_log') do
|
||||
= link_to audit_log_profile_path do
|
||||
.nav-icon-container
|
||||
= sprite_icon('log')
|
||||
%span.nav-item-name
|
||||
= _('Authentication log')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" }) do
|
||||
= link_to audit_log_profile_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Authentication Log')
|
||||
= render_if_exists 'layouts/nav/sidebar/profile_usage_quotas_link'
|
||||
|
||||
= render 'shared/sidebar_toggle_button'
|
||||
= render partial: 'shared/nav/sidebar', object: Sidebars::UserSettings::Panel.new(Sidebars::Context.new(current_user: current_user, container: current_user))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
%li.context-header
|
||||
= link_to profile_path, title: _('User Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 32, alt: current_user.name, class: 'gl-mr-3 js-sidebar-user-avatar', avatar_options: { data: { testid: 'sidebar-user-avatar' } })
|
||||
%span.sidebar-context-title= _('User Settings')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: reduce_sub_batch_size_on_timeouts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109354
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393556
|
||||
milestone: '15.10'
|
||||
type: development
|
||||
group: group::database
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: synchronize_fork
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114299
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395793
|
||||
milestone: '15.10'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -192,7 +192,7 @@ accordingly, while also consulting the
|
|||
- GitLab 12: `12.0.12` > [`12.1.17`](#1210) > [`12.10.14`](#12100)
|
||||
- GitLab 13: `13.0.14` > [`13.1.11`](#1310) > [`13.8.8`](#1388) > [`13.12.15`](#13120)
|
||||
- GitLab 14: [`14.0.12`](#1400) > [`14.3.6`](#1430) > [`14.9.5`](#1490) > [`14.10.5`](#14100)
|
||||
- GitLab 15: [`15.0.5`](#1500) > [`15.1.6`](#1510) (for GitLab instances with multiple web nodes) > [`15.4.6`](#1540) > [`15.6.x, 15.7.x or 15.8.x`](#user-profile-data-loss-bug-in-159x) > [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
|
||||
- GitLab 15: [`15.0.5`](#1500) > [`15.1.6`](#1510) (for GitLab instances with multiple web nodes) > [`15.4.6`](#1540) > [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
|
||||
|
||||
NOTE:
|
||||
When not explicitly specified, upgrade GitLab to the latest available patch
|
||||
|
|
@ -266,7 +266,9 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
|||
|
||||
### 15.9.0
|
||||
|
||||
- There is a [database migration bug in GitLab 15.9.x](#user-profile-data-loss-bug-in-159x) that can cause data to be lost from the user profile fields. This bug affects all currently available 15.9.x releases. Until a bug fix is released, you should upgrade to 15.6.x, 15.7.x, or 15.8.x first.
|
||||
- **Upgrade to patch release 15.9.3 or later**. This provides fixes for two database migration bugs:
|
||||
- Patch releases 15.9.0, 15.9.1, 15.9.2 have [a bug that can cause data loss](#user-profile-data-loss-bug-in-159x) from the user profile fields.
|
||||
- The second [bug fix](https://gitlab.com/gitlab-org/gitlab/-/issues/394760) ensures it is possible to upgrade directly from 15.4.x.
|
||||
- As part of the [CI Partitioning effort](../architecture/blueprints/ci_data_decay/pipeline_partitioning.md), a [new Foreign Key](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107547) was added to `ci_builds_needs`. On GitLab instances with large CI tables, adding this constraint can take longer than usual. Make sure that this migration is finished before upgrading to 15.9.
|
||||
- Praefect's metadata verifier's [invalid metadata deletion behavior](../administration/gitaly/praefect.md#enable-deletions) is now enabled by default.
|
||||
|
||||
|
|
@ -298,7 +300,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
|||
|
||||
### 15.8.0
|
||||
|
||||
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
|
||||
- Git 2.38.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../install/installation.md#git).
|
||||
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
|
||||
- Geo: We discovered an issue where [replication and verification of projects and wikis was not keeping up](https://gitlab.com/gitlab-org/gitlab/-/issues/387980) on small number of Geo installations. Your installation may be affected if you see some projects and/or wikis persistently in the "Queued" state for verification. This can lead to data loss after a failover.
|
||||
|
|
@ -351,7 +352,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
|||
|
||||
### 15.7.0
|
||||
|
||||
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
|
||||
- This version validates a `NOT NULL DB` constraint on the `issues.work_item_type_id` column.
|
||||
To upgrade to this version, no records with a `NULL` `work_item_type_id` should exist on the `issues` table.
|
||||
There are multiple `BackfillWorkItemTypeIdForIssues` background migrations that will be finalized with
|
||||
|
|
@ -461,7 +461,6 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
|||
|
||||
### 15.6.0
|
||||
|
||||
- Due to a bug in GitLab 15.9.x that can cause data to be lost from certain user profile fields, 15.6, 15.7, or 15.8 is temporarily a required stop on the upgrade path. This requirement will be removed when a 15.9.x bug fix is released. [Read more about this issue](#user-profile-data-loss-bug-in-159x).
|
||||
- You should use one of the [officially supported PostgreSQL versions](../administration/package_information/postgresql_versions.md). Some database migrations can cause stability and performance issues with older PostgreSQL versions.
|
||||
- Git 2.37.0 and later is required by Gitaly. For installations from source, we recommend you use the [Git version provided by Gitaly](../install/installation.md#git).
|
||||
- A database change to modify the behavior of four indexes fails on instances
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ module API
|
|||
|
||||
before { authenticate_non_get! }
|
||||
|
||||
rescue_from ActiveRecord::QueryCanceled do |_e|
|
||||
render_api_error!({ error: 'Request timed out' }, 408)
|
||||
end
|
||||
|
||||
helpers Helpers::MergeRequestsHelpers
|
||||
|
||||
# These endpoints are defined in `TimeTrackingEndpoints` and is shared by
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Gitlab
|
|||
#
|
||||
# Job arguments needed must be defined explicitly,
|
||||
# see https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#job-arguments.
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
class BatchedMigrationJob
|
||||
include Gitlab::Database::DynamicModelHelpers
|
||||
include Gitlab::ClassAttributes
|
||||
|
|
@ -60,7 +62,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def initialize(
|
||||
start_id:, end_id:, batch_table:, batch_column:, sub_batch_size:, pause_ms:, job_arguments: [], connection:
|
||||
start_id:, end_id:, batch_table:, batch_column:, sub_batch_size:, pause_ms:, job_arguments: [], connection:,
|
||||
sub_batch_exception: nil
|
||||
)
|
||||
|
||||
@start_id = start_id
|
||||
|
|
@ -71,6 +74,7 @@ module Gitlab
|
|||
@pause_ms = pause_ms
|
||||
@job_arguments = job_arguments
|
||||
@connection = connection
|
||||
@sub_batch_exception = sub_batch_exception
|
||||
end
|
||||
|
||||
def filter_batch(relation)
|
||||
|
|
@ -87,7 +91,8 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
attr_reader :start_id, :end_id, :batch_table, :batch_column, :sub_batch_size, :pause_ms, :connection
|
||||
attr_reader :start_id, :end_id, :batch_table, :batch_column, :sub_batch_size,
|
||||
:pause_ms, :connection, :sub_batch_exception
|
||||
|
||||
def each_sub_batch(batching_arguments: {}, batching_scope: nil)
|
||||
all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments)
|
||||
|
|
@ -98,6 +103,10 @@ module Gitlab
|
|||
sub_batch_relation.each_batch(**all_batching_arguments) do |relation|
|
||||
batch_metrics.instrument_operation(operation_name) do
|
||||
yield relation
|
||||
rescue *Gitlab::Database::BackgroundMigration::BatchedJob::TIMEOUT_EXCEPTIONS => exception
|
||||
exception_class = sub_batch_exception || exception.class
|
||||
|
||||
raise exception_class, exception
|
||||
end
|
||||
|
||||
sleep([pause_ms, 0].max * 0.001)
|
||||
|
|
@ -137,3 +146,5 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module Gitlab
|
|||
module Database
|
||||
module BackgroundMigration
|
||||
SplitAndRetryError = Class.new(StandardError)
|
||||
ReduceSubBatchSizeError = Class.new(StandardError)
|
||||
|
||||
class BatchedJob < SharedModel
|
||||
include EachBatch
|
||||
|
|
@ -12,6 +13,9 @@ module Gitlab
|
|||
self.table_name = :batched_background_migration_jobs
|
||||
|
||||
MAX_ATTEMPTS = 3
|
||||
MIN_BATCH_SIZE = 1
|
||||
SUB_BATCH_SIZE_REDUCE_FACTOR = 0.75
|
||||
SUB_BATCH_SIZE_THRESHOLD = 65
|
||||
STUCK_JOBS_TIMEOUT = 1.hour.freeze
|
||||
TIMEOUT_EXCEPTIONS = [ActiveRecord::StatementTimeout, ActiveRecord::ConnectionTimeoutError,
|
||||
ActiveRecord::AdapterTimeout, ActiveRecord::LockWaitTimeout,
|
||||
|
|
@ -59,12 +63,12 @@ module Gitlab
|
|||
end
|
||||
|
||||
after_transition any => :failed do |job, transition|
|
||||
error_hash = transition.args.find { |arg| arg[:error].present? }
|
||||
exception, from_sub_batch = job.class.extract_transition_options(transition.args)
|
||||
|
||||
exception = error_hash&.fetch(:error)
|
||||
job.reduce_sub_batch_size! if from_sub_batch && job.can_reduce_sub_batch_size?
|
||||
|
||||
job.split_and_retry! if job.can_split?(exception)
|
||||
rescue SplitAndRetryError => error
|
||||
rescue SplitAndRetryError, ReduceSubBatchSizeError => error
|
||||
Gitlab::AppLogger.error(
|
||||
message: error.message,
|
||||
batched_job_id: job.id,
|
||||
|
|
@ -75,9 +79,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
after_transition do |job, transition|
|
||||
error_hash = transition.args.find { |arg| arg[:error].present? }
|
||||
|
||||
exception = error_hash&.fetch(:error)
|
||||
exception, _ = job.class.extract_transition_options(transition.args)
|
||||
|
||||
job.batched_job_transition_logs.create(previous_status: transition.from, next_status: transition.to, exception_class: exception&.class, exception_message: exception&.message)
|
||||
|
||||
|
|
@ -100,6 +102,17 @@ module Gitlab
|
|||
delegate :job_class, :table_name, :column_name, :job_arguments, :job_class_name,
|
||||
to: :batched_migration, prefix: :migration
|
||||
|
||||
def self.extract_transition_options(args)
|
||||
error_hash = args.find { |arg| arg[:error].present? }
|
||||
|
||||
return [] unless error_hash
|
||||
|
||||
exception = error_hash.fetch(:error)
|
||||
from_sub_batch = error_hash[:from_sub_batch]
|
||||
|
||||
[exception, from_sub_batch]
|
||||
end
|
||||
|
||||
def time_efficiency
|
||||
return unless succeeded?
|
||||
return unless finished_at && started_at
|
||||
|
|
@ -111,10 +124,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
def can_split?(exception)
|
||||
attempts >= MAX_ATTEMPTS &&
|
||||
exception&.class&.in?(TIMEOUT_EXCEPTIONS) &&
|
||||
batch_size > sub_batch_size &&
|
||||
batch_size > 1
|
||||
return if still_retryable?
|
||||
|
||||
exception.class.in?(TIMEOUT_EXCEPTIONS) && within_batch_size_boundaries?
|
||||
end
|
||||
|
||||
def can_reduce_sub_batch_size?
|
||||
return false unless Feature.enabled?(:reduce_sub_batch_size_on_timeouts)
|
||||
|
||||
still_retryable? && within_batch_size_boundaries?
|
||||
end
|
||||
|
||||
def split_and_retry!
|
||||
|
|
@ -163,6 +181,51 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# It reduces the size of +sub_batch_size+ by 25%
|
||||
def reduce_sub_batch_size!
|
||||
raise ReduceSubBatchSizeError, 'Only sub_batch_size of failed jobs can be reduced' unless failed?
|
||||
|
||||
return if sub_batch_exceeds_threshold?
|
||||
|
||||
with_lock do
|
||||
actual_sub_batch_size = sub_batch_size
|
||||
reduced_sub_batch_size = (sub_batch_size * SUB_BATCH_SIZE_REDUCE_FACTOR).to_i.clamp(1, batch_size)
|
||||
|
||||
update!(sub_batch_size: reduced_sub_batch_size)
|
||||
|
||||
Gitlab::AppLogger.warn(
|
||||
message: 'Sub batch size reduced due to timeout',
|
||||
batched_job_id: id,
|
||||
sub_batch_size: actual_sub_batch_size,
|
||||
reduced_sub_batch_size: reduced_sub_batch_size,
|
||||
attempts: attempts,
|
||||
batched_migration_id: batched_migration.id,
|
||||
job_class_name: migration_job_class_name,
|
||||
job_arguments: migration_job_arguments
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def still_retryable?
|
||||
attempts < MAX_ATTEMPTS
|
||||
end
|
||||
|
||||
def within_batch_size_boundaries?
|
||||
batch_size > MIN_BATCH_SIZE && batch_size > sub_batch_size
|
||||
end
|
||||
|
||||
# It doesn't allow sub-batch size to be reduced lower than the threshold
|
||||
#
|
||||
# @info It will prevent the next iteration to reduce the +sub_batch_size+ lower
|
||||
# than the +SUB_BATCH_SIZE_THRESHOLD+ or 65% of its original size.
|
||||
def sub_batch_exceeds_threshold?
|
||||
initial_sub_batch_size = batched_migration.sub_batch_size
|
||||
reduced_sub_batch_size = (sub_batch_size * SUB_BATCH_SIZE_REDUCE_FACTOR).to_i
|
||||
diff = initial_sub_batch_size - reduced_sub_batch_size
|
||||
|
||||
(1.0 * diff / initial_sub_batch_size * 100).round(2) > SUB_BATCH_SIZE_THRESHOLD
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,13 +15,21 @@ module Gitlab
|
|||
# when starting and finishing execution, and optionally saves batch_metrics
|
||||
# the migration provides, if any are given.
|
||||
#
|
||||
# The job's batch_metrics are serialized to JSON for storage.
|
||||
# @info The job's batch_metrics are serialized to JSON for storage.
|
||||
#
|
||||
# @info Track exceptions that could happen when processing sub-batches
|
||||
# through +Gitlab::BackgroundMigration::SubBatchTimeoutException+
|
||||
def perform(batch_tracking_record)
|
||||
start_tracking_execution(batch_tracking_record)
|
||||
|
||||
execute_batch(batch_tracking_record)
|
||||
|
||||
batch_tracking_record.succeed!
|
||||
rescue SubBatchTimeoutError => exception
|
||||
caused_by = exception.caused_by
|
||||
batch_tracking_record.failure!(error: caused_by, from_sub_batch: true)
|
||||
|
||||
raise caused_by
|
||||
rescue Exception => error # rubocop:disable Lint/RescueException
|
||||
batch_tracking_record.failure!(error: error)
|
||||
|
||||
|
|
@ -67,7 +75,8 @@ module Gitlab
|
|||
sub_batch_size: tracking_record.sub_batch_size,
|
||||
pause_ms: tracking_record.pause_ms,
|
||||
job_arguments: tracking_record.migration_job_arguments,
|
||||
connection: connection)
|
||||
connection: connection,
|
||||
sub_batch_exception: ::Gitlab::Database::BackgroundMigration::SubBatchTimeoutError)
|
||||
|
||||
job_instance.perform
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
class SubBatchTimeoutError < StandardError
|
||||
def initialize(caused_by)
|
||||
@caused_by = caused_by
|
||||
|
||||
super(caused_by)
|
||||
end
|
||||
|
||||
attr_reader :caused_by
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,8 @@ module Gitlab
|
|||
module Database
|
||||
module SchemaValidation
|
||||
class Database
|
||||
STATIC_PARTITIONS_SCHEMA = 'gitlab_partitions_static'
|
||||
|
||||
def initialize(connection)
|
||||
@connection = connection
|
||||
end
|
||||
|
|
@ -12,33 +14,69 @@ module Gitlab
|
|||
index_map[index_name]
|
||||
end
|
||||
|
||||
def indexes
|
||||
index_map.values
|
||||
def fetch_trigger_by_name(trigger_name)
|
||||
trigger_map[trigger_name]
|
||||
end
|
||||
|
||||
def index_exists?(index_name)
|
||||
index_map[index_name].present?
|
||||
end
|
||||
|
||||
def trigger_exists?(trigger_name)
|
||||
trigger_map[trigger_name].present?
|
||||
end
|
||||
|
||||
def indexes
|
||||
index_map.values
|
||||
end
|
||||
|
||||
def triggers
|
||||
trigger_map.values
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :connection
|
||||
|
||||
def schemas
|
||||
@schemas ||= [STATIC_PARTITIONS_SCHEMA, connection.current_schema]
|
||||
end
|
||||
|
||||
def index_map
|
||||
@index_map ||=
|
||||
fetch_indexes.transform_values! do |index_stmt|
|
||||
Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
|
||||
SchemaObjects::Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :connection
|
||||
def trigger_map
|
||||
@trigger_map ||=
|
||||
fetch_triggers.transform_values! do |trigger_stmt|
|
||||
SchemaObjects::Trigger.new(PgQuery.parse(trigger_stmt).tree.stmts.first.stmt.create_trig_stmt)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_indexes
|
||||
sql = <<~SQL
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ('public', 'gitlab_partitions_static');
|
||||
WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ($1, $2);
|
||||
SQL
|
||||
|
||||
@fetch_indexes ||= connection.exec_query(sql).rows.to_h
|
||||
connection.select_rows(sql, nil, schemas).to_h
|
||||
end
|
||||
|
||||
def fetch_triggers
|
||||
sql = <<~SQL
|
||||
SELECT triggers.tgname, pg_get_triggerdef(triggers.oid)
|
||||
FROM pg_catalog.pg_trigger triggers
|
||||
INNER JOIN pg_catalog.pg_class rel ON triggers.tgrelid = rel.oid
|
||||
INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = rel.relnamespace
|
||||
WHERE triggers.tgisinternal IS FALSE
|
||||
AND nsp.nspname IN ($1, $2)
|
||||
SQL
|
||||
|
||||
connection.select_rows(sql, nil, schemas).to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
class Index
|
||||
def initialize(parsed_stmt)
|
||||
@parsed_stmt = parsed_stmt
|
||||
end
|
||||
|
||||
def name
|
||||
parsed_stmt.idxname
|
||||
end
|
||||
|
||||
def statement
|
||||
@statement ||= PgQuery.deparse_stmt(parsed_stmt)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :parsed_stmt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
class Indexes
|
||||
def initialize(structure_sql, database)
|
||||
@structure_sql = structure_sql
|
||||
@database = database
|
||||
end
|
||||
|
||||
def missing_indexes
|
||||
structure_sql.indexes.map(&:name) - database.indexes.map(&:name)
|
||||
end
|
||||
|
||||
def extra_indexes
|
||||
database.indexes.map(&:name) - structure_sql.indexes.map(&:name)
|
||||
end
|
||||
|
||||
def wrong_indexes
|
||||
structure_sql.indexes.filter_map do |structure_sql_index|
|
||||
database_index = database.fetch_index_by_name(structure_sql_index.name)
|
||||
|
||||
next if database_index.nil?
|
||||
next if database_index.statement == structure_sql_index.statement
|
||||
|
||||
structure_sql_index.name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :structure_sql, :database
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module SchemaObjects
|
||||
class Base
|
||||
def initialize(parsed_stmt)
|
||||
@parsed_stmt = parsed_stmt
|
||||
end
|
||||
|
||||
def name
|
||||
raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}"
|
||||
end
|
||||
|
||||
def statement
|
||||
@statement ||= PgQuery.deparse_stmt(parsed_stmt)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :parsed_stmt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module SchemaObjects
|
||||
class Index < Base
|
||||
def name
|
||||
parsed_stmt.idxname
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module SchemaObjects
|
||||
class Trigger < Base
|
||||
def name
|
||||
parsed_stmt.trigname
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,33 +4,56 @@ module Gitlab
|
|||
module Database
|
||||
module SchemaValidation
|
||||
class StructureSql
|
||||
def initialize(structure_file_path)
|
||||
DEFAULT_SCHEMA = 'public'
|
||||
|
||||
def initialize(structure_file_path, schema_name = DEFAULT_SCHEMA)
|
||||
@structure_file_path = structure_file_path
|
||||
@schema_name = schema_name
|
||||
end
|
||||
|
||||
def index_exists?(index_name)
|
||||
indexes.find { |index| index.name == index_name }.present?
|
||||
end
|
||||
|
||||
def indexes
|
||||
@indexes ||= index_statements.map do |index_statement|
|
||||
index_statement.relation.schemaname = "public" if index_statement.relation.schemaname == ''
|
||||
def trigger_exists?(trigger_name)
|
||||
triggers.find { |trigger| trigger.name == trigger_name }.present?
|
||||
end
|
||||
|
||||
Index.new(index_statement)
|
||||
end
|
||||
def indexes
|
||||
@indexes ||= map_with_default_schema(index_statements, SchemaObjects::Index)
|
||||
end
|
||||
|
||||
def triggers
|
||||
@triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :structure_file_path
|
||||
attr_reader :structure_file_path, :schema_name
|
||||
|
||||
def index_statements
|
||||
parsed_structure_file.tree.stmts.filter_map { |s| s.stmt.index_stmt }
|
||||
statements.filter_map { |s| s.stmt.index_stmt }
|
||||
end
|
||||
|
||||
def trigger_statements
|
||||
statements.filter_map { |s| s.stmt.create_trig_stmt }
|
||||
end
|
||||
|
||||
def statements
|
||||
@statements ||= parsed_structure_file.tree.stmts
|
||||
end
|
||||
|
||||
def parsed_structure_file
|
||||
PgQuery.parse(File.read(structure_file_path))
|
||||
end
|
||||
|
||||
def map_with_default_schema(statements, validation_class)
|
||||
statements.map do |statement|
|
||||
statement.relation.schemaname = schema_name if statement.relation.schemaname == ''
|
||||
|
||||
validation_class.new(statement)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,8 +15,11 @@ module Gitlab
|
|||
def self.all_validators
|
||||
[
|
||||
ExtraIndexes,
|
||||
ExtraTriggers,
|
||||
MissingIndexes,
|
||||
DifferentDefinitionIndexes
|
||||
MissingTriggers,
|
||||
DifferentDefinitionIndexes,
|
||||
DifferentDefinitionTriggers
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module Validators
|
||||
class DifferentDefinitionTriggers < BaseValidator
|
||||
def execute
|
||||
structure_sql.triggers.filter_map do |structure_sql_trigger|
|
||||
database_trigger = database.fetch_trigger_by_name(structure_sql_trigger.name)
|
||||
|
||||
next if database_trigger.nil?
|
||||
next if database_trigger.statement == structure_sql_trigger.statement
|
||||
|
||||
build_inconsistency(self.class, structure_sql_trigger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module Validators
|
||||
class ExtraTriggers < BaseValidator
|
||||
def execute
|
||||
database.triggers.filter_map do |trigger|
|
||||
next if structure_sql.trigger_exists?(trigger.name)
|
||||
|
||||
build_inconsistency(self.class, trigger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module SchemaValidation
|
||||
module Validators
|
||||
class MissingTriggers < BaseValidator
|
||||
def execute
|
||||
structure_sql.triggers.filter_map do |index|
|
||||
next if database.trigger_exists?(index.name)
|
||||
|
||||
build_inconsistency(self.class, index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module Concerns
|
||||
module RenderIfLoggedIn
|
||||
def render?
|
||||
!!context.current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class AccessTokensMenu < ::Sidebars::Menu
|
||||
override :link
|
||||
def link
|
||||
profile_personal_access_tokens_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Access Tokens')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'token'
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
!!context.current_user && !Gitlab::CurrentSettings.personal_access_tokens_disabled?
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :personal_access_tokens }
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{ 'data-qa-selector': 'access_token_link' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class AccountMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_account_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Account')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'account'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: [:accounts, :two_factor_auths] }
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{ 'data-qa-selector': 'profile_account_link' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class ActiveSessionsMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_active_sessions_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Active Sessions')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'monitor-lines'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :active_sessions }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class ApplicationsMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
applications_profile_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Applications')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'applications'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: 'oauth/applications' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class AuthenticationLogMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
audit_log_profile_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Authentication Log')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'log'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ path: 'profiles#audit_log' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class ChatMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_chat_names_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Chat')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'comment'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :chat_names }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class EmailsMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_emails_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Emails')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'mail'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :emails }
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{ 'data-qa-selector': 'profile_emails_link' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class GpgKeysMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_gpg_keys_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('GPG Keys')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'key'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :gpg_keys }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class NotificationsMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_notifications_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Notifications')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'notifications'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :notifications }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class PasswordMenu < ::Sidebars::Menu
|
||||
override :link
|
||||
def link
|
||||
edit_profile_password_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Password')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'lock'
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
!!context.current_user&.allow_password_authentication?
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :passwords }
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{ 'data-qa-selector': 'profile_password_link' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class PreferencesMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_preferences_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Preferences')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'preferences'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :preferences }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class ProfileMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Profile')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'profile'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ path: 'profiles#show' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class SavedRepliesMenu < ::Sidebars::Menu
|
||||
include UsersHelper
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_saved_replies_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Saved Replies')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'symlink'
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
!!context.current_user && saved_replies_enabled?
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :saved_replies }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
context.current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
module Menus
|
||||
class SshKeysMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::RenderIfLoggedIn
|
||||
|
||||
override :link
|
||||
def link
|
||||
profile_keys_path
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('SSH Keys')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'key'
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :keys }
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{ 'data-qa-selector': 'ssh_keys_link' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module UserSettings
|
||||
class Panel < ::Sidebars::Panel
|
||||
override :configure_menus
|
||||
def configure_menus
|
||||
add_menus
|
||||
end
|
||||
|
||||
override :aria_label
|
||||
def aria_label
|
||||
_('User settings')
|
||||
end
|
||||
|
||||
override :render_raw_scope_menu_partial
|
||||
def render_raw_scope_menu_partial
|
||||
"shared/nav/user_settings_scope_header"
|
||||
end
|
||||
|
||||
override :super_sidebar_context_header
|
||||
def super_sidebar_context_header
|
||||
@super_sidebar_context_header ||= {
|
||||
title: aria_label,
|
||||
avatar: context.current_user.avatar_url
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_menus
|
||||
add_menu(Sidebars::UserSettings::Menus::ProfileMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::AccountMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::ApplicationsMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::ChatMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::AccessTokensMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::EmailsMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::PasswordMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::NotificationsMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::SshKeysMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::GpgKeysMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::PreferencesMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::SavedRepliesMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::ActiveSessionsMenu.new(context))
|
||||
add_menu(Sidebars::UserSettings::Menus::AuthenticationLogMenu.new(context))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sidebars::UserSettings::Panel.prepend_mod_with('Sidebars::UserSettings::Panel')
|
||||
|
|
@ -64,11 +64,11 @@ namespace :tw do
|
|||
CodeOwnerRule.new('Respond', '@msedlakjakubowski'),
|
||||
CodeOwnerRule.new('Runner', '@fneill'),
|
||||
CodeOwnerRule.new('Runner SaaS', '@fneill'),
|
||||
CodeOwnerRule.new('Pods', '@jglassman1'),
|
||||
CodeOwnerRule.new('Security Policies', '@dianalogan'),
|
||||
CodeOwnerRule.new('Source Code', '@aqualls'),
|
||||
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
|
||||
CodeOwnerRule.new('Style Guide', '@sselhorn'),
|
||||
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
|
||||
CodeOwnerRule.new('Testing', '@eread'),
|
||||
CodeOwnerRule.new('Threat Insights', '@rdickenson'),
|
||||
CodeOwnerRule.new('Tutorials', '@kpaizee'),
|
||||
|
|
|
|||
|
|
@ -33046,9 +33046,6 @@ msgstr ""
|
|||
msgid "Profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profile Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profile failed to delete"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39821,6 +39818,9 @@ msgstr ""
|
|||
msgid "SelfMonitoring|Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?"
|
||||
msgstr ""
|
||||
|
||||
msgid "SelfMonitoring|Deprecation notice"
|
||||
msgstr ""
|
||||
|
||||
msgid "SelfMonitoring|Self-monitoring"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39833,6 +39833,9 @@ msgstr ""
|
|||
msgid "SelfMonitoring|Self-monitoring project successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "SelfMonitoring|Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Send"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -8,25 +8,35 @@ module QA
|
|||
# since tablets have the regular top navigation bar but still close the left nav
|
||||
prepend QA::Mobile::Page::SubMenus::Common if QA::Runtime::Env.remote_mobile_device_name
|
||||
|
||||
view 'app/views/layouts/nav/sidebar/_profile.html.haml' do
|
||||
element :access_token_link, 'link_to profile_personal_access_tokens_path' # rubocop:disable QA/ElementWithPattern
|
||||
element :access_token_title, 'Access Tokens' # rubocop:disable QA/ElementWithPattern
|
||||
element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern
|
||||
element :ssh_keys, 'SSH Keys' # rubocop:disable QA/ElementWithPattern
|
||||
view 'lib/sidebars/user_settings/menus/access_tokens_menu.rb' do
|
||||
element :access_token_link
|
||||
end
|
||||
|
||||
view 'lib/sidebars/user_settings/menus/ssh_keys_menu.rb' do
|
||||
element :ssh_keys_link
|
||||
end
|
||||
|
||||
view 'lib/sidebars/user_settings/menus/emails_menu.rb' do
|
||||
element :profile_emails_link
|
||||
end
|
||||
|
||||
view 'lib/sidebars/user_settings/menus/password_menu.rb' do
|
||||
element :profile_password_link
|
||||
end
|
||||
|
||||
view 'lib/sidebars/user_settings/menus/account_menu.rb' do
|
||||
element :profile_account_link
|
||||
end
|
||||
|
||||
def click_access_tokens
|
||||
within_sidebar do
|
||||
click_link('Access Tokens')
|
||||
click_element(:access_token_link)
|
||||
end
|
||||
end
|
||||
|
||||
def click_ssh_keys
|
||||
within_sidebar do
|
||||
click_link('SSH Keys')
|
||||
click_element(:ssh_keys_link)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe 'User visits the authentication log', feature_category: :user_pro
|
|||
it 'shows correct menu item' do
|
||||
visit(audit_log_profile_path)
|
||||
|
||||
expect(page).to have_active_navigation('Authentication log')
|
||||
expect(page).to have_active_navigation('Authentication Log')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
|
|||
end
|
||||
end
|
||||
|
||||
it "verified and the gpg user's profile doesn't exist anymore" do
|
||||
it "verified and the gpg user's profile doesn't exist anymore", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/395802' do
|
||||
user_1_key
|
||||
|
||||
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
|
||||
|
|
|
|||
|
|
@ -18,3 +18,11 @@ CREATE TABLE ci_project_mirrors (
|
|||
project_id integer NOT NULL,
|
||||
namespace_id integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1();
|
||||
|
||||
CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION my_function();
|
||||
|
||||
CREATE TRIGGER missing_trigger_1 BEFORE INSERT OR UPDATE ON public.t3 FOR EACH ROW EXECUTE FUNCTION t3();
|
||||
|
||||
CREATE TRIGGER projects_loose_fk_trigger AFTER DELETE ON projects REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ describe('GroupSelect', () => {
|
|||
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
|
||||
exclude_internal: true,
|
||||
active: true,
|
||||
order_by: 'similarity',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,41 @@ exports[`self-monitor component When the self-monitor project has not been creat
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<gl-alert-stub
|
||||
class="gl-mb-3"
|
||||
dismissible="true"
|
||||
dismisslabel="Dismiss"
|
||||
primarybuttonlink=""
|
||||
primarybuttontext=""
|
||||
secondarybuttonlink=""
|
||||
secondarybuttontext=""
|
||||
showicon="true"
|
||||
title="Deprecation notice"
|
||||
variant="danger"
|
||||
>
|
||||
<div>
|
||||
Self-monitoring was
|
||||
<a
|
||||
href="/help/update/deprecations.md#gitlab-self-monitoring-project"
|
||||
>
|
||||
deprecated
|
||||
</a>
|
||||
in GitLab 14.9, and is
|
||||
<a
|
||||
href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909"
|
||||
>
|
||||
scheduled for removal
|
||||
</a>
|
||||
in GitLab 16.0. For information on a possible replacement,
|
||||
<a
|
||||
href="https://gitlab.com/groups/gitlab-org/-/epics/6976"
|
||||
>
|
||||
learn more about Opstrace
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</gl-alert-stub>
|
||||
|
||||
<div
|
||||
class="settings-content"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -214,6 +214,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
|
|||
expect(helper.super_sidebar_nav_panel(nav: 'group')).to be_a(Sidebars::Groups::SuperSidebarPanel)
|
||||
end
|
||||
|
||||
it 'returns User Settings Panel for profile nav' do
|
||||
expect(helper.super_sidebar_nav_panel(nav: 'profile')).to be_a(Sidebars::UserSettings::Panel)
|
||||
end
|
||||
|
||||
it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do
|
||||
expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -301,6 +301,28 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
|
|||
perform_job
|
||||
end
|
||||
|
||||
context 'when using a sub batch exception for timeouts' do
|
||||
let(:job_class) do
|
||||
Class.new(described_class) do
|
||||
operation_name :update
|
||||
|
||||
def perform(*_)
|
||||
each_sub_batch { raise ActiveRecord::StatementTimeout } # rubocop:disable Lint/UnreachableLoop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:job_instance) do
|
||||
job_class.new(start_id: 1, end_id: 10, batch_table: '_test_table', batch_column: 'id',
|
||||
sub_batch_size: 2, pause_ms: 1000, connection: connection,
|
||||
sub_batch_exception: StandardError)
|
||||
end
|
||||
|
||||
it 'raises the expected error type' do
|
||||
expect { job_instance.perform }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when batching_arguments are given' do
|
||||
it 'forwards them for batching' do
|
||||
expect(job_instance).to receive(:base_relation).and_return(test_table)
|
||||
|
|
|
|||
|
|
@ -184,6 +184,35 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
|
|||
expect(transition_log.exception_message).to eq('RuntimeError')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job fails during sub batch processing' do
|
||||
let(:args) { { error: ActiveRecord::StatementTimeout.new, from_sub_batch: true } }
|
||||
let(:attempts) { 0 }
|
||||
let(:failure) { job.failure!(**args) }
|
||||
let(:job) do
|
||||
create(:batched_background_migration_job, :running, batch_size: 20, sub_batch_size: 10, attempts: attempts)
|
||||
end
|
||||
|
||||
context 'when sub batch size can be reduced in 25%' do
|
||||
it { expect { failure }.to change { job.sub_batch_size }.to 7 }
|
||||
end
|
||||
|
||||
context 'when retries exceeds 2 attempts' do
|
||||
let(:attempts) { 3 }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:split_and_retry!)
|
||||
end
|
||||
|
||||
it 'calls split_and_retry! once sub_batch_size cannot be decreased anymore' do
|
||||
failure
|
||||
|
||||
expect(job).to have_received(:split_and_retry!).once
|
||||
end
|
||||
|
||||
it { expect { failure }.not_to change { job.sub_batch_size } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
|
|
@ -271,6 +300,24 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
|
|||
end
|
||||
end
|
||||
|
||||
describe '.extract_transition_options' do
|
||||
let(:perform) { subject.class.extract_transition_options(args) }
|
||||
|
||||
where(:args, :expected_result) do
|
||||
[
|
||||
[[], []],
|
||||
[[{ error: StandardError }], [StandardError, nil]],
|
||||
[[{ error: StandardError, from_sub_batch: true }], [StandardError, true]]
|
||||
]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'matches expected keys and result' do
|
||||
expect(perform).to match_array(expected_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can_split?' do
|
||||
subject { job.can_split?(exception) }
|
||||
|
||||
|
|
@ -327,6 +374,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
|
|||
end
|
||||
end
|
||||
|
||||
describe '#can_reduce_sub_batch_size?' do
|
||||
let(:attempts) { 0 }
|
||||
let(:batch_size) { 10 }
|
||||
let(:sub_batch_size) { 6 }
|
||||
let(:feature_flag) { :reduce_sub_batch_size_on_timeouts }
|
||||
let(:job) do
|
||||
create(:batched_background_migration_job, attempts: attempts,
|
||||
batch_size: batch_size, sub_batch_size: sub_batch_size)
|
||||
end
|
||||
|
||||
where(:feature_flag_state, :within_boundaries, :outside_boundaries, :limit_reached) do
|
||||
[
|
||||
[true, true, false, false],
|
||||
[false, false, false, false]
|
||||
]
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(feature_flag => feature_flag_state)
|
||||
end
|
||||
|
||||
context 'when the number of attempts is lower than the limit and batch size are within boundaries' do
|
||||
let(:attempts) { 1 }
|
||||
|
||||
it { expect(job.can_reduce_sub_batch_size?).to be(within_boundaries) }
|
||||
end
|
||||
|
||||
context 'when the number of attempts is lower than the limit and batch size are outside boundaries' do
|
||||
let(:batch_size) { 1 }
|
||||
|
||||
it { expect(job.can_reduce_sub_batch_size?).to be(outside_boundaries) }
|
||||
end
|
||||
|
||||
context 'when the number of attempts is greater than the limit and batch size are within boundaries' do
|
||||
let(:attempts) { 3 }
|
||||
|
||||
it { expect(job.can_reduce_sub_batch_size?).to be(limit_reached) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#time_efficiency' do
|
||||
subject { job.time_efficiency }
|
||||
|
||||
|
|
@ -465,4 +554,80 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reduce_sub_batch_size!' do
|
||||
let(:migration_batch_size) { 20 }
|
||||
let(:migration_sub_batch_size) { 10 }
|
||||
let(:job_batch_size) { 20 }
|
||||
let(:job_sub_batch_size) { 10 }
|
||||
let(:status) { :failed }
|
||||
|
||||
let(:migration) do
|
||||
create(:batched_background_migration, :active, batch_size: migration_batch_size,
|
||||
sub_batch_size: migration_sub_batch_size)
|
||||
end
|
||||
|
||||
let(:job) do
|
||||
create(:batched_background_migration_job, status, sub_batch_size: job_sub_batch_size,
|
||||
batch_size: job_batch_size, batched_migration: migration)
|
||||
end
|
||||
|
||||
context 'when the job sub batch size can be reduced' do
|
||||
let(:expected_sub_batch_size) { 7 }
|
||||
|
||||
it 'reduces sub batch size in 25%' do
|
||||
expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to(expected_sub_batch_size)
|
||||
end
|
||||
|
||||
it 'log the changes' do
|
||||
expect(Gitlab::AppLogger).to receive(:warn).with(
|
||||
message: 'Sub batch size reduced due to timeout',
|
||||
batched_job_id: job.id,
|
||||
sub_batch_size: job_sub_batch_size,
|
||||
reduced_sub_batch_size: expected_sub_batch_size,
|
||||
attempts: job.attempts,
|
||||
batched_migration_id: migration.id,
|
||||
job_class_name: job.migration_job_class_name,
|
||||
job_arguments: job.migration_job_arguments
|
||||
)
|
||||
|
||||
job.reduce_sub_batch_size!
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reduced sub_batch_size is greater than sub_batch' do
|
||||
let(:job_batch_size) { 5 }
|
||||
|
||||
it "doesn't allow sub_batch_size to greater than sub_batch" do
|
||||
expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to 5
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sub_batch_size is already 1' do
|
||||
let(:job_sub_batch_size) { 1 }
|
||||
|
||||
it "updates sub_batch_size to it's minimum value" do
|
||||
expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job has not failed' do
|
||||
let(:status) { :succeeded }
|
||||
let(:error) { Gitlab::Database::BackgroundMigration::ReduceSubBatchSizeError }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { job.reduce_sub_batch_size! }.to raise_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the amount to be reduced exceeds the threshold' do
|
||||
let(:migration_batch_size) { 150 }
|
||||
let(:migration_sub_batch_size) { 100 }
|
||||
let(:job_sub_batch_size) { 30 }
|
||||
|
||||
it 'prevents sub batch size to be reduced' do
|
||||
expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
|
|||
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
|
||||
let(:metrics_tracker) { instance_double('::Gitlab::Database::BackgroundMigration::PrometheusMetrics', track: nil) }
|
||||
let(:job_class) { Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) }
|
||||
let(:sub_batch_exception) { Gitlab::Database::BackgroundMigration::SubBatchTimeoutError }
|
||||
|
||||
let_it_be(:pause_ms) { 250 }
|
||||
let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) }
|
||||
|
|
@ -39,7 +40,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
|
|||
sub_batch_size: 1,
|
||||
pause_ms: pause_ms,
|
||||
job_arguments: active_migration.job_arguments,
|
||||
connection: connection)
|
||||
connection: connection,
|
||||
sub_batch_exception: sub_batch_exception)
|
||||
.and_return(job_instance)
|
||||
|
||||
expect(job_instance).to receive(:perform).with(no_args)
|
||||
|
|
@ -119,12 +121,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
|
|||
end
|
||||
|
||||
context 'when the migration job raises an error' do
|
||||
shared_examples 'an error is raised' do |error_class|
|
||||
shared_examples 'an error is raised' do |error_class, cause|
|
||||
let(:expected_to_raise) { cause || error_class }
|
||||
|
||||
it 'marks the tracking record as failed' do
|
||||
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
|
||||
|
||||
freeze_time do
|
||||
expect { perform }.to raise_error(error_class)
|
||||
expect { perform }.to raise_error(expected_to_raise)
|
||||
|
||||
reloaded_job_record = job_record.reload
|
||||
|
||||
|
|
@ -137,13 +141,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
|
|||
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
|
||||
expect(metrics_tracker).to receive(:track).with(job_record)
|
||||
|
||||
expect { perform }.to raise_error(error_class)
|
||||
expect { perform }.to raise_error(expected_to_raise)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an error is raised', RuntimeError.new('Something broke!')
|
||||
it_behaves_like 'an error is raised', SignalException.new('SIGTERM')
|
||||
it_behaves_like 'an error is raised', ActiveRecord::StatementTimeout.new('Timeout!')
|
||||
|
||||
error = StandardError.new
|
||||
it_behaves_like('an error is raised', Gitlab::Database::BackgroundMigration::SubBatchTimeoutError.new(error), error)
|
||||
end
|
||||
|
||||
context 'when the batched background migration does not inherit from BatchedMigrationJob' do
|
||||
|
|
|
|||
|
|
@ -3,61 +3,108 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
|
||||
let(:database_name) { 'main' }
|
||||
let(:database_indexes) do
|
||||
[['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
|
||||
end
|
||||
|
||||
let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
|
||||
let(:database_model) { Gitlab::Database.database_base_models[database_name] }
|
||||
let(:connection) { database_model.connection }
|
||||
|
||||
subject(:database) { described_class.new(connection) }
|
||||
|
||||
before do
|
||||
allow(connection).to receive(:exec_query).and_return(query_result)
|
||||
end
|
||||
let(:database_model) { Gitlab::Database.database_base_models['main'] }
|
||||
let(:connection) { database_model.connection }
|
||||
|
||||
describe '#fetch_index_by_name' do
|
||||
context 'when index does not exist' do
|
||||
it 'returns nil' do
|
||||
index = database.fetch_index_by_name('non_existing_index')
|
||||
context 'when having indexes' do
|
||||
let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index }
|
||||
let(:results) do
|
||||
[['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
|
||||
end
|
||||
|
||||
expect(index).to be_nil
|
||||
before do
|
||||
allow(connection).to receive(:select_rows).and_return(results)
|
||||
end
|
||||
|
||||
describe '#fetch_index_by_name' do
|
||||
context 'when index does not exist' do
|
||||
it 'returns nil' do
|
||||
index = database.fetch_index_by_name('non_existing_index')
|
||||
|
||||
expect(index).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns index by name' do
|
||||
index = database.fetch_index_by_name('index')
|
||||
|
||||
expect(index.name).to eq('index')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns index by name' do
|
||||
index = database.fetch_index_by_name('index')
|
||||
describe '#index_exists?' do
|
||||
context 'when index exists' do
|
||||
it 'returns true' do
|
||||
index_exists = database.index_exists?('index')
|
||||
|
||||
expect(index.name).to eq('index')
|
||||
end
|
||||
end
|
||||
expect(index_exists).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#index_exists?' do
|
||||
context 'when index exists' do
|
||||
it 'returns true' do
|
||||
index_exists = database.index_exists?('index')
|
||||
context 'when index does not exist' do
|
||||
it 'returns false' do
|
||||
index_exists = database.index_exists?('non_existing_index')
|
||||
|
||||
expect(index_exists).to be_truthy
|
||||
expect(index_exists).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when index does not exist' do
|
||||
it 'returns false' do
|
||||
index_exists = database.index_exists?('non_existing_index')
|
||||
describe '#indexes' do
|
||||
it 'returns indexes' do
|
||||
indexes = database.indexes
|
||||
|
||||
expect(index_exists).to be_falsey
|
||||
expect(indexes).to all(be_a(schema_object))
|
||||
expect(indexes.map(&:name)).to eq(['index'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#indexes' do
|
||||
it 'returns indexes' do
|
||||
indexes = database.indexes
|
||||
context 'when having triggers' do
|
||||
let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Trigger }
|
||||
let(:results) do
|
||||
{ 'my_trigger' => 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
|
||||
end
|
||||
|
||||
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
|
||||
expect(indexes.map(&:name)).to eq(['index'])
|
||||
before do
|
||||
allow(database).to receive(:fetch_triggers).and_return(results)
|
||||
end
|
||||
|
||||
describe '#fetch_trigger_by_name' do
|
||||
context 'when trigger does not exist' do
|
||||
it 'returns nil' do
|
||||
expect(database.fetch_trigger_by_name('non_existing_trigger')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns trigger by name' do
|
||||
expect(database.fetch_trigger_by_name('my_trigger').name).to eq('my_trigger')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#trigger_exists?' do
|
||||
context 'when trigger exists' do
|
||||
it 'returns true' do
|
||||
expect(database.trigger_exists?('my_trigger')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trigger does not exist' do
|
||||
it 'returns false' do
|
||||
expect(database.trigger_exists?('non_existing_trigger')).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#triggers' do
|
||||
it 'returns triggers' do
|
||||
triggers = database.triggers
|
||||
|
||||
expect(triggers).to all(be_a(schema_object))
|
||||
expect(triggers.map(&:name)).to eq(['my_trigger'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do
|
||||
let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
|
||||
|
||||
let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt }
|
||||
|
||||
let(:index) { described_class.new(stmt) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns index name' do
|
||||
expect(index.name).to eq('index_name')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#statement' do
|
||||
it 'returns index statement' do
|
||||
expect(index.statement).to eq(index_statement)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ RSpec.describe Gitlab::Database::SchemaValidation::Runner, feature_category: :da
|
|||
let(:connection) { ActiveRecord::Base.connection }
|
||||
|
||||
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
|
||||
let(:structure_sql) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
|
||||
let(:structure_sql) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, 'public') }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:inconsistencies) { described_class.new(structure_sql, database).execute }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Index, feature_category: :database do
|
||||
let(:statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
|
||||
let(:name) { 'index_name' }
|
||||
|
||||
include_examples 'schema objects assertions for', 'index_stmt'
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Trigger, feature_category: :database do
|
||||
let(:statement) { 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
|
||||
let(:name) { 'my_trigger' }
|
||||
|
||||
include_examples 'schema objects assertions for', 'create_trig_stmt'
|
||||
end
|
||||
|
|
@ -4,44 +4,79 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::StructureSql, feature_category: :database do
|
||||
let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
|
||||
let(:schema_name) { 'public' }
|
||||
|
||||
subject(:structure_sql) { described_class.new(structure_file_path) }
|
||||
subject(:structure_sql) { described_class.new(structure_file_path, schema_name) }
|
||||
|
||||
describe '#index_exists?' do
|
||||
subject(:index_exists) { structure_sql.index_exists?(index_name) }
|
||||
context 'when having indexes' do
|
||||
describe '#index_exists?' do
|
||||
subject(:index_exists) { structure_sql.index_exists?(index_name) }
|
||||
|
||||
context 'when the index does not exist' do
|
||||
let(:index_name) { 'non-existent-index' }
|
||||
context 'when the index does not exist' do
|
||||
let(:index_name) { 'non-existent-index' }
|
||||
|
||||
it 'returns false' do
|
||||
expect(index_exists).to be_falsey
|
||||
it 'returns false' do
|
||||
expect(index_exists).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the index exists' do
|
||||
let(:index_name) { 'index' }
|
||||
|
||||
it 'returns true' do
|
||||
expect(index_exists).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the index exists' do
|
||||
let(:index_name) { 'index' }
|
||||
describe '#indexes' do
|
||||
it 'returns indexes' do
|
||||
indexes = structure_sql.indexes
|
||||
|
||||
it 'returns true' do
|
||||
expect(index_exists).to be_truthy
|
||||
expected_indexes = %w[
|
||||
missing_index
|
||||
wrong_index
|
||||
index
|
||||
index_namespaces_public_groups_name_id
|
||||
index_on_deploy_keys_id_and_type_and_public
|
||||
index_users_on_public_email_excluding_null_and_empty
|
||||
]
|
||||
|
||||
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Index))
|
||||
expect(indexes.map(&:name)).to eq(expected_indexes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#indexes' do
|
||||
it 'returns indexes' do
|
||||
indexes = structure_sql.indexes
|
||||
context 'when having triggers' do
|
||||
describe '#trigger_exists?' do
|
||||
subject(:trigger_exists) { structure_sql.trigger_exists?(name) }
|
||||
|
||||
expected_indexes = %w[
|
||||
missing_index
|
||||
wrong_index
|
||||
index
|
||||
index_namespaces_public_groups_name_id
|
||||
index_on_deploy_keys_id_and_type_and_public
|
||||
index_users_on_public_email_excluding_null_and_empty
|
||||
]
|
||||
context 'when the trigger does not exist' do
|
||||
let(:name) { 'non-existent-trigger' }
|
||||
|
||||
expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
|
||||
expect(indexes.map(&:name)).to eq(expected_indexes)
|
||||
it 'returns false' do
|
||||
expect(trigger_exists).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the trigger exists' do
|
||||
let(:name) { 'trigger' }
|
||||
|
||||
it 'returns true' do
|
||||
expect(trigger_exists).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#triggers' do
|
||||
it 'returns triggers' do
|
||||
triggers = structure_sql.triggers
|
||||
expected_triggers = %w[trigger wrong_trigger missing_trigger_1 projects_loose_fk_trigger]
|
||||
|
||||
expect(triggers).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Trigger))
|
||||
expect(triggers.map(&:name)).to eq(expected_triggers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ RSpec.describe Gitlab::Database::SchemaValidation::Validators::BaseValidator, fe
|
|||
it 'returns an array of all validators' do
|
||||
expect(all_validators).to eq([
|
||||
Gitlab::Database::SchemaValidation::Validators::ExtraIndexes,
|
||||
Gitlab::Database::SchemaValidation::Validators::ExtraTriggers,
|
||||
Gitlab::Database::SchemaValidation::Validators::MissingIndexes,
|
||||
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes
|
||||
Gitlab::Database::SchemaValidation::Validators::MissingTriggers,
|
||||
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes,
|
||||
Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers
|
||||
])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers,
|
||||
feature_category: :database do
|
||||
include_examples 'trigger validators', described_class, ['wrong_trigger']
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraTriggers, feature_category: :database do
|
||||
include_examples 'trigger validators', described_class, ['extra_trigger']
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingTriggers, feature_category: :database do
|
||||
missing_triggers = %w[missing_trigger_1 projects_loose_fk_trigger]
|
||||
|
||||
include_examples 'trigger validators', described_class, missing_triggers
|
||||
end
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::AccessTokensMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/personal_access_tokens',
|
||||
title: _('Access Tokens'),
|
||||
icon: 'token',
|
||||
active_routes: { controller: :personal_access_tokens }
|
||||
|
||||
describe '#render?' do
|
||||
subject { described_class.new(context) }
|
||||
|
||||
let_it_be(:user) { build(:user) }
|
||||
|
||||
context 'when personal access tokens are disabled' do
|
||||
before do
|
||||
allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: true)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when personal access tokens are enabled' do
|
||||
before do
|
||||
allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: false)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
it 'renders' do
|
||||
expect(subject.render?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::AccountMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/account',
|
||||
title: _('Account'),
|
||||
icon: 'account',
|
||||
active_routes: { controller: [:accounts, :two_factor_auths] }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::ActiveSessionsMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/active_sessions',
|
||||
title: _('Active Sessions'),
|
||||
icon: 'monitor-lines',
|
||||
active_routes: { controller: :active_sessions }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::ApplicationsMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/applications',
|
||||
title: _('Applications'),
|
||||
icon: 'applications',
|
||||
active_routes: { controller: 'oauth/applications' }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::AuthenticationLogMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/audit_log',
|
||||
title: _('Authentication Log'),
|
||||
icon: 'log',
|
||||
active_routes: { path: 'profiles#audit_log' }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::ChatMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/chat',
|
||||
title: _('Chat'),
|
||||
icon: 'comment',
|
||||
active_routes: { controller: :chat_names }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::EmailsMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/emails',
|
||||
title: _('Emails'),
|
||||
icon: 'mail',
|
||||
active_routes: { controller: :emails }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::GpgKeysMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/gpg_keys',
|
||||
title: _('GPG Keys'),
|
||||
icon: 'key',
|
||||
active_routes: { controller: :gpg_keys }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::NotificationsMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/notifications',
|
||||
title: _('Notifications'),
|
||||
icon: 'notifications',
|
||||
active_routes: { controller: :notifications }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::PasswordMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/password',
|
||||
title: _('Password'),
|
||||
icon: 'lock',
|
||||
active_routes: { controller: :passwords }
|
||||
|
||||
describe '#render?' do
|
||||
subject { described_class.new(context) }
|
||||
|
||||
let_it_be(:user) { build(:user) }
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
context 'when password authentication is enabled' do
|
||||
before do
|
||||
allow(user).to receive(:allow_password_authentication?).and_return(true)
|
||||
end
|
||||
|
||||
it 'renders' do
|
||||
expect(subject.render?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when password authentication is disabled' do
|
||||
before do
|
||||
allow(user).to receive(:allow_password_authentication?).and_return(false)
|
||||
end
|
||||
|
||||
it 'renders' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::PreferencesMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/preferences',
|
||||
title: _('Preferences'),
|
||||
icon: 'preferences',
|
||||
active_routes: { controller: :preferences }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::ProfileMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile',
|
||||
title: _('Profile'),
|
||||
icon: 'profile',
|
||||
active_routes: { path: 'profiles#show' }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::SavedRepliesMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/saved_replies',
|
||||
title: _('Saved Replies'),
|
||||
icon: 'symlink',
|
||||
active_routes: { controller: :saved_replies }
|
||||
|
||||
describe '#render?' do
|
||||
subject { described_class.new(context) }
|
||||
|
||||
let_it_be(:user) { build(:user) }
|
||||
|
||||
context 'when saved replies are enabled' do
|
||||
before do
|
||||
allow(subject).to receive(:saved_replies_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when saved replies are disabled' do
|
||||
before do
|
||||
allow(subject).to receive(:saved_replies_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
it 'renders' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Menus::SshKeysMenu, feature_category: :navigation do
|
||||
it_behaves_like 'User settings menu',
|
||||
link: '/-/profile/keys',
|
||||
title: _('SSH Keys'),
|
||||
icon: 'key',
|
||||
active_routes: { controller: :keys }
|
||||
|
||||
it_behaves_like 'User settings menu #render? method'
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::UserSettings::Panel, feature_category: :navigation do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'implements #super_sidebar_context_header' do
|
||||
expect(subject.super_sidebar_context_header).to eq({ title: _('User settings'), avatar: user.avatar_url })
|
||||
end
|
||||
end
|
||||
|
|
@ -28,7 +28,7 @@ RSpec.describe EnsureTimelogsNoteIdBigintBackfillIsFinishedForGitlabDotCom, feat
|
|||
|
||||
expect(described_class).send(
|
||||
expectation,
|
||||
ensure_bacthed_background_migration_is_finished_for(migration_arguments)
|
||||
ensure_batched_background_migration_is_finished_for(migration_arguments)
|
||||
)
|
||||
|
||||
migrate!
|
||||
|
|
|
|||
|
|
@ -32,6 +32,24 @@ RSpec.describe "Sync project fork", feature_category: :source_code_management do
|
|||
source_project.change_head('feature')
|
||||
end
|
||||
|
||||
context 'when synchronize_fork feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(synchronize_fork: false)
|
||||
end
|
||||
|
||||
it 'does not call the sync service' do
|
||||
expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_mutation_response(:project_sync_fork)).to eq(
|
||||
{
|
||||
'details' => nil,
|
||||
'errors' => ['Feature flag is disabled']
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have permission' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,17 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when DB timeouts occur' do
|
||||
it 'returns a :request_timeout status' do
|
||||
allow(MergeRequestsFinder).to receive(:new).and_raise(ActiveRecord::QueryCanceled)
|
||||
|
||||
path = endpoint_path + '?view=simple'
|
||||
get api(path, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:request_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an array of all merge_requests using simple mode' do
|
||||
path = endpoint_path + '?view=simple'
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ RSpec::Matchers.define :be_finalize_background_migration_of do |migration|
|
|||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :ensure_bacthed_background_migration_is_finished_for do |migration_arguments|
|
||||
RSpec::Matchers.define :ensure_batched_background_migration_is_finished_for do |migration_arguments|
|
||||
define_method :matches? do |klass|
|
||||
expect_next_instance_of(klass) do |instance|
|
||||
expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@ RSpec.shared_examples "index validators" do |validator, expected_result|
|
|||
|
||||
let(:connection) { database_model.connection }
|
||||
|
||||
let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
|
||||
let(:schema) { connection.current_schema }
|
||||
|
||||
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
|
||||
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
|
||||
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
|
||||
|
||||
subject(:result) { validator.new(structure_file, database).execute }
|
||||
|
||||
before do
|
||||
allow(connection).to receive(:exec_query).and_return(query_result)
|
||||
allow(connection).to receive(:select_rows).and_return(database_indexes)
|
||||
end
|
||||
|
||||
it 'returns index inconsistencies' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples "schema objects assertions for" do |stmt_name|
|
||||
let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt }
|
||||
let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns schema object name' do
|
||||
expect(schema_object.name).to eq(name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#statement' do
|
||||
it 'returns schema object statement' do
|
||||
expect(schema_object.statement).to eq(statement)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'trigger validators' do |validator, expected_result|
|
||||
subject(:result) { validator.new(structure_file, database).execute }
|
||||
|
||||
let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
|
||||
let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
|
||||
let(:inconsistency_type) { validator.name.demodulize.underscore }
|
||||
let(:database_name) { 'main' }
|
||||
let(:schema) { 'public' }
|
||||
let(:database_model) { Gitlab::Database.database_base_models[database_name] }
|
||||
let(:connection) { database_model.connection }
|
||||
let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
|
||||
|
||||
let(:database_triggers) do
|
||||
[
|
||||
['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'],
|
||||
['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'],
|
||||
['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()']
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(connection).to receive(:select_rows).and_return(database_triggers)
|
||||
end
|
||||
|
||||
it 'returns trigger inconsistencies' do
|
||||
expect(result.map(&:object_name)).to match_array(expected_result)
|
||||
expect(result.map(&:type)).to all(eql inconsistency_type)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:|
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
subject { described_class.new(context) }
|
||||
|
||||
it 'does not contain any sub menu' do
|
||||
expect(subject.has_items?).to be false
|
||||
end
|
||||
|
||||
it 'renders the correct link' do
|
||||
expect(subject.link).to match link
|
||||
end
|
||||
|
||||
it 'renders the correct title' do
|
||||
expect(subject.title).to eq title
|
||||
end
|
||||
|
||||
it 'renders the correct icon' do
|
||||
expect(subject.sprite_icon).to be icon
|
||||
end
|
||||
|
||||
it 'defines correct active route' do
|
||||
expect(subject.active_routes).to eq active_routes
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'User settings menu #render? method' do
|
||||
describe '#render?' do
|
||||
subject { described_class.new(context) }
|
||||
|
||||
context 'when user is logged in' do
|
||||
let_it_be(:user) { build(:user) }
|
||||
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
|
||||
|
||||
it 'renders' do
|
||||
expect(subject.render?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
|
||||
|
||||
it 'does not render' do
|
||||
expect(subject.render?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue