From 256c5ea115fccfa52f1eb4cac8bf9530eecc1751 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 10 Mar 2023 21:12:47 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/CODEOWNERS | 4 +- .../merge_request_templates/Stable Branch.md | 8 +- GITALY_SERVER_VERSION | 2 +- .../batch_comments/components/draft_note.vue | 1 + .../components/group_select.vue | 1 + .../components/self_monitor_form.vue | 34 +++- app/graphql/mutations/projects/sync_fork.rb | 3 + app/helpers/nav_helper.rb | 2 +- app/helpers/sidebars_helper.rb | 3 + .../layouts/nav/sidebar/_profile.html.haml | 170 +----------------- .../nav/_user_settings_scope_header.html.haml | 4 + .../reduce_sub_batch_size_on_timeouts.yml | 8 + .../development/synchronize_fork.yml | 8 + doc/update/index.md | 9 +- lib/api/merge_requests.rb | 4 + .../batched_migration_job.rb | 15 +- .../background_migration/batched_job.rb | 83 +++++++-- .../batched_migration_wrapper.rb | 13 +- .../sub_batch_timeout_error.rb | 17 ++ .../database/schema_validation/database.rb | 50 +++++- .../database/schema_validation/index.rb | 25 --- .../database/schema_validation/indexes.rb | 37 ---- .../schema_validation/schema_objects/base.rb | 27 +++ .../schema_validation/schema_objects/index.rb | 15 ++ .../schema_objects/trigger.rb | 15 ++ .../schema_validation/structure_sql.rb | 39 +++- .../validators/base_validator.rb | 5 +- .../different_definition_triggers.rb | 22 +++ .../validators/extra_triggers.rb | 19 ++ .../validators/missing_triggers.rb | 19 ++ lib/sidebars/concerns/render_if_logged_in.rb | 11 ++ .../user_settings/menus/access_tokens_menu.rb | 39 ++++ .../user_settings/menus/account_menu.rb | 36 ++++ .../menus/active_sessions_menu.rb | 31 ++++ .../user_settings/menus/applications_menu.rb | 31 ++++ .../menus/authentication_log_menu.rb | 31 ++++ lib/sidebars/user_settings/menus/chat_menu.rb | 31 ++++ .../user_settings/menus/emails_menu.rb | 36 ++++ .../user_settings/menus/gpg_keys_menu.rb | 31 ++++ .../user_settings/menus/notifications_menu.rb | 31 ++++ .../user_settings/menus/password_menu.rb | 39 ++++ .../user_settings/menus/preferences_menu.rb | 31 ++++ .../user_settings/menus/profile_menu.rb | 31 ++++ .../user_settings/menus/saved_replies_menu.rb | 42 +++++ .../user_settings/menus/ssh_keys_menu.rb | 36 ++++ lib/sidebars/user_settings/panel.rb | 51 ++++++ lib/tasks/gitlab/tw/codeowners.rake | 2 +- locale/gitlab.pot | 9 +- qa/qa/page/profile/menu.rb | 24 ++- ..._visits_profile_authentication_log_spec.rb | 2 +- spec/features/signed_commits_spec.rb | 2 +- spec/fixtures/structure.sql | 8 + .../components/group_select_spec.js | 1 + .../self_monitor_form_spec.js.snap | 35 ++++ spec/helpers/sidebars_helper_spec.rb | 4 + .../batched_migration_job_spec.rb | 22 +++ .../background_migration/batched_job_spec.rb | 165 +++++++++++++++++ .../batched_migration_wrapper_spec.rb | 15 +- .../schema_validation/database_spec.rb | 119 ++++++++---- .../database/schema_validation/index_spec.rb | 22 --- .../database/schema_validation/runner_spec.rb | 2 +- .../schema_objects/index_spec.rb | 10 ++ .../schema_objects/trigger_spec.rb | 10 ++ .../schema_validation/structure_sql_spec.rb | 83 ++++++--- .../validators/base_validator_spec.rb | 5 +- .../different_definition_triggers_spec.rb | 8 + .../validators/extra_triggers_spec.rb | 7 + .../validators/missing_triggers_spec.rb | 9 + .../menus/access_tokens_menu_spec.rb | 65 +++++++ .../user_settings/menus/account_menu_spec.rb | 13 ++ .../menus/active_sessions_menu_spec.rb | 13 ++ .../menus/applications_menu_spec.rb | 13 ++ .../menus/authentication_log_menu_spec.rb | 13 ++ .../user_settings/menus/chat_menu_spec.rb | 13 ++ .../user_settings/menus/emails_menu_spec.rb | 13 ++ .../user_settings/menus/gpg_keys_menu_spec.rb | 13 ++ .../menus/notifications_menu_spec.rb | 13 ++ .../user_settings/menus/password_menu_spec.rb | 38 ++++ .../menus/preferences_menu_spec.rb | 13 ++ .../user_settings/menus/profile_menu_spec.rb | 13 ++ .../menus/saved_replies_menu_spec.rb | 65 +++++++ .../user_settings/menus/ssh_keys_menu_spec.rb | 13 ++ spec/lib/sidebars/user_settings/panel_spec.rb | 15 ++ ...ill_is_finished_for_gitlab_dot_com_spec.rb | 2 +- .../mutations/projects/sync_fork_spec.rb | 18 ++ spec/requests/api/merge_requests_spec.rb | 11 ++ .../background_migrations_matchers.rb | 2 +- .../index_validators_shared_examples.rb | 6 +- .../schema_objects_shared_examples.rb | 20 +++ .../trigger_validators_shared_examples.rb | 33 ++++ .../user_settings_menus_shared_examples.rb | 52 ++++++ 91 files changed, 1856 insertions(+), 378 deletions(-) create mode 100644 app/views/shared/nav/_user_settings_scope_header.html.haml create mode 100644 config/feature_flags/development/reduce_sub_batch_size_on_timeouts.yml create mode 100644 config/feature_flags/development/synchronize_fork.yml create mode 100644 lib/gitlab/database/background_migration/sub_batch_timeout_error.rb delete mode 100644 lib/gitlab/database/schema_validation/index.rb delete mode 100644 lib/gitlab/database/schema_validation/indexes.rb create mode 100644 lib/gitlab/database/schema_validation/schema_objects/base.rb create mode 100644 lib/gitlab/database/schema_validation/schema_objects/index.rb create mode 100644 lib/gitlab/database/schema_validation/schema_objects/trigger.rb create mode 100644 lib/gitlab/database/schema_validation/validators/different_definition_triggers.rb create mode 100644 lib/gitlab/database/schema_validation/validators/extra_triggers.rb create mode 100644 lib/gitlab/database/schema_validation/validators/missing_triggers.rb create mode 100644 lib/sidebars/concerns/render_if_logged_in.rb create mode 100644 lib/sidebars/user_settings/menus/access_tokens_menu.rb create mode 100644 lib/sidebars/user_settings/menus/account_menu.rb create mode 100644 lib/sidebars/user_settings/menus/active_sessions_menu.rb create mode 100644 lib/sidebars/user_settings/menus/applications_menu.rb create mode 100644 lib/sidebars/user_settings/menus/authentication_log_menu.rb create mode 100644 lib/sidebars/user_settings/menus/chat_menu.rb create mode 100644 lib/sidebars/user_settings/menus/emails_menu.rb create mode 100644 lib/sidebars/user_settings/menus/gpg_keys_menu.rb create mode 100644 lib/sidebars/user_settings/menus/notifications_menu.rb create mode 100644 lib/sidebars/user_settings/menus/password_menu.rb create mode 100644 lib/sidebars/user_settings/menus/preferences_menu.rb create mode 100644 lib/sidebars/user_settings/menus/profile_menu.rb create mode 100644 lib/sidebars/user_settings/menus/saved_replies_menu.rb create mode 100644 lib/sidebars/user_settings/menus/ssh_keys_menu.rb create mode 100644 lib/sidebars/user_settings/panel.rb delete mode 100644 spec/lib/gitlab/database/schema_validation/index_spec.rb create mode 100644 spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb create mode 100644 spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb create mode 100644 spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb create mode 100644 spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb create mode 100644 spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/account_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/password_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb create mode 100644 spec/lib/sidebars/user_settings/panel_spec.rb create mode 100644 spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 5b8c4f201ca..e6f9fe9767b 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -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 diff --git a/.gitlab/merge_request_templates/Stable Branch.md b/.gitlab/merge_request_templates/Stable Branch.md index e584296cbb1..9f1408e06bc 100644 --- a/.gitlab/merge_request_templates/Stable Branch.md +++ b/.gitlab/merge_request_templates/Stable Branch.md @@ -1,6 +1,10 @@ ## What does this MR do and why? diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5687640cd8a..1a6d66d7a86 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -37fd51c54395a06dc39322606f9a3e4ba2dafa3d +ac7304c0ce981bf97dca39ac1017b31860c043b5 diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue index 9afe8181873..b78874d372c 100644 --- a/app/assets/javascripts/batch_comments/components/draft_note.vue +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -99,6 +99,7 @@ export default { diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue index e7f5211dc25..0e9781d77fe 100644 --- a/app/assets/javascripts/invite_members/components/group_select.vue +++ b/app/assets/javascripts/invite_members/components/group_select.vue @@ -114,6 +114,7 @@ export default { defaultFetchOptions: { exclude_internal: true, active: true, + order_by: 'similarity', }, }; diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue index d9e969e2278..e5a11487c90 100644 --- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue +++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue @@ -1,5 +1,5 @@ @@ -140,6 +162,16 @@ export default { {{ __('Learn more.') }}

