Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-09 18:12:57 +00:00
parent 427dbb30f0
commit 7c0e5472c8
57 changed files with 1217 additions and 135 deletions

View File

@ -167,6 +167,7 @@ setup-test-env:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
- echo $CI_MERGE_REQUEST_APPROVED
- source scripts/gitlab_workhorse_component_helpers.sh - source scripts/gitlab_workhorse_component_helpers.sh
- run_timed_command "download_and_extract_gitlab_workhorse_package" || true - run_timed_command "download_and_extract_gitlab_workhorse_package" || true
- run_timed_command "scripts/setup-test-env" - run_timed_command "scripts/setup-test-env"

View File

@ -538,6 +538,9 @@
rules: rules:
- <<: *if-merge-request-approved - <<: *if-merge-request-approved
when: never when: never
# Temporarily disabled minimal rspec jobs before and after approval because of https://gitlab.com/gitlab-org/gitlab/-/issues/373064.
- <<: *if-merge-request-not-approved
when: never
- <<: *if-automated-merge-request - <<: *if-automated-merge-request
when: never when: never
- <<: *if-security-merge-request - <<: *if-security-merge-request
@ -559,8 +562,6 @@
changes: *backend-patterns changes: *backend-patterns
- <<: *if-security-merge-request - <<: *if-security-merge-request
changes: *backend-patterns changes: *backend-patterns
- <<: *if-merge-request-not-approved
when: never
.rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules: .rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules:
rules: rules:
@ -590,8 +591,6 @@
changes: *code-backstage-patterns changes: *code-backstage-patterns
- <<: *if-security-merge-request - <<: *if-security-merge-request
changes: *code-backstage-patterns changes: *code-backstage-patterns
- <<: *if-merge-request-not-approved
when: never
.rails:rules:system:minimal-default-rules: .rails:rules:system:minimal-default-rules:
rules: rules:
@ -725,6 +724,16 @@
################## ##################
# Frontend rules # # Frontend rules #
################## ##################
.frontend:rules:minimal-default-rules:
rules:
- <<: *if-merge-request-approved
when: never
- <<: *if-automated-merge-request
when: never
- <<: *if-security-merge-request
when: never
.frontend:rules:compile-production-assets: .frontend:rules:compile-production-assets:
rules: rules:
- <<: *if-not-canonical-namespace - <<: *if-not-canonical-namespace
@ -798,7 +807,7 @@
rules: rules:
- <<: *if-fork-merge-request - <<: *if-fork-merge-request
changes: *code-backstage-patterns changes: *code-backstage-patterns
- !reference [".rails:rules:minimal-default-rules", rules] - !reference [".frontend:rules:minimal-default-rules", rules]
- <<: *if-merge-request-labels-run-all-jest - <<: *if-merge-request-labels-run-all-jest
when: never when: never
- changes: *core-frontend-patterns - changes: *core-frontend-patterns
@ -824,7 +833,7 @@
.frontend:rules:jest:minimal:as-if-foss: .frontend:rules:jest:minimal:as-if-foss:
rules: rules:
- !reference [".strict-ee-only-rules", rules] - !reference [".strict-ee-only-rules", rules]
- !reference [".rails:rules:minimal-default-rules", rules] - !reference [".frontend:rules:minimal-default-rules", rules]
- <<: *if-merge-request-labels-run-all-jest - <<: *if-merge-request-labels-run-all-jest
when: never when: never
- <<: *if-fork-merge-request - <<: *if-fork-merge-request
@ -1022,8 +1031,6 @@
changes: *db-patterns changes: *db-patterns
- <<: *if-security-merge-request - <<: *if-security-merge-request
changes: *db-patterns changes: *db-patterns
- <<: *if-merge-request-not-approved
when: never
- changes: *db-patterns - changes: *db-patterns
.rails:rules:ee-and-foss-migration:minimal: .rails:rules:ee-and-foss-migration:minimal:
@ -1127,8 +1134,6 @@
changes: *db-patterns changes: *db-patterns
- <<: *if-security-merge-request - <<: *if-security-merge-request
changes: *db-patterns changes: *db-patterns
- <<: *if-merge-request-not-approved
when: never
- changes: *db-patterns - changes: *db-patterns
.rails:rules:ee-only-migration:minimal: .rails:rules:ee-only-migration:minimal:
@ -1218,8 +1223,6 @@
changes: *db-patterns changes: *db-patterns
- <<: *if-security-merge-request - <<: *if-security-merge-request
changes: *db-patterns changes: *db-patterns
- <<: *if-merge-request-not-approved
when: never
.rails:rules:as-if-foss-migration:minimal: .rails:rules:as-if-foss-migration:minimal:
rules: rules:

View File

@ -90,7 +90,7 @@ export default {
</script> </script>
<template> <template>
<div v-if="showBlobControls"> <div v-if="showBlobControls" class="gl-display-flex gl-gap-3">
<gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList"> <gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList">
{{ $options.i18n.findFile }} {{ $options.i18n.findFile }}
</gl-button> </gl-button>

View File

@ -50,7 +50,11 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true)) result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
if Feature.enabled?(:mr_review_submit_comment, @project) if Feature.enabled?(:mr_review_submit_comment, @project)
::Notes::CreateService.new(@project, current_user, create_note_params).execute if create_note_params[:note] if create_note_params[:note]
::Notes::CreateService.new(@project, current_user, create_note_params).execute
merge_request_activity_counter.track_submit_review_comment(user: current_user)
end
if Gitlab::Utils.to_boolean(approve_params[:approve]) if Gitlab::Utils.to_boolean(approve_params[:approve])
success = ::MergeRequests::ApprovalService.new(project: @project, current_user: current_user, params: approve_params).execute(merge_request) success = ::MergeRequests::ApprovalService.new(project: @project, current_user: current_user, params: approve_params).execute(merge_request)
@ -58,6 +62,8 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
unless success unless success
return render json: { message: _('An error occurred while approving, please try again.') }, status: :internal_server_error return render json: { message: _('An error occurred while approving, please try again.') }, status: :internal_server_error
end end
merge_request_activity_counter.track_submit_review_approve(user: current_user)
end end
end end
@ -159,6 +165,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def authorize_create_note! def authorize_create_note!
access_denied! unless can?(current_user, :create_note, merge_request) access_denied! unless can?(current_user, :create_note, merge_request)
end end
def merge_request_activity_counter
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
end
end end
Projects::MergeRequests::DraftsController.prepend_mod Projects::MergeRequests::DraftsController.prepend_mod

View File

@ -11,7 +11,7 @@ module Projects
before_action :integration, only: [:edit, :update, :test] before_action :integration, only: [:edit, :update, :test]
before_action :default_integration, only: [:edit, :update] before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update] before_action :web_hook_logs, only: [:edit, :update]
before_action -> { check_rate_limit!(:project_testing_integration, scope: [@project, current_user]) }, only: :test before_action -> { check_test_rate_limit! }, only: :test
respond_to :html respond_to :html
@ -140,6 +140,15 @@ module Projects
def use_inherited_settings?(attributes) def use_inherited_settings?(attributes)
default_integration && attributes[:inherit_from_id] == default_integration.id.to_s default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
end end
def check_test_rate_limit!
check_rate_limit!(:project_testing_integration, scope: [@project, current_user]) do
render json: {
error: true,
message: _('This endpoint has been requested too many times. Try again later.')
}, status: :ok
end
end
end end
end end
end end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module BranchRules
class BranchProtectionType < BaseObject
graphql_name 'BranchProtection'
description 'Branch protection details for a branch rule.'
accepts ::ProtectedBranch
authorize :read_protected_branch
field :allow_force_push,
type: GraphQL::Types::Boolean,
null: false,
description: 'Toggle force push to the branch for users with write access.'
end
end
end

View File

@ -13,6 +13,12 @@ module Types
null: false, null: false,
description: 'Branch name, with wildcards, for the branch rules.' description: 'Branch name, with wildcards, for the branch rules.'
field :branch_protection,
type: Types::BranchRules::BranchProtectionType,
null: false,
description: 'Branch protections configured for this branch rule.',
method: :itself
field :created_at, field :created_at,
Types::TimeType, Types::TimeType,
null: false, null: false,

