From 9e16447c7c4cec7373bb2df0d94b4dc8b8300b08 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 28 Jul 2021 06:09:29 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITLAB_KAS_VERSION | 2 +- .../components/performance_bar_app.vue | 2 +- .../mailgun/permanent_failures_controller.rb | 65 +++++++++ app/mailers/emails/members.rb | 6 +- app/services/members/mailgun.rb | 8 ++ .../mailgun/process_webhook_service.rb | 39 ++++++ .../application_settings/_mailgun.html.haml | 2 - .../development/gitaly_backup.yml | 2 +- .../development/mailgun_events_receiver.yml | 8 -- config/routes.rb | 1 + config/routes/members.rb | 7 + ...2928_add_invite_email_success_to_member.rb | 13 ++ db/schema_migrations/20210719192928 | 1 + db/structure.sql | 1 + doc/administration/integration/mailgun.md | 41 ++++++ .../monitoring/performance/performance_bar.md | 15 +- doc/api/settings.md | 2 +- doc/raketasks/backup_restore.md | 44 ------ doc/user/admin_area/settings/index.md | 1 + .../img/compliance_report_v13_11.png | Bin 15732 -> 0 bytes .../compliance/compliance_report/index.md | 24 ++-- lib/gitlab/database/connection.rb | 9 +- lib/peek/views/active_record.rb | 13 +- lib/tasks/gitlab/backup.rake | 2 +- spec/features/admin/admin_settings_spec.rb | 29 ++-- spec/lib/gitlab/database/connection_spec.rb | 13 ++ .../import_export/safe_model_attributes.yml | 1 + spec/lib/peek/views/active_record_spec.rb | 57 ++++++-- spec/mailers/notify_spec.rb | 8 +- .../members/mailgun/permanent_failure_spec.rb | 128 ++++++++++++++++++ .../mailgun/process_webhook_service_spec.rb | 42 ++++++ 31 files changed, 467 insertions(+), 119 deletions(-) create mode 100644 app/controllers/members/mailgun/permanent_failures_controller.rb create mode 100644 app/services/members/mailgun.rb create mode 100644 app/services/members/mailgun/process_webhook_service.rb delete mode 100644 config/feature_flags/development/mailgun_events_receiver.yml create mode 100644 config/routes/members.rb create mode 100644 db/migrate/20210719192928_add_invite_email_success_to_member.rb create mode 100644 db/schema_migrations/20210719192928 create mode 100644 doc/administration/integration/mailgun.md delete mode 100644 doc/user/compliance/compliance_report/img/compliance_report_v13_11.png create mode 100644 spec/requests/members/mailgun/permanent_failure_spec.rb create mode 100644 spec/services/members/mailgun/process_webhook_service_spec.rb diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 7b3b6e02bb3..26f2bbc1975 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -14.1.0 +14.1.1 diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 214e1729bf8..670b0535ca3 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -40,7 +40,7 @@ export default { metric: 'active-record', title: 'pg', header: s__('PerformanceBar|SQL queries'), - keys: ['sql', 'cached', 'transaction', 'db_role'], + keys: ['sql', 'cached', 'transaction', 'db_role', 'db_config_name'], }, { metric: 'bullet', diff --git a/app/controllers/members/mailgun/permanent_failures_controller.rb b/app/controllers/members/mailgun/permanent_failures_controller.rb new file mode 100644 index 00000000000..685faa34694 --- /dev/null +++ b/app/controllers/members/mailgun/permanent_failures_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Members + module Mailgun + class PermanentFailuresController < ApplicationController + respond_to :json + + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + before_action :ensure_feature_enabled! + before_action :authenticate_signature! + before_action :validate_invite_email! + + feature_category :authentication_and_authorization + + def create + webhook_processor.execute + + head :ok + end + + private + + def ensure_feature_enabled! + render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled? + end + + def authenticate_signature! + access_denied! unless valid_signature? + end + + def valid_signature? + return false if Gitlab::CurrentSettings.mailgun_signing_key.blank? + + # per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks + digest = OpenSSL::Digest.new('SHA256') + data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join + + hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data) + + ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest) + end + + def validate_invite_email! + # permanent_failures webhook does not provide a way to filter failures, so we'll get them all on this endpoint + # and we only care about our invite_emails + render_406 unless payload[:tags]&.include?(::Members::Mailgun::INVITE_EMAIL_TAG) + end + + def webhook_processor + ::Members::Mailgun::ProcessWebhookService.new(payload) + end + + def payload + @payload ||= params.permit!['event-data'] + end + + def render_406 + # failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks + head :not_acceptable + end + end + end +end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index d1870065845..738794a94e7 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -150,10 +150,10 @@ module Emails end def invite_email_headers - if Gitlab.dev_env_or_com? + if Gitlab::CurrentSettings.mailgun_events_enabled? { - 'X-Mailgun-Tag' => 'invite_email', - 'X-Mailgun-Variables' => { 'invite_token' => @token }.to_json + 'X-Mailgun-Tag' => ::Members::Mailgun::INVITE_EMAIL_TAG, + 'X-Mailgun-Variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => @token }.to_json } else {} diff --git a/app/services/members/mailgun.rb b/app/services/members/mailgun.rb new file mode 100644 index 00000000000..43fb5a14ef1 --- /dev/null +++ b/app/services/members/mailgun.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Members + module Mailgun + INVITE_EMAIL_TAG = 'invite_email' + INVITE_EMAIL_TOKEN_KEY = :invite_token + end +end diff --git a/app/services/members/mailgun/process_webhook_service.rb b/app/services/members/mailgun/process_webhook_service.rb new file mode 100644 index 00000000000..e359a83ad42 --- /dev/null +++ b/app/services/members/mailgun/process_webhook_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Members + module Mailgun + class ProcessWebhookService + ProcessWebhookServiceError = Class.new(StandardError) + + def initialize(payload) + @payload = payload + end + + def execute + @member = Member.find_by_invite_token(invite_token) + update_member_and_log if member + rescue ProcessWebhookServiceError => e + Gitlab::ErrorTracking.track_exception(e) + end + + private + + attr_reader :payload, :member + + def update_member_and_log + log_update_event if member.update(invite_email_success: false) + end + + def log_update_event + Gitlab::AppLogger.info "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}" + end + + def invite_token + # may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this + # gets more complex + payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) || + raise(ProcessWebhookServiceError, "Failed to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} in user-variables: #{payload}") + end + end + end +end diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml index 6204f7df5dc..40b4d5cac6d 100644 --- a/app/views/admin/application_settings/_mailgun.html.haml +++ b/app/views/admin/application_settings/_mailgun.html.haml @@ -1,5 +1,3 @@ -- return unless Feature.enabled?(:mailgun_events_receiver) - - expanded = integration_expanded?('mailgun_') %section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/config/feature_flags/development/gitaly_backup.yml b/config/feature_flags/development/gitaly_backup.yml index 67552d39d92..4f7a0a4baf9 100644 --- a/config/feature_flags/development/gitaly_backup.yml +++ b/config/feature_flags/development/gitaly_backup.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333034 milestone: '14.0' type: development group: group::gitaly -default_enabled: true +default_enabled: false diff --git a/config/feature_flags/development/mailgun_events_receiver.yml b/config/feature_flags/development/mailgun_events_receiver.yml deleted file mode 100644 index 119d8d34f21..00000000000 --- a/config/feature_flags/development/mailgun_events_receiver.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: mailgun_events_receiver -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64249 -rollout_issue_url: -milestone: '14.1' -type: development -group: group::expansion -default_enabled: false diff --git a/config/routes.rb b/config/routes.rb index a4404f9d3a8..ff979d7da10 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -221,6 +221,7 @@ Rails.application.routes.draw do draw :snippets draw :profile + draw :members # Product analytics collector match '/collector/i', to: ProductAnalytics::CollectorApp.new, via: :all diff --git a/config/routes/members.rb b/config/routes/members.rb new file mode 100644 index 00000000000..e84f0987171 --- /dev/null +++ b/config/routes/members.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +namespace :members do + namespace :mailgun do + resources :permanent_failures, only: [:create] + end +end diff --git a/db/migrate/20210719192928_add_invite_email_success_to_member.rb b/db/migrate/20210719192928_add_invite_email_success_to_member.rb new file mode 100644 index 00000000000..40feb13a564 --- /dev/null +++ b/db/migrate/20210719192928_add_invite_email_success_to_member.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddInviteEmailSuccessToMember < ActiveRecord::Migration[6.1] + def up + unless column_exists?(:members, :invite_email_success) + add_column :members, :invite_email_success, :boolean, null: false, default: true + end + end + + def down + remove_column :members, :invite_email_success + end +end diff --git a/db/schema_migrations/20210719192928 b/db/schema_migrations/20210719192928 new file mode 100644 index 00000000000..b15de2220ed --- /dev/null +++ b/db/schema_migrations/20210719192928 @@ -0,0 +1 @@ +eed403573697ac7f454ce47d6e4ab3561a10a62177caaaea40d5d70953068175 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9f631d08b93..a9b56afdfd0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14708,6 +14708,7 @@ CREATE TABLE members ( expires_at date, ldap boolean DEFAULT false NOT NULL, override boolean DEFAULT false NOT NULL, + invite_email_success boolean DEFAULT true NOT NULL, state smallint DEFAULT 0 ); diff --git a/doc/administration/integration/mailgun.md b/doc/administration/integration/mailgun.md new file mode 100644 index 00000000000..6486cc9de04 --- /dev/null +++ b/doc/administration/integration/mailgun.md @@ -0,0 +1,41 @@ +--- +stage: Growth +group: Expansion +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: reference, howto +--- + +# Mailgun and GitLab **(FREE SELF)** + +When you use Mailgun to send emails for your GitLab instance and [Mailgun](https://www.mailgun.com/) +integration is enabled and configured in GitLab, you can receive their webhook for +permanent invite email failures. To set up the integration, you must: + +1. [Configure your Mailgun domain](#configure-your-mailgun-domain). +1. [Enable Mailgun integration](#enable-mailgun-integration). + +After completing the integration, Mailgun `permanent_failure` webhooks are sent to your GitLab instance. + +## Configure your Mailgun domain + +Before you can enable Mailgun in GitLab, set up your own Mailgun permanent failure endpoint to receive the webhooks. + +Using the [Mailgun webhook guide](https://www.mailgun.com/blog/a-guide-to-using-mailguns-webhooks/): + +1. Add a webhook with the **Event type** set to **Permanent Failure**. +1. Fill in the URL of your instance and include the `/-/members/mailgun/permanent_failures` path. + - Example: `https://myinstance.gitlab.com/-/members/mailgun/permanent_failures` + +## Enable Mailgun integration + +After configuring your Mailgun domain for the permanent failures endpoint, +you're ready to enable the Mailgun integration: + +1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user. +1. On the top bar, select **Menu >** **{admin}** **Admin**. +1. In the left sidebar, go to **Settings > General** and expand the **Mailgun** section. +1. Select the **Enable Mailgun** check box. +1. Enter the Mailgun HTTP webhook signing key as described in + [the Mailgun documentation](https://documentation.mailgun.com/en/latest/user_manual.html#webhooks) and + shown in the [API security](https://app.mailgun.com/app/account/security/api_keys) section for your Mailgun account. +1. Select **Save changes**. diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md index 6d806f9ee2a..7440c5a95d9 100644 --- a/doc/administration/monitoring/performance/performance_bar.md +++ b/doc/administration/monitoring/performance/performance_bar.md @@ -21,7 +21,20 @@ From left to right, the performance bar displays: - **Current Host**: the current host serving the page. - **Database queries**: the time taken (in milliseconds) and the total number of database queries, displayed in the format `00ms / 00 (00 cached) pg`. Click to display - a modal window with more details. + a modal window with more details. You can use this to see the following + details for each query: + - **In a transaction**: shows up below the query if it was executed in + the context of a transaction + - **Role**: shows up when [database load + balancing](../../database_load_balancing.md) is enabled. It shows + which server role was used for the query. "Primary" means that the query + was sent to the read/write primary server. "Replica" means it was sent + to a read-only replica. + - **Config name**: shows up only when the + `multiple_database_metrics` feature flag is enabled. This is used to + distinguish between different databases configured for different GitLab + features. The name shown is the same name used to configure database + connections in GitLab. - **Gitaly calls**: the time taken (in milliseconds) and the total number of [Gitaly](../../gitaly/index.md) calls. Click to display a modal window with more details. diff --git a/doc/api/settings.md b/doc/api/settings.md index e3366cf176c..671a9c008fc 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -328,7 +328,7 @@ listed in the descriptions of the relevant settings. | `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.| | `keep_latest_artifact` | boolean | no | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. | | `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. | -| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook | +| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. | | `mailgun_events_enabled` | boolean | no | Enable Mailgun event receiver. | | `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode. | | `maintenance_mode` | boolean | no | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. | diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 02afe8da6d7..b393be18910 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1472,47 +1472,3 @@ If this happens, examine the following: - Confirm there is sufficient disk space for the Gzip operation. - If NFS is being used, check if the mount option `timeout` is set. The default is `600`, and changing this to smaller values results in this error. - -### `gitaly-backup` for repository backup and restore **(FREE SELF)** - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333034) in GitLab 14.2. -> - [Deployed behind a feature flag](../user/feature_flags.md), enabled by default. -> - Recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-or-enable-gitaly-backup). - -There can be -[risks when disabling released features](../user/feature_flags.md#risks-when-disabling-released-features). -Refer to this feature's version history for more details. - -`gitaly-backup` is used by the backup Rake task to create and restore repository backups from Gitaly. -`gitaly-backup` replaces the previous backup method that directly calls RPCs on Gitaly from GitLab. - -The backup Rake task must be able to find this executable. It can be configured in Omnibus GitLab packages: - -1. Add the following to `/etc/gitlab/gitlab.rb`: - - ```ruby - gitlab_rails['backup_gitaly_backup_path'] = '/path/to/gitaly-backup' - ``` - -1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) - for the changes to take effect - -#### Disable or enable `gitaly-backup` - -`gitaly-backup` is under development but ready for production use. -It is deployed behind a feature flag that is **enabled by default**. -[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) -can opt to disable it. - -To disable it: - -```ruby -Feature.disable(:gitaly_backup) -``` - -To enable it: - -```ruby -Feature.enable(:gitaly_backup) -``` diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index 6f8aa6a2e04..92b8cd03009 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -39,6 +39,7 @@ To access the default page for Admin Area settings: | ------ | ----------- | | [Elasticsearch](../../../integration/elasticsearch.md#enabling-advanced-search) | Elasticsearch integration. Elasticsearch AWS IAM. | | [Kroki](../../../administration/integration/kroki.md#enable-kroki-in-gitlab) | Allow rendering of diagrams in AsciiDoc and Markdown documents using [kroki.io](https://kroki.io). | +| [Mailgun](../../../administration/integration/mailgun.md) | Enable your GitLab instance to receive invite email bounce events from Mailgun, if it is your email provider. | | [PlantUML](../../../administration/integration/plantuml.md) | Allow rendering of PlantUML diagrams in documents. | | [Slack application](../../../user/project/integrations/gitlab_slack_application.md#configuration) **(FREE SAAS)** | Slack integration allows you to interact with GitLab via slash commands in a chat window. This option is only available on GitLab.com, though it may be [available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). | | [Third party offers](third_party_offers.md) | Control the display of third party offers. | diff --git a/doc/user/compliance/compliance_report/img/compliance_report_v13_11.png b/doc/user/compliance/compliance_report/img/compliance_report_v13_11.png deleted file mode 100644 index 73a5c92670af5d9a39939bcea16ad2872d32d9ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15732 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 G{C@z_!0X}w diff --git a/doc/user/compliance/compliance_report/index.md b/doc/user/compliance/compliance_report/index.md index d07b14de545..68cd4a022ee 100644 --- a/doc/user/compliance/compliance_report/index.md +++ b/doc/user/compliance/compliance_report/index.md @@ -7,21 +7,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Compliance report **(ULTIMATE)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36524) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.8. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36524) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.8 as Compliance Dashboard. > - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/299360) to compliance report in GitLab 14.2. -The compliance report gives you the ability to see a group's merge request activity -by providing a high-level view for all projects in the group. For example, code approved -for merging into production. +Compliance report gives you the ability to see a group's merge request activity. It provides a +high-level view for all projects in the group. For example, code approved for merging into +production. -## Overview - -To access the compliance report for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu. - -![Compliance report](img/compliance_report_v13_11.png) +To access compliance report for a group, go to **{shield}** **Security & Compliance > Compliance** +on the group's menu. NOTE: -The compliance report shows only the latest MR on each project. +Compliance report shows only the latest merge request on each project. ## Merge request drawer @@ -67,7 +64,7 @@ Our criteria for the separation of duties is as follows: - [A merge request committer is **not** allowed to approve a merge request they have added commits to](../../project/merge_requests/approvals/settings.md#prevent-committers-from-approving-their-own-work) - [The minimum number of approvals required to merge a merge request is **at least** two](../../project/merge_requests/approvals/rules.md) -The "Approval status" column shows you, at a glance, whether a merge request is complying with the above. +The **Approval status** column shows you at a glance whether a merge request is complying with the above. This column has four states: | State | Description | @@ -77,8 +74,7 @@ This column has four states: | ![Warning](img/warning_icon_v13_3.png) | The merge request complies with **some** of the above criteria | | ![Success](img/success_icon_v13_3.png) | The merge request complies with **all** of the above criteria | -If you do not see the success icon in your compliance report; please review the above criteria for the merge requests -project to make sure it complies with the separation of duties described above. +If you see a non-success state, review the criteria for the merge request's project to ensure it complies with the separation of duties. ## Chain of Custody report **(ULTIMATE)** @@ -87,7 +83,7 @@ project to make sure it complies with the separation of duties described above. The Chain of Custody report allows customers to export a list of merge commits within the group. The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA, merge request author, merge request ID, merge user, pipeline ID, group name, project name, and merge request approvers. -Depending on the merge strategy, the merge commit SHA can either be a merge commit, squash commit or a diff head commit. +Depending on the merge strategy, the merge commit SHA can be a merge commit, squash commit, or a diff head commit. To download the Chain of Custody report, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu and click **List of all merge commits** diff --git a/lib/gitlab/database/connection.rb b/lib/gitlab/database/connection.rb index b8680a3331c..965b4876642 100644 --- a/lib/gitlab/database/connection.rb +++ b/lib/gitlab/database/connection.rb @@ -34,9 +34,12 @@ module Gitlab Gitlab::Runtime.max_threads + headroom end + def uncached_config + scope.connection_db_config.configuration_hash.with_indifferent_access + end + def config - @config ||= - scope.connection_db_config.configuration_hash.with_indifferent_access + @config ||= uncached_config end def pool_size @@ -69,7 +72,7 @@ module Gitlab # Disables prepared statements for the current database connection. def disable_prepared_statements - scope.establish_connection(config.merge(prepared_statements: false)) + scope.establish_connection(uncached_config.merge(prepared_statements: false)) end def read_only? diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb index 8e1200338c2..6d2c9a86c62 100644 --- a/lib/peek/views/active_record.rb +++ b/lib/peek/views/active_record.rb @@ -66,7 +66,8 @@ module Peek backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller), cached: data[:cached] ? 'Cached' : '', transaction: data[:connection].transaction_open? ? 'In a transaction' : '', - db_role: db_role(data) + db_role: db_role(data), + db_config_name: "Config name: #{::Gitlab::Database.db_config_name(data[:connection])}" } end @@ -76,7 +77,15 @@ module Peek role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(data[:connection]) || ::Gitlab::Database::LoadBalancing::ROLE_UNKNOWN - role.to_s.capitalize + "Role: #{role.to_s.capitalize}" + end + + def format_call_details(call) + if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml) + super + else + super.except(:db_config_name) + end end end end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 1a65be04d09..ed74dd472ff 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -297,7 +297,7 @@ namespace :gitlab do end def repository_backup_strategy - if Feature.enabled?(:gitaly_backup, default_enabled: :yaml) + if Feature.enabled?(:gitaly_backup) max_concurrency = ENV['GITLAB_BACKUP_MAX_CONCURRENCY'].presence max_storage_concurrency = ENV['GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY'].presence Backup::GitalyBackup.new(progress, parallel: max_concurrency, parallel_storage: max_storage_concurrency) diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index fe4c532060b..cc65e5753c0 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -269,10 +269,7 @@ RSpec.describe 'Admin updates settings' do end context 'Integrations page' do - let(:mailgun_events_receiver_enabled) { true } - before do - stub_feature_flags(mailgun_events_receiver: mailgun_events_receiver_enabled) visit general_admin_application_settings_path end @@ -286,26 +283,16 @@ RSpec.describe 'Admin updates settings' do expect(current_settings.hide_third_party_offers).to be true end - context 'when mailgun_events_receiver feature flag is enabled' do - it 'enabling Mailgun events', :aggregate_failures do - page.within('.as-mailgun') do - check 'Enable Mailgun event receiver' - fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY' - click_button 'Save changes' - end - - expect(page).to have_content 'Application settings saved successfully' - expect(current_settings.mailgun_events_enabled).to be true - expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY' + it 'enabling Mailgun events', :aggregate_failures do + page.within('.as-mailgun') do + check 'Enable Mailgun event receiver' + fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY' + click_button 'Save changes' end - end - context 'when mailgun_events_receiver feature flag is disabled' do - let(:mailgun_events_receiver_enabled) { false } - - it 'does not have mailgun' do - expect(page).not_to have_selector('.as-mailgun') - end + expect(page).to have_content 'Application settings saved successfully' + expect(current_settings.mailgun_events_enabled).to be true + expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY' end end diff --git a/spec/lib/gitlab/database/connection_spec.rb b/spec/lib/gitlab/database/connection_spec.rb index 21ef1555dce..d4c6b3fdd4e 100644 --- a/spec/lib/gitlab/database/connection_spec.rb +++ b/spec/lib/gitlab/database/connection_spec.rb @@ -140,6 +140,19 @@ RSpec.describe Gitlab::Database::Connection do expect(connection.scope.connection.prepared_statements).to eq(false) end + + context 'with dynamic connection pool size' do + before do + connection.scope.establish_connection(connection.config.merge(pool: 7)) + end + + it 'retains the set pool size' do + connection.disable_prepared_statements + + expect(connection.scope.connection.prepared_statements).to eq(false) + expect(connection.scope.connection.pool.size).to eq(7) + end + end end describe '#read_only?' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 77d126e012e..10162ade48b 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -167,6 +167,7 @@ ProjectMember: - expires_at - ldap - override +- invite_email_success User: - id - username diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb index e5aae2822ed..b9d977c38e4 100644 --- a/spec/lib/peek/views/active_record_spec.rb +++ b/spec/lib/peek/views/active_record_spec.rb @@ -52,6 +52,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do allow(connection_primary_1).to receive(:transaction_open?).and_return(false) allow(connection_primary_2).to receive(:transaction_open?).and_return(true) allow(connection_unknown).to receive(:transaction_open?).and_return(false) + allow(::Gitlab::Database).to receive(:db_config_name).and_return('the_db_config_name') end context 'when database load balancing is not enabled' do @@ -77,32 +78,48 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do cached: '', transaction: '', duration: 1000.0, - sql: 'SELECT * FROM users WHERE id = 10' + sql: 'SELECT * FROM users WHERE id = 10', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), cached: 'Cached', transaction: '', duration: 2000.0, - sql: 'SELECT * FROM users WHERE id = 10' + sql: 'SELECT * FROM users WHERE id = 10', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), cached: '', transaction: 'In a transaction', duration: 3000.0, - sql: 'UPDATE users SET admin = true WHERE id = 10' + sql: 'UPDATE users SET admin = true WHERE id = 10', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), cached: '', transaction: '', duration: 4000.0, - sql: 'SELECT VERSION()' + sql: 'SELECT VERSION()', + db_config_name: "Config name: the_db_config_name" ) ) ) end + + context 'when the multiple_database_metrics feature flag is disabled' do + before do + stub_feature_flags(multiple_database_metrics: false) + end + + it 'does not include db_config_name field' do + ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1) + + expect(subject.results[:details][0][:db_config_name]).to be_nil + end + end end context 'when database load balancing is enabled' do @@ -114,7 +131,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_unknown).and_return(nil) end - it 'includes db role data' do + it 'includes db role data and db_config_name name' do Timecop.freeze(2021, 2, 23, 10, 0) do ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1) ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2) @@ -127,9 +144,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do summary: { "Cached" => 1, "In a transaction" => 1, - "Primary" => 2, - "Replica" => 1, - "Unknown" => 1 + "Role: Primary" => 2, + "Role: Replica" => 1, + "Role: Unknown" => 1 }, duration: '10000.00ms', warnings: ["active-record duration: 10000.0 over 3000"], @@ -140,7 +157,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do transaction: '', duration: 1000.0, sql: 'SELECT * FROM users WHERE id = 10', - db_role: 'Primary' + db_role: 'Role: Primary', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), @@ -148,7 +166,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do transaction: '', duration: 2000.0, sql: 'SELECT * FROM users WHERE id = 10', - db_role: 'Replica' + db_role: 'Role: Replica', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), @@ -156,7 +175,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do transaction: 'In a transaction', duration: 3000.0, sql: 'UPDATE users SET admin = true WHERE id = 10', - db_role: 'Primary' + db_role: 'Role: Primary', + db_config_name: "Config name: the_db_config_name" ), a_hash_including( start: be_a(Time), @@ -164,10 +184,23 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do transaction: '', duration: 4000.0, sql: 'SELECT VERSION()', - db_role: 'Unknown' + db_role: 'Role: Unknown', + db_config_name: "Config name: the_db_config_name" ) ) ) end + + context 'when the multiple_database_metrics feature flag is disabled' do + before do + stub_feature_flags(multiple_database_metrics: false) + end + + it 'does not include db_config_name field' do + ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1) + + expect(subject.results[:details][0][:db_config_name]).to be_nil + end + end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ae956adf563..64fb10d1556 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -827,15 +827,15 @@ RSpec.describe Notify do end end - context 'when on gitlab.com' do + context 'when mailgun events are enabled' do before do - allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) + stub_application_setting(mailgun_events_enabled: true) end it 'has custom headers' do aggregate_failures do - expect(subject).to have_header('X-Mailgun-Tag', 'invite_email') - expect(subject).to have_header('X-Mailgun-Variables', { 'invite_token' => project_member.invite_token }.to_json) + expect(subject).to have_header('X-Mailgun-Tag', ::Members::Mailgun::INVITE_EMAIL_TAG) + expect(subject).to have_header('X-Mailgun-Variables', { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => project_member.invite_token }.to_json) end end end diff --git a/spec/requests/members/mailgun/permanent_failure_spec.rb b/spec/requests/members/mailgun/permanent_failure_spec.rb new file mode 100644 index 00000000000..e47aedf8e94 --- /dev/null +++ b/spec/requests/members/mailgun/permanent_failure_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'receive a permanent failure' do + describe 'POST /members/mailgun/permanent_failures', :aggregate_failures do + let_it_be(:member) { create(:project_member, :invited) } + + let(:raw_invite_token) { member.raw_invite_token } + let(:mailgun_events) { true } + let(:mailgun_signing_key) { 'abc123' } + + subject(:post_request) { post members_mailgun_permanent_failures_path(standard_params) } + + before do + stub_application_setting(mailgun_events_enabled: mailgun_events, mailgun_signing_key: mailgun_signing_key) + end + + it 'marks the member invite email success as false' do + expect { post_request }.to change { member.reload.invite_email_success }.from(true).to(false) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the change to a member is not made' do + context 'with incorrect signing key' do + context 'with incorrect signing key' do + let(:mailgun_signing_key) { '_foobar_' } + + it 'does not change member status and responds as not_found' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with nil signing key' do + let(:mailgun_signing_key) { nil } + + it 'does not change member status and responds as not_found' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when the feature is not enabled' do + let(:mailgun_events) { false } + + it 'does not change member status and responds as expected' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_acceptable) + end + end + + context 'when it is not an invite email' do + before do + stub_const('::Members::Mailgun::INVITE_EMAIL_TAG', '_foobar_') + end + + it 'does not change member status and responds as expected' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_acceptable) + end + end + end + + def standard_params + { + "signature": { + "timestamp": "1625056677", + "token": "eb944d0ace7227667a1b97d2d07276ae51d2b849ed2cfa68f3", + "signature": "9790cc6686eb70f0b1f869180d906870cdfd496d27fee81da0aa86b9e539e790" + }, + "event-data": { + "severity": "permanent", + "tags": ["invite_email"], + "timestamp": 1521233195.375624, + "storage": { + "url": "_anything_", + "key": "_anything_" + }, + "log-level": "error", + "id": "_anything_", + "campaigns": [], + "reason": "suppress-bounce", + "user-variables": { + "invite_token": raw_invite_token + }, + "flags": { + "is-routed": false, + "is-authenticated": true, + "is-system-test": false, + "is-test-mode": false + }, + "recipient-domain": "example.com", + "envelope": { + "sender": "bob@mg.gitlab.com", + "transport": "smtp", + "targets": "alice@example.com" + }, + "message": { + "headers": { + "to": "Alice ", + "message-id": "20130503192659.13651.20287@mg.gitlab.com", + "from": "Bob ", + "subject": "Test permanent_fail webhook" + }, + "attachments": [], + "size": 111 + }, + "recipient": "alice@example.com", + "event": "failed", + "delivery-status": { + "attempt-no": 1, + "message": "", + "code": 605, + "description": "Not delivering to previously bounced address", + "session-seconds": 0 + } + } + } + end + end +end diff --git a/spec/services/members/mailgun/process_webhook_service_spec.rb b/spec/services/members/mailgun/process_webhook_service_spec.rb new file mode 100644 index 00000000000..d6a21183395 --- /dev/null +++ b/spec/services/members/mailgun/process_webhook_service_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::Mailgun::ProcessWebhookService do + describe '#execute', :aggregate_failures do + let_it_be(:member) { create(:project_member, :invited) } + + let(:raw_invite_token) { member.raw_invite_token } + let(:payload) { { 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } } } + + subject(:service) { described_class.new(payload).execute } + + it 'marks the member invite email success as false' do + expect(Gitlab::AppLogger).to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/).and_call_original + + expect { service }.to change { member.reload.invite_email_success }.from(true).to(false) + end + + context 'when member can not be found' do + let(:raw_invite_token) { '_foobar_' } + + it 'does not change member status' do + expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + + context 'when invite token is not found in payload' do + let(:payload) { {} } + + it 'does not change member status and logs an error' do + expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + an_instance_of(described_class::ProcessWebhookServiceError)) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + end +end