+ +
+

diff --git a/app/graphql/mutations/projects/sync_fork.rb b/app/graphql/mutations/projects/sync_fork.rb index bb92f078fae..121c16df87b 100644 --- a/app/graphql/mutations/projects/sync_fork.rb +++ b/app/graphql/mutations/projects/sync_fork.rb @@ -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) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 55a191d85b3..563a58d6eb3 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -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 diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index b4657fce467..9ea30b055c8 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -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) diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 087eca3ba35..d53316442f8 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -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)) diff --git a/app/views/shared/nav/_user_settings_scope_header.html.haml b/app/views/shared/nav/_user_settings_scope_header.html.haml new file mode 100644 index 00000000000..c1601822736 --- /dev/null +++ b/app/views/shared/nav/_user_settings_scope_header.html.haml @@ -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') diff --git a/config/feature_flags/development/reduce_sub_batch_size_on_timeouts.yml b/config/feature_flags/development/reduce_sub_batch_size_on_timeouts.yml new file mode 100644 index 00000000000..507fce9cfe5 --- /dev/null +++ b/config/feature_flags/development/reduce_sub_batch_size_on_timeouts.yml @@ -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 diff --git a/config/feature_flags/development/synchronize_fork.yml b/config/feature_flags/development/synchronize_fork.yml new file mode 100644 index 00000000000..46307136c33 --- /dev/null +++ b/config/feature_flags/development/synchronize_fork.yml @@ -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 diff --git a/doc/update/index.md b/doc/update/index.md index 9c3d5840e75..2117ba07055 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -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 diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index cd46b442b68..e99e8f5421c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -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 diff --git a/lib/gitlab/background_migration/batched_migration_job.rb b/lib/gitlab/background_migration/batched_migration_job.rb index 4039a79cfa7..952e6d01f1a 100644 --- a/lib/gitlab/background_migration/batched_migration_job.rb +++ b/lib/gitlab/background_migration/batched_migration_job.rb @@ -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 diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb index 5f922d87c6f..5147ea92291 100644 --- a/lib/gitlab/database/background_migration/batched_job.rb +++ b/lib/gitlab/database/background_migration/batched_job.rb @@ -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 diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb index f1fc3efae9e..8fdaa685ba9 100644 --- a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb +++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb @@ -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 diff --git a/lib/gitlab/database/background_migration/sub_batch_timeout_error.rb b/lib/gitlab/database/background_migration/sub_batch_timeout_error.rb new file mode 100644 index 00000000000..815dff11e1f --- /dev/null +++ b/lib/gitlab/database/background_migration/sub_batch_timeout_error.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/database.rb b/lib/gitlab/database/schema_validation/database.rb index 15f003a1b0c..07bd02e58e1 100644 --- a/lib/gitlab/database/schema_validation/database.rb +++ b/lib/gitlab/database/schema_validation/database.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/index.rb b/lib/gitlab/database/schema_validation/index.rb deleted file mode 100644 index af0d5f31f4e..00000000000 --- a/lib/gitlab/database/schema_validation/index.rb +++ /dev/null @@ -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 diff --git a/lib/gitlab/database/schema_validation/indexes.rb b/lib/gitlab/database/schema_validation/indexes.rb deleted file mode 100644 index b7c3705bde9..00000000000 --- a/lib/gitlab/database/schema_validation/indexes.rb +++ /dev/null @@ -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 diff --git a/lib/gitlab/database/schema_validation/schema_objects/base.rb b/lib/gitlab/database/schema_validation/schema_objects/base.rb new file mode 100644 index 00000000000..b0c8eb087dd --- /dev/null +++ b/lib/gitlab/database/schema_validation/schema_objects/base.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/schema_objects/index.rb b/lib/gitlab/database/schema_validation/schema_objects/index.rb new file mode 100644 index 00000000000..28d61b18266 --- /dev/null +++ b/lib/gitlab/database/schema_validation/schema_objects/index.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/schema_objects/trigger.rb b/lib/gitlab/database/schema_validation/schema_objects/trigger.rb new file mode 100644 index 00000000000..508e6b27ed3 --- /dev/null +++ b/lib/gitlab/database/schema_validation/schema_objects/trigger.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/structure_sql.rb b/lib/gitlab/database/schema_validation/structure_sql.rb index 306cc22f9f5..cb62af8d8b8 100644 --- a/lib/gitlab/database/schema_validation/structure_sql.rb +++ b/lib/gitlab/database/schema_validation/structure_sql.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/validators/base_validator.rb b/lib/gitlab/database/schema_validation/validators/base_validator.rb index 1fd3d7c85c4..14995b5f378 100644 --- a/lib/gitlab/database/schema_validation/validators/base_validator.rb +++ b/lib/gitlab/database/schema_validation/validators/base_validator.rb @@ -15,8 +15,11 @@ module Gitlab def self.all_validators [ ExtraIndexes, + ExtraTriggers, MissingIndexes, - DifferentDefinitionIndexes + MissingTriggers, + DifferentDefinitionIndexes, + DifferentDefinitionTriggers ] end diff --git a/lib/gitlab/database/schema_validation/validators/different_definition_triggers.rb b/lib/gitlab/database/schema_validation/validators/different_definition_triggers.rb new file mode 100644 index 00000000000..efb87a70ca8 --- /dev/null +++ b/lib/gitlab/database/schema_validation/validators/different_definition_triggers.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/validators/extra_triggers.rb b/lib/gitlab/database/schema_validation/validators/extra_triggers.rb new file mode 100644 index 00000000000..f03bb49526c --- /dev/null +++ b/lib/gitlab/database/schema_validation/validators/extra_triggers.rb @@ -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 diff --git a/lib/gitlab/database/schema_validation/validators/missing_triggers.rb b/lib/gitlab/database/schema_validation/validators/missing_triggers.rb new file mode 100644 index 00000000000..c7137c68c1c --- /dev/null +++ b/lib/gitlab/database/schema_validation/validators/missing_triggers.rb @@ -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 diff --git a/lib/sidebars/concerns/render_if_logged_in.rb b/lib/sidebars/concerns/render_if_logged_in.rb new file mode 100644 index 00000000000..221af7df23b --- /dev/null +++ b/lib/sidebars/concerns/render_if_logged_in.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Sidebars + module Concerns + module RenderIfLoggedIn + def render? + !!context.current_user + end + end + end +end diff --git a/lib/sidebars/user_settings/menus/access_tokens_menu.rb b/lib/sidebars/user_settings/menus/access_tokens_menu.rb new file mode 100644 index 00000000000..f52be22e044 --- /dev/null +++ b/lib/sidebars/user_settings/menus/access_tokens_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/account_menu.rb b/lib/sidebars/user_settings/menus/account_menu.rb new file mode 100644 index 00000000000..a26dee83da3 --- /dev/null +++ b/lib/sidebars/user_settings/menus/account_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/active_sessions_menu.rb b/lib/sidebars/user_settings/menus/active_sessions_menu.rb new file mode 100644 index 00000000000..f806c04e77c --- /dev/null +++ b/lib/sidebars/user_settings/menus/active_sessions_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/applications_menu.rb b/lib/sidebars/user_settings/menus/applications_menu.rb new file mode 100644 index 00000000000..c71f9a9660b --- /dev/null +++ b/lib/sidebars/user_settings/menus/applications_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/authentication_log_menu.rb b/lib/sidebars/user_settings/menus/authentication_log_menu.rb new file mode 100644 index 00000000000..c5a27acf1fd --- /dev/null +++ b/lib/sidebars/user_settings/menus/authentication_log_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/chat_menu.rb b/lib/sidebars/user_settings/menus/chat_menu.rb new file mode 100644 index 00000000000..54795c29b3f --- /dev/null +++ b/lib/sidebars/user_settings/menus/chat_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/emails_menu.rb b/lib/sidebars/user_settings/menus/emails_menu.rb new file mode 100644 index 00000000000..3b6b4ae1daf --- /dev/null +++ b/lib/sidebars/user_settings/menus/emails_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/gpg_keys_menu.rb b/lib/sidebars/user_settings/menus/gpg_keys_menu.rb new file mode 100644 index 00000000000..89e447aa277 --- /dev/null +++ b/lib/sidebars/user_settings/menus/gpg_keys_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/notifications_menu.rb b/lib/sidebars/user_settings/menus/notifications_menu.rb new file mode 100644 index 00000000000..f7ea0de0cc9 --- /dev/null +++ b/lib/sidebars/user_settings/menus/notifications_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/password_menu.rb b/lib/sidebars/user_settings/menus/password_menu.rb new file mode 100644 index 00000000000..9a53e0c727e --- /dev/null +++ b/lib/sidebars/user_settings/menus/password_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/preferences_menu.rb b/lib/sidebars/user_settings/menus/preferences_menu.rb new file mode 100644 index 00000000000..b6b94ec1ad9 --- /dev/null +++ b/lib/sidebars/user_settings/menus/preferences_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/profile_menu.rb b/lib/sidebars/user_settings/menus/profile_menu.rb new file mode 100644 index 00000000000..73119070586 --- /dev/null +++ b/lib/sidebars/user_settings/menus/profile_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/saved_replies_menu.rb b/lib/sidebars/user_settings/menus/saved_replies_menu.rb new file mode 100644 index 00000000000..25c8b2f8eb2 --- /dev/null +++ b/lib/sidebars/user_settings/menus/saved_replies_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/menus/ssh_keys_menu.rb b/lib/sidebars/user_settings/menus/ssh_keys_menu.rb new file mode 100644 index 00000000000..8d92db0147a --- /dev/null +++ b/lib/sidebars/user_settings/menus/ssh_keys_menu.rb @@ -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 diff --git a/lib/sidebars/user_settings/panel.rb b/lib/sidebars/user_settings/panel.rb new file mode 100644 index 00000000000..14a52a8fb23 --- /dev/null +++ b/lib/sidebars/user_settings/panel.rb @@ -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') diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index a111cbba46d..f3b409da135 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -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'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 06b3318e269..ac9ac545feb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/page/profile/menu.rb b/qa/qa/page/profile/menu.rb index 947fa2fec0f..651603a77db 100644 --- a/qa/qa/page/profile/menu.rb +++ b/qa/qa/page/profile/menu.rb @@ -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 diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb index 90f24c5b866..ac0ed91468c 100644 --- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb +++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb @@ -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 diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 5d9b451cdf6..541f94a9340 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -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) diff --git a/spec/fixtures/structure.sql b/spec/fixtures/structure.sql index 49061dfa8ea..800c33bb9b9 100644 --- a/spec/fixtures/structure.sql +++ b/spec/fixtures/structure.sql @@ -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(); diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js index 75b91c89bd5..a1ca9a69926 100644 --- a/spec/frontend/invite_members/components/group_select_spec.js +++ b/spec/frontend/invite_members/components/group_select_spec.js @@ -61,6 +61,7 @@ describe('GroupSelect', () => { expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, { exclude_internal: true, active: true, + order_by: 'similarity', }); }); diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap index efe3f7e8dbf..c278bb4579f 100644 --- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap +++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap @@ -40,6 +40,41 @@ exports[`self-monitor component When the self-monitor project has not been creat