View File

@ -38,6 +38,7 @@ class NotificationRecipient
return !unsubscribed? if @type == :subscription return !unsubscribed? if @type == :subscription
return false unless suitable_notification_level? return false unless suitable_notification_level?
return false if email_blocked?
# check this last because it's expensive # check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed # nobody should receive notifications if they've specifically unsubscribed
@ -95,6 +96,15 @@ class NotificationRecipient
end end
end end
def email_blocked?
return false if Feature.disabled?(:block_emails_with_failures)
recipient_email = user.notification_email_for(@group)
Gitlab::ApplicationRateLimiter.peek(:permanent_email_failure, scope: recipient_email) ||
Gitlab::ApplicationRateLimiter.peek(:temporary_email_failure, scope: recipient_email)
end
def has_access? def has_access?
DeclarativePolicy.subject_scope do DeclarativePolicy.subject_scope do
break false unless user.can?(:receive_notifications) break false unless user.can?(:receive_notifications)

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
module MergeRequests
module Mergeability
class Logger
include Gitlab::Utils::StrongMemoize
def initialize(merge_request:, destination: Gitlab::AppJsonLogger)
@destination = destination
@merge_request = merge_request
end
def commit
return unless enabled?
commit_logs
end
def instrument(mergeability_name:)
raise ArgumentError, 'block not given' unless block_given?
return yield unless enabled?
op_start_db_counters = current_db_counter_payload
op_started_at = current_monotonic_time
result = yield
observe("mergeability.#{mergeability_name}.duration_s", current_monotonic_time - op_started_at)
observe_sql_counters(mergeability_name, op_start_db_counters, current_db_counter_payload)
result
end
private
attr_reader :destination, :merge_request
def observe(name, value)
return unless enabled?
observations[name.to_s].push(value)
end
def commit_logs
attributes = Gitlab::ApplicationContext.current.merge({
mergeability_project_id: merge_request.project.id
})
attributes[:mergeability_merge_request_id] = merge_request.id
attributes.merge!(observations_hash)
attributes.compact!
attributes.stringify_keys!
destination.info(attributes)
end
def observations_hash
transformed = observations.transform_values do |values|
next if values.empty?
{
'values' => values
}
end.compact
transformed.each_with_object({}) do |key, hash|
key[1].each { |k, v| hash["#{key[0]}.#{k}"] = v }
end
end
def observations
strong_memoize(:observations) do
Hash.new { |hash, key| hash[key] = [] }
end
end
def observe_sql_counters(name, start_db_counters, end_db_counters)
end_db_counters.each do |key, value|
result = value - start_db_counters.fetch(key, 0)
next if result == 0
observe("mergeability.#{name}.#{key}", result)
end
end
def current_db_counter_payload
::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload
end
def enabled?
strong_memoize(:enabled) do
::Feature.enabled?(:mergeability_checks_logger, merge_request.project)
end
end
def current_monotonic_time
::Gitlab::Metrics::System.monotonic_time
end
end
end
end

View File

@ -15,12 +15,17 @@ module MergeRequests
next if check.skip? next if check.skip?
check_result = run_check(check) check_result = logger.instrument(mergeability_name: check_class.to_s.demodulize.underscore) do
run_check(check)
end
result_hash << check_result result_hash << check_result
break result_hash if check_result.failed? break result_hash if check_result.failed?
end end
logger.commit
self self
end end
@ -57,6 +62,12 @@ module MergeRequests
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request) Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
end end
end end
def logger
strong_memoize(:logger) do
MergeRequests::Mergeability::Logger.new(merge_request: merge_request)
end
end
end end
end end
end end

View File

@ -0,0 +1,46 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
require_relative 'cdn/google_cdn'
module ObjectStorage
module CDN
module Concern
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
def use_cdn?(request_ip)
return false unless cdn_options.is_a?(Hash) && cdn_options['provider']
return false unless cdn_provider
cdn_provider.use_cdn?(request_ip)
end
def cdn_signed_url
cdn_provider&.signed_url(path)
end
private
def cdn_provider
strong_memoize(:cdn_provider) do
provider = cdn_options['provider']&.downcase
next unless provider
next GoogleCDN.new(cdn_options) if provider == 'google'
raise "Unknown CDN provider: #{provider}"
end
end
def cdn_options
return {} unless options.object_store.key?('cdn')
options.object_store.cdn
end
end
end
end
# rubocop:enable Naming/FileName

View File

@ -0,0 +1,141 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
module ObjectStorage
module CDN
class GoogleCDN
include Gitlab::Utils::StrongMemoize
IpListNotRetrievedError = Class.new(StandardError)
GOOGLE_CDN_LIST_KEY = 'google_cdn_ip_list'
GOOGLE_IP_RANGES_URL = 'https://www.gstatic.com/ipranges/cloud.json'
EXPECTED_CONTENT_TYPE = 'application/json'
RESPONSE_BODY_LIMIT = 1.megabyte
CACHE_EXPIRATION_TIME = 1.day
attr_reader :options
def initialize(options)
@options = HashWithIndifferentAccess.new(options.to_h)
end
def use_cdn?(request_ip)
return false unless config_valid?
ip = IPAddr.new(request_ip)
return false if ip.private?
return false unless google_ip_ranges.present?
!google_ip?(request_ip)
end
def signed_url(path, expiry: 10.minutes)
expiration = (Time.current + expiry).utc.to_i
uri = Addressable::URI.parse(cdn_url)
uri.path = path
uri.query = "Expires=#{expiration}&KeyName=#{key_name}"
signature = OpenSSL::HMAC.digest('SHA1', decoded_key, uri.to_s)
encoded_signature = Base64.urlsafe_encode64(signature)
uri.query += "&Signature=#{encoded_signature}"
uri.to_s
end
private
def config_valid?
[key_name, decoded_key, cdn_url].all?(&:present?)
end
def key_name
strong_memoize(:key_name) do
options['key_name']
end
end
def decoded_key
strong_memoize(:decoded_key) do
Base64.urlsafe_decode64(options['key']) if options['key']
rescue ArgumentError
Gitlab::ErrorTracking.log_exception(ArgumentError.new("Google CDN key is not base64-encoded"))
nil
end
end
def cdn_url
strong_memoize(:cdn_url) do
options['url']
end
end
def google_ip?(request_ip)
google_ip_ranges.any? { |range| range.include?(request_ip) }
end
def google_ip_ranges
strong_memoize(:google_ip_ranges) do
cache_value(GOOGLE_CDN_LIST_KEY) { fetch_google_ip_list }
end
rescue IpListNotRetrievedError => err
Gitlab::ErrorTracking.log_exception(err)
[]
end
def cache_value(key, expires_in: CACHE_EXPIRATION_TIME, &block)
l1_cache.fetch(key, expires_in: expires_in) do
l2_cache.fetch(key, expires_in: expires_in) { yield }
end
end
def l1_cache
Gitlab::ProcessMemoryCache.cache_backend
end
def l2_cache
Rails.cache
end
def fetch_google_ip_list
response = Gitlab::HTTP.get(GOOGLE_IP_RANGES_URL)
raise IpListNotRetrievedError, "response was #{response.code}" unless response.code == 200
if response.body&.bytesize.to_i > RESPONSE_BODY_LIMIT
raise IpListNotRetrievedError, "response was too large: #{response.body.bytesize}"
end
parsed_response = response.parsed_response
unless response.content_type == EXPECTED_CONTENT_TYPE && parsed_response.is_a?(Hash)
raise IpListNotRetrievedError, "response was not JSON"
end
parse_google_prefixes(parsed_response)
end
def parse_google_prefixes(parsed_response)
prefixes = parsed_response['prefixes']
raise IpListNotRetrievedError, "JSON was type #{prefixes.class}, expected Array" unless prefixes.is_a?(Array)
ranges = prefixes.map do |prefix|
ip_range = prefix['ipv4Prefix'] || prefix['ipv6Prefix']
next unless ip_range
IPAddr.new(ip_range)
end.compact
raise IpListNotRetrievedError, "#{GOOGLE_IP_RANGES_URL} did not return any IP ranges" if ranges.empty?
ranges
end
end
end
end
# rubocop:enable Naming/FileName