+ +
+ Self-monitoring was + + deprecated + + in GitLab 14.9, and is + + scheduled for removal + + in GitLab 16.0. For information on a possible replacement, + + learn more about Opstrace + + . +
+
+
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index d03c97442ad..f23de35877b 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb index faaaccfdfaf..781bf93dd85 100644 --- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb @@ -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) diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb index cc9f3d5b7f1..073a30e7839 100644 --- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index f3a292abbae..8d74d16f4e5 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb index 68e12c90819..eadaf683a29 100644 --- a/spec/lib/gitlab/database/schema_validation/database_spec.rb +++ b/spec/lib/gitlab/database/schema_validation/database_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/index_spec.rb b/spec/lib/gitlab/database/schema_validation/index_spec.rb deleted file mode 100644 index 297211d79ed..00000000000 --- a/spec/lib/gitlab/database/schema_validation/index_spec.rb +++ /dev/null @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/runner_spec.rb b/spec/lib/gitlab/database/schema_validation/runner_spec.rb index 13980cb148b..ddbdedcd8b4 100644 --- a/spec/lib/gitlab/database/schema_validation/runner_spec.rb +++ b/spec/lib/gitlab/database/schema_validation/runner_spec.rb @@ -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 } diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb new file mode 100644 index 00000000000..1aaa994e3bb --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb new file mode 100644 index 00000000000..8000a54ee27 --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb b/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb index 00566d884cf..cc0bd4125ef 100644 --- a/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb +++ b/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb index cf5207aee95..2f38c25cf68 100644 --- a/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb +++ b/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb new file mode 100644 index 00000000000..4d065929708 --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb new file mode 100644 index 00000000000..d2e1c18a1ab --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb new file mode 100644 index 00000000000..87bc3ded808 --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb new file mode 100644 index 00000000000..fa33e7bedfb --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb new file mode 100644 index 00000000000..d5810d9c5ae --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb new file mode 100644 index 00000000000..be5f826ee58 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb new file mode 100644 index 00000000000..eeda4fb844c --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb new file mode 100644 index 00000000000..33be5050c37 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb new file mode 100644 index 00000000000..2a0587e2504 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb new file mode 100644 index 00000000000..2f16c68e601 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb new file mode 100644 index 00000000000..1f4340ad29c --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb new file mode 100644 index 00000000000..282324056d4 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb new file mode 100644 index 00000000000..168019fea5d --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb new file mode 100644 index 00000000000..83a67a40081 --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb new file mode 100644 index 00000000000..8410ba7cfcd --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb new file mode 100644 index 00000000000..ea1a2a3539f --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb new file mode 100644 index 00000000000..8c781cc743b --- /dev/null +++ b/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb @@ -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 diff --git a/spec/lib/sidebars/user_settings/panel_spec.rb b/spec/lib/sidebars/user_settings/panel_spec.rb new file mode 100644 index 00000000000..aa05d99912a --- /dev/null +++ b/spec/lib/sidebars/user_settings/panel_spec.rb @@ -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 diff --git a/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb b/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb index 41864c25357..9fc04f711a0 100644 --- a/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb +++ b/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb @@ -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! diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb index 33074029a69..a77c026dd06 100644 --- a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb +++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb @@ -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) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 19a630e5218..81815fdab62 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -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' diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index 041958deeea..97993b158c8 100644 --- a/spec/support/matchers/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb @@ -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) diff --git a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb index 6b0ea9421de..6f0cede7130 100644 --- a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb new file mode 100644 index 00000000000..d5ecab0cb6b --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb new file mode 100644 index 00000000000..13a112275c2 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb new file mode 100644 index 00000000000..b91386d1935 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb @@ -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