View File

@ -46,18 +46,18 @@
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-form-input gl-mt-2' = f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-form-input gl-mt-2'
.help-block .help-block
= _('Specify an email address regex pattern to identify default internal users.') = _('Specify an email address regex pattern to identify default internal users.')
= link_to _('Learn more'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank', rel: 'noopener noreferrer'
- unless Gitlab.com? - unless Gitlab.com?
.form-group .form-group
= f.label :deactivate_dormant_users, _('Dormant users'), class: 'label-bold' = f.label :deactivate_dormant_users, _('Dormant users'), class: 'label-bold'
- dormant_users_help_link = help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users') - dormant_users_help_link = help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users')
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link } - dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe } = f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
.form-group .form-group
= f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light' = f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
= f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1' = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
.form-text.text-muted .form-text.text-muted
= _('Period of inactivity before deactivation') = _('Period of inactivity before deactivation.')
.form-group .form-group
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light' = f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'

View File

@ -0,0 +1,8 @@
---
name: block_emails_with_failures
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96902
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373159
milestone: '15.4'
type: development
group: group::project management
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: mergeability_checks_logger
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96128
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371717
milestone: '15.4'
type: development
group: group::code review
default_enabled: false

View File

@ -37,6 +37,7 @@ ActiveSupport::Inflector.inflections do |inflect|
vulnerabilities_feedback vulnerabilities_feedback
vulnerability_feedback vulnerability_feedback
) )
inflect.acronym 'CDN'
inflect.acronym 'EE' inflect.acronym 'EE'
inflect.acronym 'JH' inflect.acronym 'JH'
inflect.acronym 'CSP' inflect.acronym 'CSP'

View File

@ -116,6 +116,8 @@
- 'i_code_review_merge_request_widget_status_checks_expand_success' - 'i_code_review_merge_request_widget_status_checks_expand_success'
- 'i_code_review_merge_request_widget_status_checks_expand_warning' - 'i_code_review_merge_request_widget_status_checks_expand_warning'
- 'i_code_review_merge_request_widget_status_checks_expand_failed' - 'i_code_review_merge_request_widget_status_checks_expand_failed'
- 'i_code_review_submit_review_approve'
- 'i_code_review_submit_review_comment'
- name: code_review_category_monthly_active_users - name: code_review_category_monthly_active_users
operator: OR operator: OR
source: redis source: redis
@ -220,6 +222,8 @@
- 'i_code_review_merge_request_widget_status_checks_expand_success' - 'i_code_review_merge_request_widget_status_checks_expand_success'
- 'i_code_review_merge_request_widget_status_checks_expand_warning' - 'i_code_review_merge_request_widget_status_checks_expand_warning'
- 'i_code_review_merge_request_widget_status_checks_expand_failed' - 'i_code_review_merge_request_widget_status_checks_expand_failed'
- 'i_code_review_submit_review_approve'
- 'i_code_review_submit_review_comment'
- name: code_review_extension_category_monthly_active_users - name: code_review_extension_category_monthly_active_users
operator: OR operator: OR
source: redis source: redis

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_submit_review_approve_monthly
description: Count of unique users per month who submit a review and approve
product_stage: create
product_group: code_review
product_category: code_review
product_section: dev
value_type: number
status: active
milestone: '15.4'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_submit_review_approve
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_submit_review_comment_monthly
description: Count of unique users per month who submit a review with a comment
product_stage: create
product_group: code_review
product_category: code_review
product_section: dev
value_type: number
status: active
milestone: '15.4'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_submit_review_comment
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_submit_review_approve_weekly
description: Count of unique users per week who submit a review and approve
product_stage: create
product_group: code_review
product_category: code_review
product_section: dev
value_type: number
status: active
milestone: '15.4'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_submit_review_approve
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_submit_review_comment_weekly
description: Count of unique users per week who submit a review with a comment
product_stage: create
product_group: code_review
product_category: code_review
product_section: dev
value_type: number
status: active
milestone: '15.4'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91073
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
options:
events:
- i_code_review_submit_review_comment
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -10074,6 +10074,16 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="branchcommit"></a>`commit` | [`Commit`](#commit) | Commit for the branch. | | <a id="branchcommit"></a>`commit` | [`Commit`](#commit) | Commit for the branch. |
| <a id="branchname"></a>`name` | [`String!`](#string) | Name of the branch. | | <a id="branchname"></a>`name` | [`String!`](#string) | Name of the branch. |
### `BranchProtection`
Branch protection details for a branch rule.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="branchprotectionallowforcepush"></a>`allowForcePush` | [`Boolean!`](#boolean) | Toggle force push to the branch for users with write access. |
### `BranchRule` ### `BranchRule`
List of branch rules for a project, grouped by branch name. List of branch rules for a project, grouped by branch name.
@ -10082,6 +10092,7 @@ List of branch rules for a project, grouped by branch name.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="branchrulebranchprotection"></a>`branchProtection` | [`BranchProtection!`](#branchprotection) | Branch protections configured for this branch rule. |
| <a id="branchrulecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. | | <a id="branchrulecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. |
| <a id="branchrulename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. | | <a id="branchrulename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. |
| <a id="branchruleupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. | | <a id="branchruleupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. |

View File

@ -36,7 +36,8 @@ Sign in to BrowserStack with the credentials saved in the **Engineering** vault
## Initiatives ## Initiatives
Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend). You can find current frontend initiatives with a cross-functional impact on epics
with the label [frontend-initiative](https://gitlab.com/groups/gitlab-org/-/epics?state=opened&page=1&sort=UPDATED_AT_DESC&label_name[]=frontend-initiative).
## Principles ## Principles

View File

@ -383,7 +383,7 @@ The following table lists group permissions available for each role:
| Pull a container image using the dependency proxy | ✓ | ✓ | ✓ | ✓ | ✓ | | Pull a container image using the dependency proxy | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ | | View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
| View group [epic](group/epics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | View group [epic](group/epics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [group wiki](project/wiki/group.md) pages | ✓ (6) | ✓ | ✓ | ✓ | ✓ | | View [group wiki](project/wiki/group.md) pages | ✓ (5) | ✓ | ✓ | ✓ | ✓ |
| View [Insights](project/insights/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | View [Insights](project/insights/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [Insights](project/insights/index.md) charts | ✓ | ✓ | ✓ | ✓ | ✓ | | View [Insights](project/insights/index.md) charts | ✓ | ✓ | ✓ | ✓ | ✓ |
| View [Issue analytics](analytics/issue_analytics.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | View [Issue analytics](analytics/issue_analytics.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
@ -395,13 +395,13 @@ The following table lists group permissions available for each role:
| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ | | Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
| Delete [packages](packages/index.md) | | | | ✓ | ✓ | | Delete [packages](packages/index.md) | | | | ✓ | ✓ |
| Create/edit/delete [Maven and generic package duplicate settings](packages/generic_packages/index.md#do-not-allow-duplicate-generic-packages) | | | | ✓ | ✓ | | Create/edit/delete [Maven and generic package duplicate settings](packages/generic_packages/index.md#do-not-allow-duplicate-generic-packages) | | | | ✓ | ✓ |
| Pull a Container Registry image | ✓ (7) | ✓ | ✓ | ✓ | ✓ | | Pull a Container Registry image | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
| Remove a Container Registry image | | | ✓ | ✓ | ✓ | | Remove a Container Registry image | | | ✓ | ✓ | ✓ |
| View [Group DevOps Adoption](group/devops_adoption/index.md) | | ✓ | ✓ | ✓ | ✓ | | View [Group DevOps Adoption](group/devops_adoption/index.md) | | ✓ | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ | | View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
| View [Productivity analytics](analytics/productivity_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | View [Productivity analytics](analytics/productivity_analytics.md) | | ✓ | ✓ | ✓ | ✓ |
| Create and edit [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ | | Create and edit [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ |
| Create project in group | | | ✓ (3)(5) | ✓ (3) | ✓ (3) | | Create project in group | | | ✓ (2)(4) | ✓ (2) | ✓ (2) |
| Create/edit/delete group milestones | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete group milestones | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete iterations | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete iterations | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ | | Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
@ -409,10 +409,10 @@ The following table lists group permissions available for each role:
| Purge the dependency proxy for a group | | | | | ✓ | | Purge the dependency proxy for a group | | | | | ✓ |
| Create/edit/delete dependency proxy [cleanup policies](packages/dependency_proxy/reduce_dependency_proxy_storage.md#cleanup-policies) | | | | ✓ | ✓ | | Create/edit/delete dependency proxy [cleanup policies](packages/dependency_proxy/reduce_dependency_proxy_storage.md#cleanup-policies) | | | | ✓ | ✓ |
| Use [security dashboard](application_security/security_dashboard/index.md) | | | ✓ | ✓ | ✓ | | Use [security dashboard](application_security/security_dashboard/index.md) | | | ✓ | ✓ | ✓ |
| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ | | View group Audit Events | | | ✓ (6) | ✓ (6) | ✓ |
| Create subgroup | | | | ✓ (1) | ✓ | | Create subgroup | | | | ✓ (1) | ✓ |
| Delete [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ | | Delete [group wiki](project/wiki/group.md) pages | | | ✓ | ✓ | ✓ |
| Edit [epic](group/epics/index.md) comments (posted by any user) | | | | ✓ (2) | ✓ (2) | | Edit [epic](group/epics/index.md) comments (posted by any user) | | | | ✓ | ✓ |
| List group deploy tokens | | | | ✓ | ✓ | | List group deploy tokens | | | | ✓ | ✓ |
| Manage [group push rules](group/access_and_permissions.md#group-push-rules) | | | | ✓ | ✓ | | Manage [group push rules](group/access_and_permissions.md#group-push-rules) | | | | ✓ | ✓ |
| View/manage group-level Kubernetes cluster | | | | ✓ | ✓ | | View/manage group-level Kubernetes cluster | | | | ✓ | ✓ |
@ -423,14 +423,14 @@ The following table lists group permissions available for each role:
| Delete group [epic](group/epics/index.md) | | | | | ✓ | | Delete group [epic](group/epics/index.md) | | | | | ✓ |
| Disable notification emails | | | | | ✓ | | Disable notification emails | | | | | ✓ |
| Edit group settings | | | | | ✓ | | Edit group settings | | | | | ✓ |
| Edit [SAML SSO](group/saml_sso/index.md) | | | | | ✓ (4) | | Edit [SAML SSO](group/saml_sso/index.md) | | | | | ✓ (3) |
| Filter members by 2FA status | | | | | ✓ | | Filter members by 2FA status | | | | | ✓ |
| Manage group level CI/CD variables | | | | | ✓ | | Manage group level CI/CD variables | | | | | ✓ |
| Manage group members | | | | | ✓ | | Manage group members | | | | | ✓ |
| Share (invite) groups with groups | | | | | ✓ | | Share (invite) groups with groups | | | | | ✓ |
| View 2FA status of members | | | | | ✓ | | View 2FA status of members | | | | | ✓ |
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) | | View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (3) |
| View group [Usage Quotas](usage_quotas.md) page | | | | | ✓ (4) | | View group [Usage Quotas](usage_quotas.md) page | | | | | ✓ (3) |
| Manage group runners | | | | | ✓ | | Manage group runners | | | | | ✓ |
| [Migrate groups](group/import/index.md) | | | | | ✓ | | [Migrate groups](group/import/index.md) | | | | | ✓ |
| Manage [subscriptions, and purchase CI/CD minutes and storage](../subscriptions/gitlab_com/index.md) | | | | | ✓ | | Manage [subscriptions, and purchase CI/CD minutes and storage](../subscriptions/gitlab_com/index.md) | | | | | ✓ |
@ -438,14 +438,13 @@ The following table lists group permissions available for each role:
<!-- markdownlint-disable MD029 --> <!-- markdownlint-disable MD029 -->
1. Groups can be set to allow either Owners, or Owners and users with the Maintainer role, to [create subgroups](group/subgroups/index.md#create-a-subgroup). 1. Groups can be set to allow either Owners, or Owners and users with the Maintainer role, to [create subgroups](group/subgroups/index.md#create-a-subgroup).
2. Introduced in GitLab 12.2. 2. Default project creation role can be changed at:
3. Default project creation role can be changed at:
- The [instance level](admin_area/settings/visibility_and_access_controls.md#define-which-roles-can-create-projects). - The [instance level](admin_area/settings/visibility_and_access_controls.md#define-which-roles-can-create-projects).
- The [group level](group/manage.md#specify-who-can-add-projects-to-a-group). - The [group level](group/manage.md#specify-who-can-add-projects-to-a-group).
4. Does not apply to subgroups. 3. Does not apply to subgroups.
5. Developers can push commits to the default branch of a new project only if the [default branch protection](group/manage.md#change-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected". 4. Developers can push commits to the default branch of a new project only if the [default branch protection](group/manage.md#change-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
6. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages. 5. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages.
7. Users can only view events based on their individual actions. 6. Users can only view events based on their individual actions.
<!-- markdownlint-enable MD029 --> <!-- markdownlint-enable MD029 -->

View File

@ -10,9 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Creating, editing, and deleting tasks](https://gitlab.com/groups/gitlab-org/-/epics/7169) introduced in GitLab 15.0. > - [Creating, editing, and deleting tasks](https://gitlab.com/groups/gitlab-org/-/epics/7169) introduced in GitLab 15.0.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.3. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.3.
WARNING:
Tasks are in [**Alpha**](../policy/alpha-beta-support.md#alpha-features).
Known limitation: Known limitation:
- [Tasks currently cannot be accessed via REST API.](https://gitlab.com/gitlab-org/gitlab/-/issues/368055) - [Tasks currently cannot be accessed via REST API.](https://gitlab.com/gitlab-org/gitlab/-/issues/368055)

View File

@ -55,7 +55,10 @@ you must purchase additional storage. For more details, see [Excess storage usag
## View storage usage ## View storage usage
You can view storage usage for your project or [namespace](../user/namespace/index.md). Prerequisites:
- To view storage usage for a project, you must be a project Maintainer or namespace Owner.
- To view storage usage for a namespace, you must be the namespace Owner.
1. Go to your project or namespace: 1. Go to your project or namespace:
- For a project, on the top bar, select **Menu > Projects** and find your project. - For a project, on the top bar, select **Menu > Projects** and find your project.
@ -84,16 +87,16 @@ The following storage usage statistics are available to a maintainer:
## Manage your storage usage ## Manage your storage usage
You can use several methods to manage and reduce your usage for some storage types. To manage your storage, if you are a namespace Owner you can [purchase more storage for the namespace](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
For more information, see the following pages: Depending on your role, you can also use the following methods to manage or reduce your storage:
- [Reduce package registry storage](packages/package_registry/reduce_package_registry_storage.md) - [Reduce package registry storage](packages/package_registry/reduce_package_registry_storage.md).
- [Reduce dependency proxy storage](packages/dependency_proxy/reduce_dependency_proxy_storage.md) - [Reduce dependency proxy storage](packages/dependency_proxy/reduce_dependency_proxy_storage.md).
- [Reduce repository size](project/repository/reducing_the_repo_size_using_git.md) - [Reduce repository size](project/repository/reducing_the_repo_size_using_git.md).
- [Reduce container registry storage](packages/container_registry/reduce_container_registry_storage.md) - [Reduce container registry storage](packages/container_registry/reduce_container_registry_storage.md).
- [Reduce container registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md) - [Reduce container registry data transfers](packages/container_registry/reduce_container_registry_data_transfer.md).
- [Reduce wiki repository size](../administration/wikis/index.md#reduce-wiki-repository-size) - [Reduce wiki repository size](../administration/wikis/index.md#reduce-wiki-repository-size).
## Excess storage usage ## Excess storage usage

View File

@ -46,7 +46,8 @@ module Gitlab
search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute }, search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute },
gitlab_shell_operation: { threshold: 600, interval: 1.minute }, gitlab_shell_operation: { threshold: 600, interval: 1.minute },
pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute }, pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute },
temporary_email_failure: { threshold: 50, interval: 1.day }, temporary_email_failure: { threshold: 300, interval: 1.day },
permanent_email_failure: { threshold: 5, interval: 1.day },
project_testing_integration: { threshold: 5, interval: 1.minute }, project_testing_integration: { threshold: 5, interval: 1.minute },
email_verification: { threshold: 10, interval: 10.minutes }, email_verification: { threshold: 10, interval: 10.minutes },
email_verification_code_send: { threshold: 10, interval: 1.hour }, email_verification_code_send: { threshold: 10, interval: 1.hour },

View File

@ -7,7 +7,8 @@ module Gitlab
TAGS_SORT_KEY = { TAGS_SORT_KEY = {
'name' => Gitaly::FindAllTagsRequest::SortBy::Key::REFNAME, 'name' => Gitaly::FindAllTagsRequest::SortBy::Key::REFNAME,
'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE 'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE,
'version' => Gitaly::FindAllTagsRequest::SortBy::Key::VERSION_REFNAME
}.freeze }.freeze
TAGS_SORT_DIRECTION = { TAGS_SORT_DIRECTION = {
@ -258,7 +259,7 @@ module Gitlab
end end
def sort_tags_by_param(sort_by) def sort_tags_by_param(sort_by)
match = sort_by.match(/^(?<key>name|updated)_(?<direction>asc|desc)$/) match = sort_by.match(/^(?<key>name|updated|version)_(?<direction>asc|desc)$/)
return unless match return unless match

View File

@ -5,11 +5,12 @@ module Gitlab
module WebhookProcessors module WebhookProcessors
class FailureLogger < Base class FailureLogger < Base
def execute def execute
log_failure if permanent_failure? || temporary_failure_over_threshold? log_failure if permanent_failure_over_threshold? || temporary_failure_over_threshold?
end end
def permanent_failure? def permanent_failure_over_threshold?
payload['event'] == 'failed' && payload['severity'] == 'permanent' payload['event'] == 'failed' && payload['severity'] == 'permanent' &&
Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: payload['recipient'])
end end
def temporary_failure_over_threshold? def temporary_failure_over_threshold?

View File

@ -21,7 +21,6 @@ module Gitlab
CATEGORIES_FOR_TOTALS = %w[ CATEGORIES_FOR_TOTALS = %w[
analytics analytics
compliance compliance
epic_boards_usage
epics_usage epics_usage
error_tracking error_tracking
ide_edit ide_edit

View File

@ -396,3 +396,11 @@
redis_slot: code_review redis_slot: code_review
category: code_review category: code_review
aggregation: weekly aggregation: weekly
- name: i_code_review_submit_review_approve
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_submit_review_comment
redis_slot: code_review
category: code_review
aggregation: weekly

View File

@ -337,11 +337,6 @@
redis_slot: testing redis_slot: testing
category: testing category: testing
aggregation: weekly aggregation: weekly
# Container Security - Network Policies
- name: clusters_using_network_policies_ui
redis_slot: network_policies
category: network_policies
aggregation: weekly
# Geo group # Geo group
- name: g_geo_proxied_requests - name: g_geo_proxied_requests
category: geo category: geo

View File

@ -1,19 +0,0 @@
# Epic board events
#
# We are using the same slot of issue events 'project_management' for
# epic events to allow data aggregation.
# More information in: https://gitlab.com/gitlab-org/gitlab/-/issues/322405
- name: g_project_management_users_creating_epic_boards
category: epic_boards_usage
redis_slot: project_management
aggregation: daily
- name: g_project_management_users_viewing_epic_boards
category: epic_boards_usage
redis_slot: project_management
aggregation: daily
- name: g_project_management_users_updating_epic_board_names
category: epic_boards_usage
redis_slot: project_management
aggregation: daily

View File

@ -49,6 +49,8 @@ module Gitlab
MR_LOAD_CONFLICT_UI_ACTION = 'i_code_review_user_load_conflict_ui' MR_LOAD_CONFLICT_UI_ACTION = 'i_code_review_user_load_conflict_ui'
MR_RESOLVE_CONFLICT_ACTION = 'i_code_review_user_resolve_conflict' MR_RESOLVE_CONFLICT_ACTION = 'i_code_review_user_resolve_conflict'
MR_RESOLVE_THREAD_IN_ISSUE_ACTION = 'i_code_review_user_resolve_thread_in_issue' MR_RESOLVE_THREAD_IN_ISSUE_ACTION = 'i_code_review_user_resolve_thread_in_issue'
MR_SUBMIT_REVIEW_APPROVE = 'i_code_review_submit_review_approve'
MR_SUBMIT_REVIEW_COMMENT = 'i_code_review_submit_review_comment'
class << self class << self
def track_mr_diffs_action(merge_request:) def track_mr_diffs_action(merge_request:)
@ -230,6 +232,14 @@ module Gitlab
track_unique_action_by_user(MR_RESOLVE_THREAD_IN_ISSUE_ACTION, user) track_unique_action_by_user(MR_RESOLVE_THREAD_IN_ISSUE_ACTION, user)
end end
def track_submit_review_approve(user:)
track_unique_action_by_user(MR_SUBMIT_REVIEW_APPROVE, user)
end
def track_submit_review_comment(user:)
track_unique_action_by_user(MR_SUBMIT_REVIEW_COMMENT, user)
end
private private
def track_unique_action_by_merge_request(action, merge_request) def track_unique_action_by_merge_request(action, merge_request)

View File

@ -21005,7 +21005,7 @@ msgstr ""
msgid "Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}." msgid "Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}."
msgstr "" msgstr ""
msgid "Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited." msgid "Includes LFS objects. It can be overridden per group, or per project. Set to 0 for no limit."
msgstr "" msgstr ""
msgid "Includes an MVC structure to help you get started" msgid "Includes an MVC structure to help you get started"
@ -24385,7 +24385,7 @@ msgstr ""
msgid "Maximum allowable lifetime for access token (days)" msgid "Maximum allowable lifetime for access token (days)"
msgstr "" msgstr ""
msgid "Maximum allowed lifetime for SSH keys (in days)" msgid "Maximum allowed lifetime for SSH keys (days)"
msgstr "" msgstr ""
msgid "Maximum artifacts size" msgid "Maximum artifacts size"
@ -28677,7 +28677,7 @@ msgstr ""
msgid "Period of inactivity (days)" msgid "Period of inactivity (days)"
msgstr "" msgstr ""
msgid "Period of inactivity before deactivation" msgid "Period of inactivity before deactivation."
msgstr "" msgstr ""
msgid "Permalink" msgid "Permalink"
@ -43092,7 +43092,7 @@ msgstr ""
msgid "Users can launch a development environment from a GitLab browser tab when the %{linkStart}Gitpod%{linkEnd} integration is enabled." msgid "Users can launch a development environment from a GitLab browser tab when the %{linkStart}Gitpod%{linkEnd} integration is enabled."
msgstr "" msgstr ""
msgid "Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}" msgid "Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}"
msgstr "" msgstr ""
msgid "Users can render diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents using Kroki." msgid "Users can render diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents using Kroki."

View File

@ -12,6 +12,8 @@ module QA
NoValueError = Class.new(RuntimeError) NoValueError = Class.new(RuntimeError)
attr_reader :retrieved_from_cache
class << self class << self
# Initialize new instance of class without fabrication # Initialize new instance of class without fabrication
# #
@ -81,7 +83,7 @@ module QA
Support::FabricationTracker.start_fabrication Support::FabricationTracker.start_fabrication
result = yield.tap do result = yield.tap do
fabrication_time = Time.now - start fabrication_time = Time.now - start
fabrication_http_method = if resource.api_fabrication_http_method == :get fabrication_http_method = if resource.api_fabrication_http_method == :get || resource.retrieved_from_cache
if include?(Reusable) if include?(Reusable)
"Retrieved for reuse" "Retrieved for reuse"
else else
@ -92,24 +94,28 @@ module QA
end end
Support::FabricationTracker.save_fabrication(:"#{fabrication_method}_fabrication", fabrication_time) Support::FabricationTracker.save_fabrication(:"#{fabrication_method}_fabrication", fabrication_time)
Tools::TestResourceDataProcessor.collect(
resource: resource, unless resource.retrieved_from_cache
info: resource.identifier, Tools::TestResourceDataProcessor.collect(
fabrication_method: fabrication_method, resource: resource,
fabrication_time: fabrication_time info: resource.identifier,
) fabrication_method: fabrication_method,
fabrication_time: fabrication_time
)
end
Runtime::Logger.info do Runtime::Logger.info do
msg = ["==#{'=' * parents.size}>"] msg = ["==#{'=' * parents.size}>"]
msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}" msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}"
msg << resource.identifier msg << resource.identifier
msg << "as a dependency of #{parents.last}" if parents.any? msg << "as a dependency of #{parents.last}" if parents.any?
msg << "via #{fabrication_method}" msg << "via #{resource.retrieved_from_cache ? 'cache' : fabrication_method}"
msg << "in #{fabrication_time.round(2)} seconds" msg << "in #{fabrication_time.round(2)} seconds"
msg.compact.join(' ') msg.compact.join(' ')
end end
end end
Support::FabricationTracker.finish_fabrication Support::FabricationTracker.finish_fabrication
result result

View File

@ -46,8 +46,15 @@ module QA
rescue NoValueError rescue NoValueError
user_id = user.respond_to?(:id) ? user.id : Resource::User.build(user).reload!.id user_id = user.respond_to?(:id) ? user.id : Resource::User.build(user).reload!.id
token = auto_paginated_response(request_url("/personal_access_tokens?user_id=#{user_id}", per_page: '100')) api_client = Runtime::API::Client.new(:gitlab,
.find { |t| t[:name] == name } is_new_session: false,
user: user,
personal_access_token: self.token)
request_url = Runtime::API::Request.new(api_client,
"/personal_access_tokens?user_id=#{user_id}",
per_page: '100').url
token = auto_paginated_response(request_url).find { |t| t[:name] == name }
raise ResourceNotFoundError unless token raise ResourceNotFoundError unless token
@ -56,7 +63,7 @@ module QA
end end
def name def name
@name ||= "api-personal-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}" @name ||= "api-pat-#{user.username}-#{Faker::Alphanumeric.alphanumeric(number: 8)}"
end end
def api_post_body def api_post_body
@ -75,6 +82,9 @@ module QA
def find_and_set_value def find_and_set_value
@token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username) @token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username)
@retrieved_from_cache = true if @token
@token
end end
def cache_token def cache_token

View File

@ -2,11 +2,7 @@
module QA module QA
RSpec.describe 'Manage' do RSpec.describe 'Manage' do
describe 'Project owner permissions', :reliable, quarantine: { describe 'Project owner permissions', :reliable do
only: { pipeline: %i[staging staging-canary production canary] },
type: :investigating,
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373038'
} do
let!(:owner) do let!(:owner) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
end end

View File

@ -404,6 +404,11 @@ RSpec.describe Projects::MergeRequests::DraftsController do
end end
context 'when feature flag is enabled' do context 'when feature flag is enabled' do
before do
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_submit_review_comment)
end
it 'creates note' do it 'creates note' do
post :publish, params: params.merge!(note: 'Hello world') post :publish, params: params.merge!(note: 'Hello world')
@ -415,6 +420,13 @@ RSpec.describe Projects::MergeRequests::DraftsController do
expect(merge_request.notes.reload.size).to be(1) expect(merge_request.notes.reload.size).to be(1)
end end
it 'tracks merge request activity' do
post :publish, params: params.merge!(note: 'Hello world')
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to have_received(:track_submit_review_comment).with(user: user)
end
end end
end end
@ -436,6 +448,11 @@ RSpec.describe Projects::MergeRequests::DraftsController do
end end
context 'when feature flag is enabled' do context 'when feature flag is enabled' do
before do
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_submit_review_approve)
end
it 'approves merge request' do it 'approves merge request' do
post :publish, params: params.merge!(approve: true) post :publish, params: params.merge!(approve: true)
@ -447,6 +464,13 @@ RSpec.describe Projects::MergeRequests::DraftsController do
expect(merge_request.approvals.reload.size).to be(0) expect(merge_request.approvals.reload.size).to be(0)
end end
it 'tracks merge request activity' do
post :publish, params: params.merge!(approve: true)
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to have_received(:track_submit_review_approve).with(user: user)
end
end end
end end
end end

View File

@ -50,7 +50,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
end end
end end
context 'when validations fail' do context 'when validations fail', :clean_gitlab_redis_rate_limiting do
let(:integration_params) { { active: 'true', url: '' } } let(:integration_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do it 'returns error messages in JSON response' do
@ -62,7 +62,7 @@ RSpec.describe Projects::Settings::IntegrationsController do
end end
end end
context 'when successful' do context 'when successful', :clean_gitlab_redis_rate_limiting do
context 'with empty project' do context 'with empty project' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
@ -200,8 +200,8 @@ RSpec.describe Projects::Settings::IntegrationsController do
2.times { post :test, params: project_params(service: integration_params) } 2.times { post :test, params: project_params(service: integration_params) }
expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.')) expect(response.body).to include(_('This endpoint has been requested too many times. Try again later.'))
expect(response).to have_gitlab_http_status(:too_many_requests) expect(response).to have_gitlab_http_status(:ok)
end end
end end
end end

View File

@ -17,6 +17,7 @@ RSpec.describe 'User uses header search field', :js do
end end
before do before do
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
sign_in(user) sign_in(user)

17
spec/fixtures/cdn/google_cloud.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"syncToken": "1661533328840",
"creationTime": "2022-08-26T10:02:08.840384",
"prefixes": [{
"ipv4Prefix": "34.80.0.0/15",
"service": "Google Cloud",
"scope": "asia-east1"
}, {
"ipv4Prefix": "34.137.0.0/16",
"service": "Google Cloud",
"scope": "asia-east1"
}, {
"ipv6Prefix": "2600:1900:4180::/44",
"service": "Google Cloud",
"scope": "us-west4"
}]
}

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['BranchProtection'] do
subject { described_class }
let(:fields) { %i[allow_force_push] }
specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
specify { is_expected.to have_graphql_fields(fields) }
end

View File

@ -7,21 +7,16 @@ RSpec.describe GitlabSchema.types['BranchRule'] do
subject { described_class } subject { described_class }
let(:fields) { %i[name created_at updated_at] } let(:fields) do
%i[
specify { is_expected.to have_graphql_name('BranchRule') } name
branch_protection
created_at
updated_at
]
end
specify { is_expected.to require_graphql_authorizations(:read_protected_branch) } specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
specify { is_expected.to have_graphql_description }
specify { is_expected.to have_graphql_fields(fields) } specify { is_expected.to have_graphql_fields(fields) }
describe 'graphql_fields' do
subject do
described_class.all_field_definitions
end
specify { is_expected.to all(have_graphql_description) }
end
end end

View File

@ -260,6 +260,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.tags(sort_by: 'name_asc') client.tags(sort_by: 'name_asc')
end end
context 'with semantic version sorting' do
it 'sends a correct find_all_tags message' do
expected_sort_by = Gitaly::FindAllTagsRequest::SortBy.new(
key: :VERSION_REFNAME,
direction: :ASCENDING
)
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:find_all_tags)
.with(gitaly_request_with_params(sort_by: expected_sort_by), kind_of(Hash))
.and_return([])
client.tags(sort_by: 'version_asc')
end
end
end end
context 'with pagination option' do context 'with pagination option' do

View File

@ -20,18 +20,43 @@ RSpec.describe Gitlab::Mailgun::WebhookProcessors::FailureLogger do
context 'on permanent failure' do context 'on permanent failure' do
let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) } let(:processor) { described_class.new(base_payload.merge({ 'severity' => 'permanent' })) }
it 'logs the failure immediately' do before do
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
event: 'email_delivery_failure', .and_return(permanent_email_failure: { threshold: 1, interval: 1.minute })
mailgun_event_id: base_payload['id'], end
recipient: base_payload['recipient'],
failure_type: 'permanent',
failure_reason: base_payload['reason'],
failure_code: base_payload['delivery-status']['code'],
failure_message: base_payload['delivery-status']['message']
)
processor.execute context 'when threshold is not exceeded' do
it 'increments counter but does not log the failure' do
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
:permanent_email_failure, scope: 'recipient@gitlab.com'
).and_call_original
expect(Gitlab::ErrorTracking::Logger).not_to receive(:error)
processor.execute
end
end
context 'when threshold is exceeded' do
before do
processor.execute
end
it 'increments counter and logs the failure' do
expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(
:permanent_email_failure, scope: 'recipient@gitlab.com'
).and_call_original
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
event: 'email_delivery_failure',
mailgun_event_id: base_payload['id'],
recipient: base_payload['recipient'],
failure_type: 'permanent',
failure_reason: base_payload['reason'],
failure_code: base_payload['delivery-status']['code'],
failure_message: base_payload['delivery-status']['message']
)
processor.execute
end
end end
end end

View File

@ -107,10 +107,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'quickactions', 'quickactions',
'pipeline_authoring', 'pipeline_authoring',
'epics_usage', 'epics_usage',
'epic_boards_usage',
'secure', 'secure',
'importer', 'importer',
'network_policies',
'geo', 'geo',
'growth', 'growth',
'work_items', 'work_items',

View File

@ -130,18 +130,22 @@ RSpec.describe NamespaceSetting, type: :model do
describe '#show_diff_preview_in_email?' do describe '#show_diff_preview_in_email?' do
context 'when not a subgroup' do context 'when not a subgroup' do
it 'returns false' do context 'when :show_diff_preview_in_email is false' do
settings = create(:namespace_settings, show_diff_preview_in_email: false) it 'returns false' do
group = create(:group, namespace_settings: settings ) settings = create(:namespace_settings, show_diff_preview_in_email: false)
group = create(:group, namespace_settings: settings )
expect(group.show_diff_preview_in_email?).to be_falsey expect(group.show_diff_preview_in_email?).to be_falsey
end
end end
it 'returns true' do context 'when :show_diff_preview_in_email is true' do
settings = create(:namespace_settings, show_diff_preview_in_email: true) it 'returns true' do
group = create(:group, namespace_settings: settings ) settings = create(:namespace_settings, show_diff_preview_in_email: true)
group = create(:group, namespace_settings: settings )
expect(group.show_diff_preview_in_email?).to be_truthy expect(group.show_diff_preview_in_email?).to be_truthy
end
end end
it 'does not query the db when there is no parent group' do it 'does not query the db when there is no parent group' do

View File

@ -39,6 +39,56 @@ RSpec.describe NotificationRecipient do
expect(recipient.notifiable?).to eq true expect(recipient.notifiable?).to eq true
end end
end end
context 'when recipient email is blocked', :clean_gitlab_redis_rate_limiting do
before do
allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
.and_return(
temporary_email_failure: { threshold: 1, interval: 1.minute },
permanent_email_failure: { threshold: 1, interval: 1.minute }
)
end
context 'with permanent failures' do
before do
2.times { Gitlab::ApplicationRateLimiter.throttled?(:permanent_email_failure, scope: user.email) }
end
it 'returns false' do
expect(recipient.notifiable?).to eq(false)
end
context 'when block_emails_with_failures is disabled' do
before do
stub_feature_flags(block_emails_with_failures: false)
end
it 'returns true' do
expect(recipient.notifiable?).to eq(true)
end
end
end
context 'with temporary failures' do
before do
2.times { Gitlab::ApplicationRateLimiter.throttled?(:temporary_email_failure, scope: user.email) }
end
it 'returns false' do
expect(recipient.notifiable?).to eq(false)
end
context 'when block_emails_with_failures is disabled' do
before do
stub_feature_flags(block_emails_with_failures: false)
end
it 'returns true' do
expect(recipient.notifiable?).to eq(true)
end
end
end
end
end end
describe '#has_access?' do describe '#has_access?' do

View File

@ -161,6 +161,33 @@ RSpec.describe Repository do
end end
end end
context 'semantic versioning sort' do
let(:version_two) { 'v2.0.0' }
let(:version_ten) { 'v10.0.0' }
before do
repository.add_tag(user, version_two, repository.commit.id)
repository.add_tag(user, version_ten, repository.commit.id)
end
after do
repository.rm_tag(user, version_two)
repository.rm_tag(user, version_ten)
end
context 'desc' do
subject { repository.tags_sorted_by('version_desc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
it { is_expected.to eq([version_ten, version_two, 'v1.1.0', 'v1.0.0']) }
end
context 'asc' do
subject { repository.tags_sorted_by('version_asc').map(&:name) & (tags_to_compare + [version_two, version_ten]) }
it { is_expected.to eq(['v1.0.0', 'v1.1.0', version_two, version_ten]) }
end
end
context 'unknown option' do context 'unknown option' do
subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare } subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare }

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting branch protection for a branch rule' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:branch_rule) { create(:protected_branch) }
let_it_be(:project) { branch_rule.project }
let(:branch_protection_data) do
graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection')
end
let(:variables) { { path: project.full_path } }
let(:fields) { all_graphql_fields_for('BranchProtection') }
let(:query) do
<<~GQL
query($path: ID!) {
project(fullPath: $path) {
branchRules(first: 1) {
nodes {
branchProtection {
#{fields}
}
}
}
}
}
GQL
end
context 'when the user does not have read_protected_branch abilities' do
before do
project.add_guest(current_user)
post_graphql(query, current_user: current_user, variables: variables)
end
it_behaves_like 'a working graphql query'
it { expect(branch_protection_data).not_to be_present }
end
context 'when the user does have read_protected_branch abilities' do
before do
project.add_maintainer(current_user)
post_graphql(query, current_user: current_user, variables: variables)
end
it_behaves_like 'a working graphql query'
it 'includes allow_force_push' do
expect(branch_protection_data['allowForcePush']).to be_in([true, false])
expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push)
end
end
end

View File

@ -34,6 +34,7 @@ RSpec.describe API::GroupExport do
before do before do
allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
allow(strategy).to receive(:increment).and_return(0) allow(strategy).to receive(:increment).and_return(0)
allow(strategy).to receive(:read).and_return(0)
end end
upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz") upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")

View File

@ -9,6 +9,7 @@ RSpec.describe API::Search do
let_it_be(:repo_project) { create(:project, :public, :repository, group: group) } let_it_be(:repo_project) { create(:project, :public, :repository, group: group) }
before do before do
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000) allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
end end

View File

@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::Logger, :request_store do
let_it_be(:merge_request) { create(:merge_request) }
subject(:logger) { described_class.new(merge_request: merge_request) }
let(:caller_id) { 'a' }
before do
allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
end
def loggable_data(**extras)
{
'mergeability.expensive_operation.duration_s.values' => a_kind_of(Array),
"mergeability_merge_request_id" => merge_request.id,
"correlation_id" => a_kind_of(String),
"mergeability_project_id" => merge_request.project.id
}.merge(extras)
end
describe '#instrument' do
let(:operation_count) { 1 }
context 'when enabled' do
it "returns the block's value" do
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
end
it 'records durations of instrumented operations' do
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
end
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
logger.commit
end
context 'with multiple observations' do
let(:operation_count) { 2 }
it 'records durations of instrumented operations' do
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data)))
end
2.times do
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
end
logger.commit
end
end
context 'when its a query' do
let(:extra_data) do
{
'mergeability.expensive_operation.db_count.values' => a_kind_of(Array),
'mergeability.expensive_operation.db_main_count.values' => a_kind_of(Array),
'mergeability.expensive_operation.db_main_duration_s.values' => a_kind_of(Array),
'mergeability.expensive_operation.db_primary_count.values' => a_kind_of(Array),
'mergeability.expensive_operation.db_primary_duration_s.values' => a_kind_of(Array)
}
end
context 'with a single query' do
it 'includes SQL metrics' do
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
end
expect(logger.instrument(mergeability_name: :expensive_operation) { MergeRequest.count }).to eq(1)
logger.commit
end
end
context 'with multiple queries' do
it 'includes SQL metrics' do
expect_next_instance_of(Gitlab::AppJsonLogger) do |app_logger|
expect(app_logger).to receive(:info).with(match(a_hash_including(loggable_data(**extra_data))))
end
expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
.to eq(2)
logger.commit
end
end
end
end
context 'when disabled' do
before do
stub_feature_flags(mergeability_checks_logger: false)
end
it "returns the block's value" do
expect(logger.instrument(mergeability_name: :expensive_operation) { 123 }).to eq(123)
end
it 'does not call the logger' do
expect(Gitlab::AppJsonLogger).not_to receive(:new)
expect(logger.instrument(mergeability_name: :expensive_operation) { Project.count + MergeRequest.count })
.to eq(2)
logger.commit
end
end
it 'raises an error when block is not provided' do
expect { logger.instrument(mergeability_name: :expensive_operation) }
.to raise_error(ArgumentError, 'block not given')
end
end
end

View File

@ -69,6 +69,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result) expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result)
end end
expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
expect(logger).to receive(:commit)
end
expect(execute.success?).to eq(true) expect(execute.success?).to eq(true)
end end
end end
@ -80,6 +85,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true) expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true)
end end
expect_next_instance_of(MergeRequests::Mergeability::Logger, merge_request: merge_request) do |logger|
expect(logger).to receive(:instrument).with(mergeability_name: 'check_ci_status_service').and_call_original
expect(logger).to receive(:commit)
end
expect(execute.success?).to eq(true) expect(execute.success?).to eq(true)
end end
end end

View File

@ -0,0 +1,137 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ObjectStorage::CDN::GoogleCDN, :use_clean_rails_memory_store_caching do
include StubRequests
let(:key) { SecureRandom.hex }
let(:key_name) { 'test-key' }
let(:options) { { url: 'https://cdn.gitlab.example.com', key_name: key_name, key: Base64.urlsafe_encode64(key) } }
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
let(:headers) { { 'Content-Type' => 'application/json' } }
let(:public_ip) { '18.245.0.42' }
subject { described_class.new(options) }
before do
WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL)
.to_return(status: 200, body: google_cloud_ips, headers: headers)
end
describe '#use_cdn?' do
using RSpec::Parameterized::TableSyntax
where(:ip_address, :expected) do
'34.80.0.1' | false
'18.245.0.42' | true
'2500:1900:4180:0000:0000:0000:0000:0000' | true
'2600:1900:4180:0000:0000:0000:0000:0000' | false
'10.10.1.5' | false
'fc00:0000:0000:0000:0000:0000:0000:0000' | false
end
with_them do
it { expect(subject.use_cdn?(ip_address)).to eq(expected) }
end
it 'caches the value' do
expect(subject.use_cdn?(public_ip)).to be true
expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_present
expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_present
end
context 'when the key name is missing' do
let(:options) { { url: 'https://cdn.gitlab.example.com', key: Base64.urlsafe_encode64(SecureRandom.hex) } }
it 'returns false' do
expect(subject.use_cdn?(public_ip)).to be false
end
end
context 'when the key is missing' do
let(:options) { { url: 'https://invalid.example.com' } }
it 'returns false' do
expect(subject.use_cdn?(public_ip)).to be false
end
end
context 'when the key is invalid' do
let(:options) { { key_name: key_name, key: '\0x1' } }
it 'returns false' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
expect(subject.use_cdn?(public_ip)).to be false
end
end
context 'when the URL is missing' do
let(:options) { { key: Base64.urlsafe_encode64(SecureRandom.hex) } }
it 'returns false' do
expect(subject.use_cdn?(public_ip)).to be false
end
end
shared_examples 'IP range retrieval failure' do
it 'does not cache the result and logs an error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
expect(subject.use_cdn?(public_ip)).to be false
expect(Rails.cache.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_nil
expect(Gitlab::ProcessMemoryCache.cache_backend.fetch(described_class::GOOGLE_CDN_LIST_KEY)).to be_nil
end
end
context 'when the URL returns a 404' do
before do
WebMock.stub_request(:get, described_class::GOOGLE_IP_RANGES_URL).to_return(status: 404)
end
it_behaves_like 'IP range retrieval failure'
end
context 'when the URL returns too large of a payload' do
before do
stub_const("#{described_class}::RESPONSE_BODY_LIMIT", 300)
end
it_behaves_like 'IP range retrieval failure'
end
context 'when the URL returns HTML' do
let(:headers) { { 'Content-Type' => 'text/html' } }
it_behaves_like 'IP range retrieval failure'
end
context 'when the URL returns empty results' do
let(:google_cloud_ips) { '{}' }
it_behaves_like 'IP range retrieval failure'
end
end
describe '#signed_url' do
let(:path) { '/path/to/file.txt' }
it 'returns a valid signed URL' do
url = subject.signed_url(path)
expect(url).to start_with("#{options[:url]}#{path}")
uri = Addressable::URI.parse(url)
parsed_query = Rack::Utils.parse_nested_query(uri.query)
signature = parsed_query.delete('Signature')
signed_url = "#{options[:url]}#{path}?Expires=#{parsed_query['Expires']}&KeyName=#{key_name}"
computed_signature = OpenSSL::HMAC.digest('SHA1', key, signed_url)
aggregate_failures do
expect(parsed_query['Expires'].to_i).to be > 0
expect(parsed_query['KeyName']).to eq(key_name)
expect(signature).to eq(Base64.urlsafe_encode64(computed_signature))
end
end
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ObjectStorage::CDN do
let(:cdn_options) do
{
'object_store' => {
'cdn' => {
'provider' => 'google',
'url' => 'https://gitlab.example.com',
'key_name' => 'test-key',
'key' => '12345'
}
}
}.freeze
end
let(:uploader_class) do
Class.new(GitlabUploader) do
include ObjectStorage::Concern
include ObjectStorage::CDN::Concern
private
# user/:id
def dynamic_segment
File.join(model.class.underscore, model.id.to_s)
end
end
end
let(:object) { build_stubbed(:user) }
subject { uploader_class.new(object, :file) }
context 'with CDN config' do
before do
uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
end
describe '#use_cdn?' do
it 'returns true' do
expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
expect(cdn).to receive(:use_cdn?).and_return(true)
end
expect(subject.use_cdn?('18.245.0.1')).to be true
end
end
describe '#cdn_signed_url' do
it 'returns a URL' do
expect_next_instance_of(ObjectStorage::CDN::GoogleCDN) do |cdn|
expect(cdn).to receive(:signed_url).and_return("https://cdn.example.com/path")
end
expect(subject.cdn_signed_url).to eq("https://cdn.example.com/path")
end
end
end
context 'without CDN config' do
before do
uploader_class.options = Gitlab.config.uploads
end
describe '#use_cdn?' do
it 'returns false' do
expect(subject.use_cdn?('18.245.0.1')).to be false
end
end
end
context 'with an unknown CDN provider' do
before do
cdn_options['object_store']['cdn']['provider'] = 'amazon'
uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
end
it 'raises an error' do
expect { subject.use_cdn?('18.245.0.1') }.to raise_error("Unknown CDN provider: amazon")
end
end
end