Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
30b1000678
commit
613a8bc141
63 changed files with 374 additions and 874 deletions
|
@ -160,7 +160,6 @@ Lint/MixedRegexpCaptureTypes:
|
|||
- 'lib/gitlab/diff/suggestions_parser.rb'
|
||||
- 'lib/gitlab/github_import/representation/note.rb'
|
||||
- 'lib/gitlab/metrics/system.rb'
|
||||
- 'lib/gitlab/request_profiler/profile.rb'
|
||||
- 'lib/gitlab/slash_commands/issue_move.rb'
|
||||
- 'lib/gitlab/slash_commands/issue_new.rb'
|
||||
- 'lib/gitlab/slash_commands/run.rb'
|
||||
|
|
|
@ -875,7 +875,6 @@ Gitlab/NamespacedClass:
|
|||
- app/workers/repository_import_worker.rb
|
||||
- app/workers/repository_remove_remote_worker.rb
|
||||
- app/workers/repository_update_remote_mirror_worker.rb
|
||||
- app/workers/requests_profiles_worker.rb
|
||||
- app/workers/run_pipeline_schedule_worker.rb
|
||||
- app/workers/schedule_merge_request_cleanup_refs_worker.rb
|
||||
- app/workers/schedule_migrate_external_diffs_worker.rb
|
||||
|
|
|
@ -1 +1 @@
|
|||
7a8fd30510a92d436a5144c3c20f6654ff3ec51d
|
||||
66a4a9452e0ee27a29dd36fffe98ea04dab8ae24
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -195,7 +195,7 @@ gem 'state_machines-activerecord', '~> 0.8.0'
|
|||
gem 'acts-as-taggable-on', '~> 9.0'
|
||||
|
||||
# Background jobs
|
||||
gem 'sidekiq', '~> 6.3'
|
||||
gem 'sidekiq', '~> 6.4'
|
||||
gem 'sidekiq-cron', '~> 1.2'
|
||||
gem 'redis-namespace', '~> 1.8.1'
|
||||
gem 'gitlab-sidekiq-fetcher', '0.8.0', require: 'sidekiq-reliable-fetch'
|
||||
|
|
|
@ -201,7 +201,7 @@ GEM
|
|||
colored2 (3.1.2)
|
||||
commonmarker (0.23.2)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.2)
|
||||
connection_pool (2.2.5)
|
||||
contracts (0.11.0)
|
||||
cork (0.3.0)
|
||||
colored2 (~> 3.1)
|
||||
|
@ -1174,7 +1174,7 @@ GEM
|
|||
shellany (0.0.1)
|
||||
shoulda-matchers (4.0.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.3.1)
|
||||
sidekiq (6.4.0)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
|
@ -1620,7 +1620,7 @@ DEPENDENCIES
|
|||
sentry-raven (~> 3.1)
|
||||
settingslogic (~> 2.0.9)
|
||||
shoulda-matchers (~> 4.0.1)
|
||||
sidekiq (~> 6.3)
|
||||
sidekiq (~> 6.4)
|
||||
sidekiq-cron (~> 1.2)
|
||||
simple_po_parser (~> 1.1.2)
|
||||
simplecov (~> 0.18.5)
|
||||
|
|
|
@ -29,7 +29,6 @@ import {
|
|||
MAX_TREE_WIDTH,
|
||||
TREE_HIDE_STATS_WIDTH,
|
||||
MR_TREE_SHOW_KEY,
|
||||
CENTERED_LIMITED_CONTAINER_CLASSES,
|
||||
ALERT_OVERFLOW_HIDDEN,
|
||||
ALERT_MERGE_CONFLICT,
|
||||
ALERT_COLLAPSED_FILES,
|
||||
|
@ -253,13 +252,6 @@ export default {
|
|||
hideFileStats() {
|
||||
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
|
||||
},
|
||||
isLimitedContainer() {
|
||||
if (this.glFeatures.mrChangesFluidLayout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.renderFileTree && !this.isParallelView && !this.isFluidLayout;
|
||||
},
|
||||
isFullChangeset() {
|
||||
return this.startVersion === null && this.latestDiff;
|
||||
},
|
||||
|
@ -395,8 +387,6 @@ export default {
|
|||
this.adjustView();
|
||||
this.subscribeToEvents();
|
||||
|
||||
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
|
||||
|
||||
this.unwatchDiscussions = this.$watch(
|
||||
() => `${this.diffFiles.length}:${this.$store.state.notes.discussions.length}`,
|
||||
() => this.setDiscussions(),
|
||||
|
@ -643,10 +633,7 @@ export default {
|
|||
<div v-show="shouldShow">
|
||||
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
|
||||
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
|
||||
<compare-versions
|
||||
:is-limited-container="isLimitedContainer"
|
||||
:diff-files-count-text="numTotalFiles"
|
||||
/>
|
||||
<compare-versions :diff-files-count-text="numTotalFiles" />
|
||||
|
||||
<template v-if="!isBatchLoadingError">
|
||||
<hidden-files-warning
|
||||
|
@ -656,10 +643,7 @@ export default {
|
|||
:plain-diff-path="plainDiffPath"
|
||||
:email-patch-path="emailPatchPath"
|
||||
/>
|
||||
<collapsed-files-warning
|
||||
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
|
||||
:limited="isLimitedContainer"
|
||||
/>
|
||||
<collapsed-files-warning v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES" />
|
||||
</template>
|
||||
|
||||
<div
|
||||
|
@ -681,12 +665,7 @@ export default {
|
|||
/>
|
||||
<tree-list :hide-file-stats="hideFileStats" />
|
||||
</div>
|
||||
<div
|
||||
class="col-12 col-md-auto diff-files-holder"
|
||||
:class="{
|
||||
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
|
||||
}"
|
||||
>
|
||||
<div class="col-12 col-md-auto diff-files-holder">
|
||||
<commit-widget v-if="commit" :commit="commit" :collapsible="false" />
|
||||
<gl-alert
|
||||
v-if="isBatchLoadingError"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
|
||||
import { EVT_EXPAND_ALL_FILES } from '../constants';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
|
@ -11,11 +11,6 @@ export default {
|
|||
GlButton,
|
||||
},
|
||||
props: {
|
||||
limited: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
dismissed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -29,11 +24,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['diffFiles']),
|
||||
containerClasses() {
|
||||
return {
|
||||
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
|
||||
};
|
||||
},
|
||||
shouldDisplay() {
|
||||
return !this.isDismissed && this.diffFiles.length > 1;
|
||||
},
|
||||
|
@ -53,7 +43,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldDisplay" data-testid="root" :class="containerClasses" class="col-12">
|
||||
<div v-if="shouldDisplay" data-testid="root" class="col-12">
|
||||
<gl-alert
|
||||
:dismissible="true"
|
||||
:title="__('Some changes are not shown')"
|
||||
|
|
|
@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlLink, GlButtonGroup, GlButton, GlSprintf
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import { setUrlParams } from '../../lib/utils/url_utility';
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
|
||||
import { EVT_EXPAND_ALL_FILES } from '../constants';
|
||||
import eventHub from '../event_hub';
|
||||
import CompareDropdownLayout from './compare_dropdown_layout.vue';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
|
@ -24,11 +24,6 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
isLimitedContainer: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
diffFilesCountText: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -73,9 +68,6 @@ export default {
|
|||
return this.commit && (this.commit.next_commit_id || this.commit.prev_commit_id);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'setShowTreeList']),
|
||||
expandAllFiles() {
|
||||
|
@ -88,12 +80,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="mr-version-controls border-top">
|
||||
<div
|
||||
class="mr-version-menus-container content-block"
|
||||
:class="{
|
||||
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
|
||||
}"
|
||||
>
|
||||
<div class="mr-version-menus-container content-block">
|
||||
<gl-button
|
||||
v-if="hasChanges"
|
||||
v-gl-tooltip.hover
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlAlert, GlModalDirective } from '@gitlab/ui';
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -11,10 +10,6 @@ export default {
|
|||
GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
limited: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
mergeable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -24,18 +19,11 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
containerClasses() {
|
||||
return {
|
||||
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClasses">
|
||||
<div>
|
||||
<gl-alert
|
||||
:dismissible="false"
|
||||
:title="__('There are merge conflicts')"
|
||||
|
|
|
@ -50,9 +50,6 @@ export const NEW_LINE_KEY = 'new_line';
|
|||
export const TYPE_KEY = 'type';
|
||||
export const LEFT_LINE_KEY = 'left';
|
||||
|
||||
export const CENTERED_LIMITED_CONTAINER_CLASSES =
|
||||
'container-limited limit-container-width mx-lg-auto px-3';
|
||||
|
||||
export const MAX_RENDERING_DIFF_LINES = 500;
|
||||
export const MAX_RENDERING_BULK_ROWS = 30;
|
||||
export const MIN_RENDERING_MS = 2;
|
||||
|
|
|
@ -11,8 +11,10 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
|
|||
export default {
|
||||
name: 'RunnerRegistrationTokenReset',
|
||||
i18n: {
|
||||
modalTitle: __('Reset registration token'),
|
||||
modalAction: s__('Runners|Reset token'),
|
||||
modalCancel: __('Cancel'),
|
||||
modalCopy: __('Are you sure you want to reset the registration token?'),
|
||||
modalTitle: __('Reset registration token'),
|
||||
},
|
||||
components: {
|
||||
GlDropdownItem,
|
||||
|
@ -30,7 +32,7 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
},
|
||||
modalID: 'token-reset-modal',
|
||||
modalId: 'token-reset-modal',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
@ -111,10 +113,19 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-dropdown-item v-gl-modal="$options.modalID">
|
||||
<gl-dropdown-item v-gl-modal="$options.modalId">
|
||||
{{ __('Reset registration token') }}
|
||||
<gl-modal
|
||||
:modal-id="$options.modalID"
|
||||
size="sm"
|
||||
:modal-id="$options.modalId"
|
||||
:action-primary="{
|
||||
text: $options.i18n.modalAction,
|
||||
attributes: [{ variant: 'danger' }],
|
||||
}"
|
||||
:action-secondary="{
|
||||
text: $options.i18n.modalCancel,
|
||||
attributes: [{ variant: 'default' }],
|
||||
}"
|
||||
:title="$options.i18n.modalTitle"
|
||||
@primary="handleModalPrimary"
|
||||
>
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::RequestsProfilesController < Admin::ApplicationController
|
||||
feature_category :not_owned
|
||||
|
||||
def index
|
||||
@profile_token = Gitlab::RequestProfiler.profile_token
|
||||
@profiles = Gitlab::RequestProfiler.all.group_by(&:request_path)
|
||||
end
|
||||
|
||||
def show
|
||||
clean_name = Rack::Utils.clean_path_info(params[:name])
|
||||
profile = Gitlab::RequestProfiler.find(clean_name)
|
||||
|
||||
unless profile && profile.content_type
|
||||
return redirect_to admin_requests_profiles_path, alert: 'Profile not found'
|
||||
end
|
||||
|
||||
send_file profile.file_path, type: "#{profile.content_type}; charset=utf-8", disposition: 'inline'
|
||||
end
|
||||
end
|
|
@ -40,7 +40,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:rebase_without_ci_ui, @project, default_enabled: :yaml)
|
||||
|
|
28
app/graphql/mutations/user_preferences/update.rb
Normal file
28
app/graphql/mutations/user_preferences/update.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module UserPreferences
|
||||
class Update < BaseMutation
|
||||
graphql_name 'UserPreferencesUpdate'
|
||||
|
||||
argument :issues_sort, Types::IssueSortEnum,
|
||||
required: false,
|
||||
description: 'Sort order for issue lists.'
|
||||
|
||||
field :user_preferences,
|
||||
Types::UserPreferencesType,
|
||||
null: true,
|
||||
description: 'User preferences after mutation.'
|
||||
|
||||
def resolve(**attributes)
|
||||
user_preferences = current_user.user_preference
|
||||
user_preferences.update(attributes)
|
||||
|
||||
{
|
||||
user_preferences: user_preferences.valid? ? user_preferences : nil,
|
||||
errors: errors_on_object(user_preferences)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -121,6 +121,7 @@ module Types
|
|||
mount_mutation Mutations::Namespace::PackageSettings::Update
|
||||
mount_mutation Mutations::Groups::Update
|
||||
mount_mutation Mutations::UserCallouts::Create
|
||||
mount_mutation Mutations::UserPreferences::Update
|
||||
mount_mutation Mutations::Packages::Destroy
|
||||
mount_mutation Mutations::Packages::DestroyFile
|
||||
mount_mutation Mutations::Echo
|
||||
|
|
17
app/graphql/types/user_preferences_type.rb
Normal file
17
app/graphql/types/user_preferences_type.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
# Only used to render the current user's own preferences
|
||||
class UserPreferencesType < BaseObject
|
||||
graphql_name 'UserPreferences'
|
||||
|
||||
field :issues_sort, Types::IssueSortEnum,
|
||||
description: 'Sort order for issue lists.',
|
||||
null: true
|
||||
|
||||
def issues_sort
|
||||
object.issues_sort.to_sym
|
||||
end
|
||||
end
|
||||
end
|
|
@ -56,7 +56,7 @@ module NavHelper
|
|||
end
|
||||
|
||||
def admin_monitoring_nav_links
|
||||
%w(system_info background_migrations background_jobs health_check requests_profiles)
|
||||
%w(system_info background_migrations background_jobs health_check)
|
||||
end
|
||||
|
||||
def admin_analytics_nav_links
|
||||
|
|
|
@ -49,7 +49,10 @@ class WebHookService
|
|||
def execute
|
||||
return { status: :error, message: 'Hook disabled' } unless hook.executable?
|
||||
|
||||
log_recursion_limit if recursion_blocked?
|
||||
if recursion_blocked?
|
||||
log_recursion_blocked
|
||||
return { status: :error, message: 'Recursive webhook blocked' }
|
||||
end
|
||||
|
||||
Gitlab::WebHooks::RecursionDetection.register!(hook)
|
||||
|
||||
|
@ -96,9 +99,8 @@ class WebHookService
|
|||
|
||||
def async_execute
|
||||
Gitlab::ApplicationContext.with_context(hook.application_context) do
|
||||
break log_rate_limit if rate_limited?
|
||||
|
||||
log_recursion_limit if recursion_blocked?
|
||||
break log_rate_limited if rate_limited?
|
||||
break log_recursion_blocked if recursion_blocked?
|
||||
|
||||
data[:_gitlab_recursion_detection_request_uuid] = Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
|
||||
|
||||
|
@ -202,7 +204,7 @@ class WebHookService
|
|||
@rate_limit ||= hook.rate_limit
|
||||
end
|
||||
|
||||
def log_rate_limit
|
||||
def log_rate_limited
|
||||
Gitlab::AuthLogger.error(
|
||||
message: 'Webhook rate limit exceeded',
|
||||
hook_id: hook.id,
|
||||
|
@ -212,9 +214,9 @@ class WebHookService
|
|||
)
|
||||
end
|
||||
|
||||
def log_recursion_limit
|
||||
def log_recursion_blocked
|
||||
Gitlab::AuthLogger.error(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: hook.id,
|
||||
hook_type: hook.type,
|
||||
hook_name: hook_name,
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
- page_title _('Requests Profiles')
|
||||
|
||||
%h3.page-title
|
||||
= page_title
|
||||
|
||||
.bs-callout.clearfix
|
||||
= html_escape(_('Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request')) % { profile_token: @profile_token, codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe }
|
||||
|
||||
- if @profiles.present?
|
||||
.gl-mt-3
|
||||
- @profiles.each do |path, profiles|
|
||||
.card
|
||||
.card-header
|
||||
%code= path
|
||||
%ul.content-list
|
||||
- profiles.each do |profile|
|
||||
%li
|
||||
= link_to profile.time.to_s(:long) + ' ' + profile.profile_mode.capitalize,
|
||||
admin_requests_profile_path(profile)
|
||||
- else
|
||||
%p
|
||||
= _('No profiles found')
|
|
@ -103,10 +103,6 @@
|
|||
= link_to admin_health_check_path, title: _('Health Check') do
|
||||
%span
|
||||
= _('Health Check')
|
||||
= nav_link(controller: :requests_profiles) do
|
||||
= link_to admin_requests_profiles_path, title: _('Requests Profiles') do
|
||||
%span
|
||||
= _('Requests Profiles')
|
||||
- if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
|
||||
= nav_link do
|
||||
= link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard'), rel: 'noopener noreferrer' do
|
||||
|
|
|
@ -597,15 +597,6 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:requests_profiles
|
||||
:worker_name: RequestsProfilesWorker
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:schedule_merge_request_cleanup_refs
|
||||
:worker_name: ScheduleMergeRequestCleanupRefsWorker
|
||||
:feature_category: :code_review
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RequestsProfilesWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
|
||||
# rubocop:disable Scalability/CronWorkerContext
|
||||
# This worker does not perform work scoped to a context
|
||||
include CronjobQueue
|
||||
# rubocop:enable Scalability/CronWorkerContext
|
||||
|
||||
feature_category :source_code_management
|
||||
|
||||
def perform
|
||||
Gitlab::RequestProfiler.remove_all_profiles
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: import_redis_increment_by
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65773
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336226
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::import
|
||||
default_enabled: true
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: mr_changes_fluid_layout
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70815
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341809
|
||||
milestone: '14.4'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: true
|
|
@ -479,9 +479,6 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
|
|||
Settings.cron_jobs['ci_archive_traces_cron_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['ci_archive_traces_cron_worker']['cron'] ||= '17 * * * *'
|
||||
Settings.cron_jobs['ci_archive_traces_cron_worker']['job_class'] = 'Ci::ArchiveTracesCronWorker'
|
||||
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
|
||||
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
|
||||
Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
|
||||
Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.configure do |config|
|
||||
config.middleware.use(Gitlab::RequestProfiler::Middleware)
|
||||
config.middleware.use(Gitlab::Middleware::Speedscope)
|
||||
end
|
||||
|
|
|
@ -100,7 +100,6 @@ namespace :admin do
|
|||
resource :background_jobs, controller: 'background_jobs', only: [:show]
|
||||
|
||||
resource :system_info, controller: 'system_info', only: [:show]
|
||||
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.(html|txt)/ }
|
||||
|
||||
resources :projects, only: [:index]
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveProjectsCiBuildsMetadataProjectIdFk < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return if Gitlab.com? # unsafe migration, skip on GitLab.com due to https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819625526
|
||||
return unless foreign_key_exists?(:ci_builds_metadata, :projects, name: "fk_rails_ffcf702a02")
|
||||
|
||||
with_lock_retries do
|
||||
execute('LOCK projects, ci_builds_metadata IN ACCESS EXCLUSIVE MODE') if transaction_open?
|
||||
|
||||
remove_foreign_key_if_exists(:ci_builds_metadata, :projects, name: "fk_rails_ffcf702a02")
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:ci_builds_metadata, :projects, name: "fk_rails_ffcf702a02", column: :project_id, target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220124180704
Normal file
1
db/schema_migrations/20220124180704
Normal file
|
@ -0,0 +1 @@
|
|||
5b5f47f1d7038518fef71dd8c0758b234bb890be9aab57b78918f7b2dc39bdc4
|
|
@ -31538,9 +31538,6 @@ ALTER TABLE ONLY resource_label_events
|
|||
ALTER TABLE ONLY pages_deployment_states
|
||||
ADD CONSTRAINT fk_rails_ff6ca551a4 FOREIGN KEY (pages_deployment_id) REFERENCES pages_deployments(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_builds_metadata
|
||||
ADD CONSTRAINT fk_rails_ffcf702a02 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY security_orchestration_policy_configurations
|
||||
ADD CONSTRAINT fk_security_policy_configurations_management_project_id FOREIGN KEY (security_policy_management_project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -140,6 +140,18 @@ for details.
|
|||
To use TLS certificates with Let's Encrypt, you can manually point the domain to one of the Geo sites, generate
|
||||
the certificate, then copy it to all other sites.
|
||||
|
||||
## Behavior of secondary sites when the primary Geo site is down
|
||||
|
||||
Considering that web traffic is proxied to the primary, the behavior of the secondary sites differs when the primary
|
||||
site is inaccessible:
|
||||
|
||||
- UI and API traffic return the same errors as the primary (or fail if the primary is not accessible at all), since they are proxied.
|
||||
- For repositories that already exist on the specific secondary site being accessed, Git read operations still work as expected,
|
||||
including authentication through HTTP(s) or SSH.
|
||||
- Git operations for repositories that are not replicated to the secondary site return the same errors
|
||||
as the primary site, since they are proxied.
|
||||
- All Git write operations return the same errors as the primary site, since they are proxied.
|
||||
|
||||
## Features accelerated by secondary Geo sites
|
||||
|
||||
Most HTTP traffic sent to a secondary Geo site can be proxied to the primary Geo site. With this architecture,
|
||||
|
|
|
@ -893,6 +893,9 @@ Instead, follow the instructions below.
|
|||
A production-ready and secure setup requires at least three Consul nodes, two
|
||||
Patroni nodes and one PgBouncer node on the secondary site.
|
||||
|
||||
Because of [omnibus-6587](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6587), Consul can't track multiple
|
||||
services, so these need to be different than the nodes used for the Standby Cluster database.
|
||||
|
||||
Be sure to use [password credentials](../../postgresql/replication_and_failover.md#database-authorization-for-patroni)
|
||||
and other database best practices.
|
||||
|
||||
|
|
|
@ -228,7 +228,11 @@ There is a limit when embedding metrics in GitLab Flavored Markdown (GFM) for pe
|
|||
|
||||
- **Max limit**: 100 embeds.
|
||||
|
||||
## Number of webhooks
|
||||
## Webhook limits
|
||||
|
||||
Also see [Webhook rate limits](#webhook-rate-limit).
|
||||
|
||||
### Number of webhooks
|
||||
|
||||
To set the maximum number of group or project webhooks for a self-managed installation,
|
||||
run the following in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
|
||||
|
@ -246,11 +250,33 @@ Plan.default.actual_limits.update!(group_hooks: 100)
|
|||
|
||||
Set the limit to `0` to disable it.
|
||||
|
||||
- **Default maximum number of webhooks**: `100` per project, `50` per group
|
||||
- **Maximum payload size**: 25 MB
|
||||
The default maximum number of webhooks is `100` per project, `50` per group.
|
||||
|
||||
For GitLab.com, see the [webhook limits for GitLab.com](../user/gitlab_com/index.md#webhooks).
|
||||
|
||||
### Webhook payload size
|
||||
|
||||
The maximum webhook payload size is 25 MB.
|
||||
|
||||
### Recursive webhooks
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329743) in GitLab 14.8.
|
||||
|
||||
GitLab detects and blocks webhooks that are recursive or that exceed the limit
|
||||
of webhooks that can be triggered from other webhooks. This enables GitLab to
|
||||
continue to support workflows that use webhooks to call the API non-recursively, or that
|
||||
do not trigger an unreasonable number of other webhooks.
|
||||
|
||||
Recursion can happen when a webhook is configured to make a call
|
||||
to its own GitLab instance (for example, the API). The call then triggers the same
|
||||
webhook and creates an infinite loop.
|
||||
|
||||
The maximum number of requests to an instance made by a series of webhooks that
|
||||
trigger other webhooks is 100. When the limit is reached, GitLab blocks any further
|
||||
webhooks that would be triggered by the series.
|
||||
|
||||
Blocked recursive webhook calls are logged in `auth.log` with the message `"Recursive webhook blocked from executing"`.
|
||||
|
||||
## Pull Mirroring Interval
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/237891) in GitLab 13.7.
|
||||
|
|
|
@ -4886,6 +4886,25 @@ Input type: `UserCalloutCreateInput`
|
|||
| <a id="mutationusercalloutcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationusercalloutcreateusercallout"></a>`userCallout` | [`UserCallout!`](#usercallout) | User callout dismissed. |
|
||||
|
||||
### `Mutation.userPreferencesUpdate`
|
||||
|
||||
Input type: `UserPreferencesUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationuserpreferencesupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationuserpreferencesupdateissuessort"></a>`issuesSort` | [`IssueSort`](#issuesort) | Sort order for issue lists. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationuserpreferencesupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationuserpreferencesupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationuserpreferencesupdateuserpreferences"></a>`userPreferences` | [`UserPreferences`](#userpreferences) | User preferences after mutation. |
|
||||
|
||||
### `Mutation.vulnerabilityConfirm`
|
||||
|
||||
Input type: `VulnerabilityConfirmInput`
|
||||
|
@ -15740,6 +15759,14 @@ fields relate to interactions between the two entities.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="userpermissionscreatesnippet"></a>`createSnippet` | [`Boolean!`](#boolean) | Indicates the user can perform `create_snippet` on this resource. |
|
||||
|
||||
### `UserPreferences`
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="userpreferencesissuessort"></a>`issuesSort` | [`IssueSort`](#issuesort) | Sort order for issue lists. |
|
||||
|
||||
### `UserStatus`
|
||||
|
||||
#### Fields
|
||||
|
|
|
@ -690,6 +690,9 @@ Roles are not the same as [**access levels**](#access-level).
|
|||
|
||||
Use lowercase for **runners**. These are the agents that run CI/CD jobs. See also [GitLab Runner](#gitlab-runner) and [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/233529).
|
||||
|
||||
When referring to runners, if you have to specify that the runners are installed on a customer's GitLab instance,
|
||||
use **self-managed** rather than **self-hosted**.
|
||||
|
||||
## (s)
|
||||
|
||||
Do not use **(s)** to make a word optionally plural. It can slow down comprehension. For example:
|
||||
|
@ -735,6 +738,10 @@ Instead of:
|
|||
Use **select** with buttons, links, menu items, and lists. **Select** applies to more devices,
|
||||
while **click** is more specific to a mouse.
|
||||
|
||||
## self-managed
|
||||
|
||||
Use **self-managed** to refer to a customer's installation of GitLab. Do not use **self-hosted**.
|
||||
|
||||
## Service Desk
|
||||
|
||||
Use title case for **Service Desk**.
|
||||
|
|
|
@ -17,7 +17,6 @@ module Gitlab
|
|||
lib/gitlab/profiler.rb
|
||||
lib/gitlab/query_limiting/
|
||||
lib/gitlab/request_context.rb
|
||||
lib/gitlab/request_profiler/
|
||||
lib/gitlab/sidekiq_logging/
|
||||
lib/gitlab/sidekiq_middleware/
|
||||
lib/gitlab/sidekiq_status/
|
||||
|
|
|
@ -132,6 +132,10 @@ pages_deployments:
|
|||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_nullify
|
||||
ci_builds_metadata:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
terraform_state_versions:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
|
|
|
@ -1054,9 +1054,18 @@ module Gitlab
|
|||
Arel::Nodes::SqlLiteral.new(replace.to_sql)
|
||||
end
|
||||
|
||||
def remove_foreign_key_if_exists(...)
|
||||
if foreign_key_exists?(...)
|
||||
remove_foreign_key(...)
|
||||
def remove_foreign_key_if_exists(source, target = nil, **kwargs)
|
||||
reverse_lock_order = kwargs.delete(:reverse_lock_order)
|
||||
return unless foreign_key_exists?(source, target, **kwargs)
|
||||
|
||||
if target && reverse_lock_order && transaction_open?
|
||||
execute("LOCK TABLE #{target}, #{source} IN ACCESS EXCLUSIVE MODE")
|
||||
end
|
||||
|
||||
if target
|
||||
remove_foreign_key(source, target, **kwargs)
|
||||
else
|
||||
remove_foreign_key(source, **kwargs)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -71,11 +71,7 @@ module Gitlab
|
|||
|
||||
add_counter_to_list(project, operation, counter_key)
|
||||
|
||||
if Feature.disabled?(:import_redis_increment_by, default_enabled: :yaml)
|
||||
CACHING.increment(counter_key)
|
||||
else
|
||||
CACHING.increment_by(counter_key, value)
|
||||
end
|
||||
CACHING.increment_by(counter_key, value)
|
||||
end
|
||||
|
||||
def add_counter_to_list(project, operation, key)
|
||||
|
|
|
@ -16,7 +16,6 @@ module Gitlab
|
|||
lib/gitlab/middleware/
|
||||
ee/lib/gitlab/middleware/
|
||||
lib/gitlab/performance_bar/
|
||||
lib/gitlab/request_profiler/
|
||||
lib/gitlab/query_limiting/
|
||||
lib/gitlab/tracing/
|
||||
lib/gitlab/profiler.rb
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
module Gitlab
|
||||
module RequestProfiler
|
||||
PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
|
||||
|
||||
def all
|
||||
Dir["#{PROFILES_DIR}/*.{html,txt}"].map do |path|
|
||||
Profile.new(File.basename(path))
|
||||
end.select(&:valid?)
|
||||
end
|
||||
module_function :all # rubocop: disable Style/AccessModifierDeclarations
|
||||
|
||||
def find(name)
|
||||
file_path = File.join(PROFILES_DIR, name)
|
||||
return unless File.exist?(file_path)
|
||||
|
||||
Profile.new(name)
|
||||
end
|
||||
module_function :find # rubocop: disable Style/AccessModifierDeclarations
|
||||
|
||||
def profile_token
|
||||
Rails.cache.fetch('profile-token') do
|
||||
Devise.friendly_token
|
||||
end
|
||||
end
|
||||
module_function :profile_token # rubocop: disable Style/AccessModifierDeclarations
|
||||
|
||||
def remove_all_profiles
|
||||
FileUtils.rm_rf(PROFILES_DIR)
|
||||
end
|
||||
module_function :remove_all_profiles # rubocop: disable Style/AccessModifierDeclarations
|
||||
end
|
||||
end
|
|
@ -1,107 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'ruby-prof'
|
||||
require 'memory_profiler'
|
||||
|
||||
module Gitlab
|
||||
module RequestProfiler
|
||||
class Middleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if profile?(env)
|
||||
call_with_profiling(env)
|
||||
else
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
def profile?(env)
|
||||
header_token = env['HTTP_X_PROFILE_TOKEN']
|
||||
return unless header_token.present?
|
||||
|
||||
profile_token = Gitlab::RequestProfiler.profile_token
|
||||
return unless profile_token.present?
|
||||
|
||||
header_token == profile_token
|
||||
end
|
||||
|
||||
def call_with_profiling(env)
|
||||
case env['HTTP_X_PROFILE_MODE']
|
||||
when 'execution', nil
|
||||
call_with_call_stack_profiling(env)
|
||||
when 'memory'
|
||||
call_with_memory_profiling(env)
|
||||
else
|
||||
raise ActionController::BadRequest, invalid_profile_mode(env)
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_profile_mode(env)
|
||||
<<~HEREDOC
|
||||
Invalid X-Profile-Mode: #{env['HTTP_X_PROFILE_MODE']}.
|
||||
Supported profile mode request header:
|
||||
- X-Profile-Mode: execution
|
||||
- X-Profile-Mode: memory
|
||||
HEREDOC
|
||||
end
|
||||
|
||||
def call_with_call_stack_profiling(env)
|
||||
ret = nil
|
||||
report = RubyProf::Profile.profile do
|
||||
ret = catch(:warden) do # rubocop:disable Cop/BanCatchThrow
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
generate_report(env, 'execution', 'html') do |file|
|
||||
printer = RubyProf::CallStackPrinter.new(report)
|
||||
printer.print(file)
|
||||
end
|
||||
|
||||
handle_request_ret(ret)
|
||||
end
|
||||
|
||||
def call_with_memory_profiling(env)
|
||||
ret = nil
|
||||
report = MemoryProfiler.report do
|
||||
ret = catch(:warden) do # rubocop:disable Cop/BanCatchThrow
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
generate_report(env, 'memory', 'txt') do |file|
|
||||
report.pretty_print(to_file: file)
|
||||
end
|
||||
|
||||
handle_request_ret(ret)
|
||||
end
|
||||
|
||||
def generate_report(env, report_type, extension)
|
||||
file_name = "#{env['PATH_INFO'].tr('/', '|')}_#{Time.current.to_i}"\
|
||||
"_#{report_type}.#{extension}"
|
||||
file_path = "#{PROFILES_DIR}/#{file_name}"
|
||||
|
||||
FileUtils.mkdir_p(PROFILES_DIR)
|
||||
|
||||
begin
|
||||
File.open(file_path, 'wb') do |file|
|
||||
yield(file)
|
||||
end
|
||||
rescue StandardError
|
||||
FileUtils.rm(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_request_ret(ret)
|
||||
if ret.is_a?(Array)
|
||||
ret
|
||||
else
|
||||
throw(:warden, ret) # rubocop:disable Cop/BanCatchThrow
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module RequestProfiler
|
||||
class Profile
|
||||
attr_reader :name, :time, :file_path, :request_path, :profile_mode, :type
|
||||
|
||||
alias_method :to_param, :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
@file_path = File.join(PROFILES_DIR, name)
|
||||
|
||||
set_attributes
|
||||
end
|
||||
|
||||
def valid?
|
||||
@request_path.present?
|
||||
end
|
||||
|
||||
def content_type
|
||||
case type
|
||||
when 'html'
|
||||
'text/html'
|
||||
when 'txt'
|
||||
'text/plain'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_attributes
|
||||
matches = name.match(/^(?<path>.*)_(?<timestamp>\d+)(_(?<profile_mode>\w+))?\.(?<type>html|txt)$/)
|
||||
return unless matches
|
||||
|
||||
@request_path = matches[:path].tr('|', '/')
|
||||
@time = Time.at(matches[:timestamp].to_i).utc
|
||||
@profile_mode = matches[:profile_mode] || 'unknown'
|
||||
@type = matches[:type]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24162,9 +24162,6 @@ msgstr ""
|
|||
msgid "No prioritized labels with such name or description"
|
||||
msgstr ""
|
||||
|
||||
msgid "No profiles found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No project subscribes to the pipelines in this project."
|
||||
msgstr ""
|
||||
|
||||
|
@ -25712,9 +25709,6 @@ msgstr ""
|
|||
msgid "Pass job variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Passed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30313,9 +30307,6 @@ msgstr ""
|
|||
msgid "Requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requests Profiles"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requests for pages at %{code_start}%{help_text_url}%{code_end} redirect to the URL. The destination must meet certain requirements. %{docs_link_start}Learn more.%{docs_link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30900,6 +30891,9 @@ msgstr ""
|
|||
msgid "Runners|Registration token copied!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Reset token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Revision"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::RequestsProfilesController do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
let(:tmpdir) { Dir.mktmpdir('profiler-test') }
|
||||
let(:test_file) { File.join(tmpdir, basename) }
|
||||
|
||||
subject do
|
||||
get :show, params: { name: basename }
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('Gitlab::RequestProfiler::PROFILES_DIR', tmpdir)
|
||||
File.write(test_file, sample_data)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(tmpdir)
|
||||
end
|
||||
|
||||
context 'when loading HTML profile' do
|
||||
let(:basename) { "profile_#{Time.current.to_i}_execution.html" }
|
||||
|
||||
let(:sample_data) do
|
||||
'<html> <body> <h1>Heading</h1> <p>paragraph.</p> </body> </html>'
|
||||
end
|
||||
|
||||
it 'renders the data' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to eq(sample_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when loading TXT profile' do
|
||||
let(:basename) { "profile_#{Time.current.to_i}_memory.txt" }
|
||||
|
||||
let(:sample_data) do
|
||||
<<~TXT
|
||||
Total allocated: 112096396 bytes (1080431 objects)
|
||||
Total retained: 10312598 bytes (53567 objects)
|
||||
TXT
|
||||
end
|
||||
|
||||
it 'renders the data' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to eq(sample_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when loading PDF profile' do
|
||||
let(:basename) { "profile_#{Time.current.to_i}_anything.pdf" }
|
||||
|
||||
let(:sample_data) { 'mocked pdf content' }
|
||||
|
||||
it 'fails to render the data' do
|
||||
expect { subject }.to raise_error(ActionController::UrlGenerationError, /No route matches.*unmatched constraints:/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
7
spec/factories/ci/build_metadata.rb
Normal file
7
spec/factories/ci/build_metadata.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :ci_build_metadata, class: 'Ci::BuildMetadata' do
|
||||
build { association(:ci_build, strategy: :build, metadata: instance) }
|
||||
end
|
||||
end
|
|
@ -1,136 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Admin::RequestsProfilesController' do
|
||||
let(:tmpdir) { Dir.mktmpdir('profiler-test') }
|
||||
|
||||
before do
|
||||
stub_const('Gitlab::RequestProfiler::PROFILES_DIR', tmpdir)
|
||||
admin = create(:admin)
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(tmpdir)
|
||||
end
|
||||
|
||||
describe 'GET /admin/requests_profiles' do
|
||||
it 'shows the current profile token' do
|
||||
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
|
||||
|
||||
visit admin_requests_profiles_path
|
||||
|
||||
expect(page).to have_content("X-Profile-Token: #{Gitlab::RequestProfiler.profile_token}")
|
||||
end
|
||||
|
||||
context 'when having multiple profiles' do
|
||||
let(:time1) { 1.hour.ago }
|
||||
let(:time2) { 2.hours.ago }
|
||||
|
||||
let(:profiles) do
|
||||
[
|
||||
{
|
||||
request_path: '/gitlab-org/gitlab-foss',
|
||||
name: "|gitlab-org|gitlab-foss_#{time1.to_i}_execution.html",
|
||||
created: time1,
|
||||
profile_mode: 'Execution'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/gitlab-foss',
|
||||
name: "|gitlab-org|gitlab-foss_#{time2.to_i}_execution.html",
|
||||
created: time2,
|
||||
profile_mode: 'Execution'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/gitlab-foss',
|
||||
name: "|gitlab-org|gitlab-foss_#{time1.to_i}_memory.html",
|
||||
created: time1,
|
||||
profile_mode: 'Memory'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/gitlab-foss',
|
||||
name: "|gitlab-org|gitlab-foss_#{time2.to_i}_memory.html",
|
||||
created: time2,
|
||||
profile_mode: 'Memory'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/infrastructure',
|
||||
name: "|gitlab-org|infrastructure_#{time1.to_i}_execution.html",
|
||||
created: time1,
|
||||
profile_mode: 'Execution'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/infrastructure',
|
||||
name: "|gitlab-org|infrastructure_#{time2.to_i}_memory.html",
|
||||
created: time2,
|
||||
profile_mode: 'Memory'
|
||||
},
|
||||
{
|
||||
request_path: '/gitlab-org/infrastructure',
|
||||
name: "|gitlab-org|infrastructure_#{time2.to_i}.html",
|
||||
created: time2,
|
||||
profile_mode: 'Unknown'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
profiles.each do |profile|
|
||||
FileUtils.touch(File.join(Gitlab::RequestProfiler::PROFILES_DIR, profile[:name]))
|
||||
end
|
||||
end
|
||||
|
||||
it 'lists all available profiles' do
|
||||
visit admin_requests_profiles_path
|
||||
|
||||
profiles.each do |profile|
|
||||
within('.card', text: profile[:request_path]) do
|
||||
expect(page).to have_selector(
|
||||
"a[href='#{admin_requests_profile_path(profile[:name])}']",
|
||||
text: "#{profile[:created].to_s(:long)} #{profile[:profile_mode]}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /admin/requests_profiles/:profile' do
|
||||
context 'when a profile exists' do
|
||||
before do
|
||||
File.write("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile}", content)
|
||||
end
|
||||
|
||||
context 'when is valid call stack profile' do
|
||||
let(:content) { 'This is a call stack request profile' }
|
||||
let(:profile) { "|gitlab-org|gitlab-ce_#{Time.now.to_i}_execution.html" }
|
||||
|
||||
it 'displays the content' do
|
||||
visit admin_requests_profile_path(profile)
|
||||
|
||||
expect(page).to have_content(content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is valid memory profile' do
|
||||
let(:content) { 'This is a memory request profile' }
|
||||
let(:profile) { "|gitlab-org|gitlab-ce_#{Time.now.to_i}_memory.txt" }
|
||||
|
||||
it 'displays the content' do
|
||||
visit admin_requests_profile_path(profile)
|
||||
|
||||
expect(page).to have_content(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a profile does not exist' do
|
||||
it 'shows an error message' do
|
||||
visit admin_requests_profile_path('|non|existent_12345.html')
|
||||
|
||||
expect(page).to have_content('Profile not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -472,7 +472,7 @@ RSpec.describe "Admin Runners" do
|
|||
click_on 'Reset registration token'
|
||||
|
||||
within_modal do
|
||||
click_button('OK', match: :first)
|
||||
click_button('Reset token', match: :first)
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
@ -154,22 +154,6 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
props | state | expected
|
||||
${{ isFluidLayout: true }} | ${{ isParallelView: false }} | ${false}
|
||||
${{}} | ${{ isParallelView: false }} | ${true}
|
||||
${{}} | ${{ showTreeList: true, diffFiles: [{}], isParallelView: false }} | ${false}
|
||||
${{}} | ${{ showTreeList: false, diffFiles: [{}], isParallelView: false }} | ${true}
|
||||
${{}} | ${{ showTreeList: false, diffFiles: [], isParallelView: false }} | ${true}
|
||||
`(
|
||||
'uses container-limiting classes ($expected) with state ($state) and props ($props)',
|
||||
({ props, state, expected }) => {
|
||||
createComponent(props, ({ state: origState }) => Object.assign(origState.diffs, state));
|
||||
|
||||
expect(wrapper.find('.container-limited.limit-container-width').exists()).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('displays loading icon on loading', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.isLoading = true;
|
||||
|
@ -498,7 +482,6 @@ describe('diffs/components/app', () => {
|
|||
expect(wrapper.find(CompareVersions).exists()).toBe(true);
|
||||
expect(wrapper.find(CompareVersions).props()).toEqual(
|
||||
expect.objectContaining({
|
||||
isLimitedContainer: false,
|
||||
diffFilesCountText: null,
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { shallowMount, mount } from '@vue/test-utils';
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
|
||||
import { EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
|
||||
import eventHub from '~/diffs/event_hub';
|
||||
import createStore from '~/diffs/store/modules';
|
||||
|
||||
|
@ -13,7 +13,6 @@ const propsData = {
|
|||
mergeable: true,
|
||||
resolutionPath: 'a-path',
|
||||
};
|
||||
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
|
||||
|
||||
async function files(store, count) {
|
||||
const copies = Array(count).fill(file);
|
||||
|
@ -51,20 +50,6 @@ describe('CollapsedFilesWarning', () => {
|
|||
});
|
||||
|
||||
describe('when there is more than one file', () => {
|
||||
it.each`
|
||||
limited | containerClasses
|
||||
${true} | ${limitedClasses}
|
||||
${false} | ${[]}
|
||||
`(
|
||||
'has the correct container classes when limited is $limited',
|
||||
async ({ limited, containerClasses }) => {
|
||||
createComponent({ limited });
|
||||
await files(store, 2);
|
||||
|
||||
expect(wrapper.classes()).toEqual(['col-12'].concat(containerClasses));
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
present | dismissed
|
||||
${false} | ${true}
|
||||
|
|
|
@ -38,7 +38,6 @@ describe('CompareVersions', () => {
|
|||
},
|
||||
});
|
||||
};
|
||||
const findLimitedContainer = () => wrapper.find('.container-limited.limit-container-width');
|
||||
const findCompareSourceDropdown = () => wrapper.find('.mr-version-dropdown');
|
||||
const findCompareTargetDropdown = () => wrapper.find('.mr-version-compare-dropdown');
|
||||
const getCommitNavButtonsElement = () => wrapper.find('.commit-nav-buttons');
|
||||
|
@ -98,18 +97,6 @@ describe('CompareVersions', () => {
|
|||
expect(inlineBtn.html()).toContain('Inline');
|
||||
expect(parallelBtn.html()).toContain('Side-by-side');
|
||||
});
|
||||
|
||||
it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
|
||||
createWrapper({ isLimitedContainer: true });
|
||||
|
||||
expect(findLimitedContainer().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
|
||||
createWrapper({ isLimitedContainer: false });
|
||||
|
||||
expect(findLimitedContainer().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('noChangedFiles', () => {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue';
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
|
||||
|
||||
const propsData = {
|
||||
limited: true,
|
||||
mergeable: true,
|
||||
resolutionPath: 'a-path',
|
||||
};
|
||||
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
|
||||
|
||||
function findResolveButton(wrapper) {
|
||||
return wrapper.find('.gl-alert-actions a.gl-button:first-child');
|
||||
|
@ -31,19 +29,6 @@ describe('MergeConflictWarning', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each`
|
||||
limited | containerClasses
|
||||
${true} | ${limitedClasses}
|
||||
${false} | ${[]}
|
||||
`(
|
||||
'has the correct container classes when limited is $limited',
|
||||
({ limited, containerClasses }) => {
|
||||
createComponent({ limited });
|
||||
|
||||
expect(wrapper.classes()).toEqual(containerClasses);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
present | resolutionPath
|
||||
${false} | ${''}
|
||||
|
|
15
spec/graphql/types/user_preferences_type_spec.rb
Normal file
15
spec/graphql/types/user_preferences_type_spec.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::UserPreferencesType do
|
||||
specify { expect(described_class.graphql_name).to eq('UserPreferences') }
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
issues_sort
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -25,7 +25,6 @@ RSpec.describe Gitlab::BacktraceCleaner do
|
|||
"app/models/repository.rb:113:in `commit'",
|
||||
"lib/gitlab/i18n.rb:50:in `with_locale'",
|
||||
"lib/gitlab/middleware/multipart.rb:95:in `call'",
|
||||
"lib/gitlab/request_profiler/middleware.rb:14:in `call'",
|
||||
"ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'",
|
||||
"ee/lib/gitlab/jira/middleware.rb:15:in `call'"
|
||||
]
|
||||
|
|
|
@ -442,6 +442,60 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#remove_foreign_key_if_exists' do
|
||||
context 'when the foreign key does not exist' do
|
||||
before do
|
||||
allow(model).to receive(:foreign_key_exists?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does nothing' do
|
||||
expect(model).not_to receive(:remove_foreign_key)
|
||||
|
||||
model.remove_foreign_key_if_exists(:projects, :users, column: :user_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the foreign key exists' do
|
||||
before do
|
||||
allow(model).to receive(:foreign_key_exists?).and_return(true)
|
||||
end
|
||||
|
||||
it 'removes the foreign key' do
|
||||
expect(model).to receive(:remove_foreign_key).with(:projects, :users, { column: :user_id })
|
||||
|
||||
model.remove_foreign_key_if_exists(:projects, :users, column: :user_id)
|
||||
end
|
||||
|
||||
context 'when the target table is not given' do
|
||||
it 'passes the options as the second parameter' do
|
||||
expect(model).to receive(:remove_foreign_key).with(:projects, { column: :user_id })
|
||||
|
||||
model.remove_foreign_key_if_exists(:projects, column: :user_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the reverse_lock_order option is given' do
|
||||
it 'requests for lock before removing the foreign key' do
|
||||
expect(model).to receive(:transaction_open?).and_return(true)
|
||||
expect(model).to receive(:execute).with(/LOCK TABLE users, projects/)
|
||||
expect(model).not_to receive(:remove_foreign_key).with(:projects, :users)
|
||||
|
||||
model.remove_foreign_key_if_exists(:projects, :users, column: :user_id, reverse_lock_order: true)
|
||||
end
|
||||
|
||||
context 'when not inside a transaction' do
|
||||
it 'does not lock' do
|
||||
expect(model).to receive(:transaction_open?).and_return(false)
|
||||
expect(model).not_to receive(:execute).with(/LOCK TABLE users, projects/)
|
||||
expect(model).to receive(:remove_foreign_key).with(:projects, :users, { column: :user_id })
|
||||
|
||||
model.remove_foreign_key_if_exists(:projects, :users, column: :user_id, reverse_lock_order: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add_concurrent_foreign_key' do
|
||||
before do
|
||||
allow(model).to receive(:foreign_key_exists?).and_return(false)
|
||||
|
|
|
@ -13,7 +13,6 @@ RSpec.describe 'cross-database foreign keys' do
|
|||
%w(
|
||||
ci_build_report_results.project_id
|
||||
ci_builds.project_id
|
||||
ci_builds_metadata.project_id
|
||||
ci_daily_build_group_report_results.group_id
|
||||
ci_daily_build_group_report_results.project_id
|
||||
ci_freeze_periods.project_id
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::RequestProfiler::Profile do
|
||||
let(:profile) { described_class.new(filename) }
|
||||
|
||||
describe '.new' do
|
||||
context 'using old filename' do
|
||||
let(:filename) { '|api|v4|version.txt_1562854738.html' }
|
||||
|
||||
it 'returns valid data' do
|
||||
expect(profile).to be_valid
|
||||
expect(profile.request_path).to eq('/api/v4/version.txt')
|
||||
expect(profile.time).to eq(Time.at(1562854738).utc)
|
||||
expect(profile.type).to eq('html')
|
||||
end
|
||||
end
|
||||
|
||||
context 'using new filename' do
|
||||
let(:filename) { '|api|v4|version.txt_1563547949_execution.html' }
|
||||
|
||||
it 'returns valid data' do
|
||||
expect(profile).to be_valid
|
||||
expect(profile.request_path).to eq('/api/v4/version.txt')
|
||||
expect(profile.profile_mode).to eq('execution')
|
||||
expect(profile.time).to eq(Time.at(1563547949).utc)
|
||||
expect(profile.type).to eq('html')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content_type' do
|
||||
context 'when using html file' do
|
||||
let(:filename) { '|api|v4|version.txt_1562854738_memory.html' }
|
||||
|
||||
it 'returns valid data' do
|
||||
expect(profile).to be_valid
|
||||
expect(profile.content_type).to eq('text/html')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using text file' do
|
||||
let(:filename) { '|api|v4|version.txt_1562854738_memory.txt' }
|
||||
|
||||
it 'returns valid data' do
|
||||
expect(profile).to be_valid
|
||||
expect(profile.content_type).to eq('text/plain')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is unknown' do
|
||||
let(:filename) { '|api|v4|version.txt_1562854738_memory.xxx' }
|
||||
|
||||
it 'returns valid data' do
|
||||
expect(profile).not_to be_valid
|
||||
expect(profile.content_type).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,56 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::RequestProfiler do
|
||||
describe '.profile_token' do
|
||||
it 'returns a token' do
|
||||
expect(described_class.profile_token).to be_present
|
||||
end
|
||||
|
||||
it 'caches the token' do
|
||||
expect(Rails.cache).to receive(:fetch).with('profile-token')
|
||||
|
||||
described_class.profile_token
|
||||
end
|
||||
end
|
||||
|
||||
context 'with temporary PROFILES_DIR' do
|
||||
let(:tmpdir) { Dir.mktmpdir('profiler-test') }
|
||||
let(:profile_name) { '|api|v4|version.txt_1562854738_memory.html' }
|
||||
let(:profile_path) { File.join(tmpdir, profile_name) }
|
||||
|
||||
before do
|
||||
stub_const('Gitlab::RequestProfiler::PROFILES_DIR', tmpdir)
|
||||
FileUtils.touch(profile_path)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(tmpdir)
|
||||
end
|
||||
|
||||
describe '.remove_all_profiles' do
|
||||
it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do
|
||||
described_class.remove_all_profiles
|
||||
|
||||
expect(Dir.exist?(tmpdir)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '.all' do
|
||||
subject { described_class.all }
|
||||
|
||||
it 'returns all profiles' do
|
||||
expect(subject.map(&:name)).to contain_exactly(profile_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find' do
|
||||
subject { described_class.find(profile_name) }
|
||||
|
||||
it 'returns all profiles' do
|
||||
expect(subject.name).to eq(profile_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -133,4 +133,11 @@ RSpec.describe Ci::BuildMetadata do
|
|||
expect(build.cancel_gracefully?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'loose foreign key on ci_builds_metadata.project_id' do
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:parent) { create(:project) }
|
||||
let!(:model) { create(:ci_build_metadata, project: parent) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::UserPreferences::Update do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:sort_value) { 'TITLE_ASC' }
|
||||
|
||||
let(:input) do
|
||||
{
|
||||
'issuesSort' => sort_value
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:userPreferencesUpdate, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(:userPreferencesUpdate) }
|
||||
|
||||
context 'when user has no existing preference' do
|
||||
it 'creates the user preference record' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
|
||||
|
||||
expect(current_user.user_preference.persisted?).to eq(true)
|
||||
expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has existing preference' do
|
||||
before do
|
||||
current_user.create_user_preference!(issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value)
|
||||
end
|
||||
|
||||
it 'updates the existing value' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
current_user.user_preference.reload
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
|
||||
|
||||
expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,11 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
let_it_be(:project_hook) { create(:project_hook, project: project, merge_requests_events: true) }
|
||||
let_it_be(:system_hook) { create(:system_hook, merge_requests_events: true) }
|
||||
|
||||
let(:stubbed_project_hook_hostname) { stubbed_hostname(project_hook.url, hostname: stubbed_project_hook_ip_address) }
|
||||
let(:stubbed_system_hook_hostname) { stubbed_hostname(system_hook.url, hostname: stubbed_system_hook_ip_address) }
|
||||
let(:stubbed_project_hook_ip_address) { '8.8.8.8' }
|
||||
let(:stubbed_system_hook_ip_address) { '8.8.8.9' }
|
||||
|
||||
# Trigger a change to the merge request to fire the webhooks.
|
||||
def trigger_web_hooks
|
||||
params = { merge_request: { description: FFaker::Lorem.sentence } }
|
||||
|
@ -18,8 +23,8 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
end
|
||||
|
||||
def stub_requests
|
||||
stub_full_request(project_hook.url, method: :post, ip_address: '8.8.8.8')
|
||||
stub_full_request(system_hook.url, method: :post, ip_address: '8.8.8.9')
|
||||
stub_full_request(project_hook.url, method: :post, ip_address: stubbed_project_hook_ip_address)
|
||||
stub_full_request(system_hook.url, method: :post, ip_address: stubbed_system_hook_ip_address)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -37,10 +42,10 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
|
||||
trigger_web_hooks
|
||||
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname)
|
||||
.with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
|
||||
.once
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
|
||||
expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname)
|
||||
.with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
|
||||
.once
|
||||
end
|
||||
|
@ -54,24 +59,24 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
|
||||
end
|
||||
|
||||
it 'executes all webhooks and logs an error for the recursive hook', :aggregate_failures do
|
||||
it 'blocks and logs an error for the recursive webhook, but execute the non-recursive webhook', :aggregate_failures do
|
||||
stub_requests
|
||||
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
recursion_detection: {
|
||||
uuid: uuid,
|
||||
ids: [project_hook.id]
|
||||
}
|
||||
)
|
||||
).twice # Twice: once in `#async_execute`, and again in `#execute`.
|
||||
).once
|
||||
|
||||
trigger_web_hooks
|
||||
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
|
||||
expect(WebMock).not_to have_requested(:post, stubbed_project_hook_hostname)
|
||||
expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname).once
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -87,35 +92,35 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
|
||||
end
|
||||
|
||||
it 'executes and logs errors for all hooks', :aggregate_failures do
|
||||
it 'blocks and logs errors for all hooks', :aggregate_failures do
|
||||
stub_requests
|
||||
previous_hook_ids = previous_hooks.map(&:id)
|
||||
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
recursion_detection: {
|
||||
uuid: uuid,
|
||||
ids: include(*previous_hook_ids)
|
||||
}
|
||||
)
|
||||
).twice
|
||||
).once
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: system_hook.id,
|
||||
recursion_detection: {
|
||||
uuid: uuid,
|
||||
ids: include(*previous_hook_ids)
|
||||
}
|
||||
)
|
||||
).twice
|
||||
).once
|
||||
|
||||
trigger_web_hooks
|
||||
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
|
||||
expect(WebMock).not_to have_requested(:post, stubbed_project_hook_hostname)
|
||||
expect(WebMock).not_to have_requested(:post, stubbed_system_hook_hostname)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -156,10 +161,10 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
|
||||
expect(uuid_headers).to all(be_present)
|
||||
expect(uuid_headers.uniq.length).to eq(2)
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname)
|
||||
.with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
|
||||
.once
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
|
||||
expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname)
|
||||
.with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
|
||||
.once
|
||||
end
|
||||
|
@ -175,8 +180,8 @@ RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_red
|
|||
expect(uuid_headers).to all(be_present)
|
||||
expect(uuid_headers.length).to eq(4)
|
||||
expect(uuid_headers.uniq.length).to eq(4)
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).twice
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).twice
|
||||
expect(WebMock).to have_requested(:post, stubbed_project_hook_hostname).twice
|
||||
expect(WebMock).to have_requested(:post, stubbed_system_hook_hostname).twice
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Request Profiler' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
shared_examples 'profiling a request' do |profile_type, extension|
|
||||
before do
|
||||
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
|
||||
allow(RubyProf::Profile).to receive(:profile) do |&blk|
|
||||
blk.call
|
||||
RubyProf::Profile.new
|
||||
end
|
||||
allow(MemoryProfiler).to receive(:report) do |&blk|
|
||||
blk.call
|
||||
MemoryProfiler.start
|
||||
MemoryProfiler.stop
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a profile of the request' do
|
||||
project = create(:project, namespace: user.namespace)
|
||||
time = Time.now
|
||||
path = "/#{project.full_path}"
|
||||
|
||||
travel_to(time) do
|
||||
get path, params: {}, headers: { 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token, 'X-Profile-Mode' => profile_type }
|
||||
end
|
||||
|
||||
profile_type = 'execution' if profile_type.nil?
|
||||
profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}_#{profile_type}.#{extension}"
|
||||
expect(File.exist?(profile_path)).to be true
|
||||
end
|
||||
|
||||
after do
|
||||
Gitlab::RequestProfiler.remove_all_profiles
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is logged-in" do
|
||||
before do
|
||||
login_as(user)
|
||||
end
|
||||
|
||||
include_examples 'profiling a request', 'execution', 'html'
|
||||
include_examples 'profiling a request', nil, 'html'
|
||||
include_examples 'profiling a request', 'memory', 'txt'
|
||||
end
|
||||
|
||||
context "when user is not logged-in" do
|
||||
include_examples 'profiling a request', 'execution', 'html'
|
||||
include_examples 'profiling a request', nil, 'html'
|
||||
include_examples 'profiling a request', 'memory', 'txt'
|
||||
end
|
||||
end
|
|
@ -149,13 +149,13 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
.once
|
||||
end
|
||||
|
||||
it 'executes and logs if a recursive web hook is detected', :aggregate_failures do
|
||||
it 'blocks and logs if a recursive web hook is detected', :aggregate_failures do
|
||||
stub_full_request(project_hook.url, method: :post)
|
||||
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
|
||||
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
hook_type: 'ProjectHook',
|
||||
hook_name: 'push_hooks',
|
||||
|
@ -166,12 +166,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
|
||||
service_instance.execute
|
||||
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
.with(headers: headers)
|
||||
.once
|
||||
expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
end
|
||||
|
||||
it 'executes and logs if the recursion count limit would be exceeded', :aggregate_failures do
|
||||
it 'blocks and logs if the recursion count limit would be exceeded', :aggregate_failures do
|
||||
stub_full_request(project_hook.url, method: :post)
|
||||
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
|
||||
previous_hooks = create_list(:project_hook, 3)
|
||||
|
@ -179,7 +177,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
hook_type: 'ProjectHook',
|
||||
hook_name: 'push_hooks',
|
||||
|
@ -190,9 +188,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
|
||||
service_instance.execute
|
||||
|
||||
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
.with(headers: headers)
|
||||
.once
|
||||
expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
|
||||
end
|
||||
|
||||
it 'handles exceptions' do
|
||||
|
@ -496,15 +492,15 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
Gitlab::WebHooks::RecursionDetection.set_request_uuid(SecureRandom.uuid)
|
||||
end
|
||||
|
||||
it 'queues a worker and logs an error if the call chain limit would be exceeded' do
|
||||
it 'does not queue a worker and logs an error if the call chain limit would be exceeded' do
|
||||
stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
|
||||
previous_hooks = create_list(:project_hook, 3)
|
||||
previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
|
||||
|
||||
expect(WebHookWorker).to receive(:perform_async)
|
||||
expect(WebHookWorker).not_to receive(:perform_async)
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
hook_type: 'ProjectHook',
|
||||
hook_name: 'push_hooks',
|
||||
|
@ -519,13 +515,13 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
|
|||
service_instance.async_execute
|
||||
end
|
||||
|
||||
it 'queues a worker and logs an error if a recursive call chain is detected' do
|
||||
it 'does not queue a worker and logs an error if a recursive call chain is detected' do
|
||||
Gitlab::WebHooks::RecursionDetection.register!(project_hook)
|
||||
|
||||
expect(WebHookWorker).to receive(:perform_async)
|
||||
expect(WebHookWorker).not_to receive(:perform_async)
|
||||
expect(Gitlab::AuthLogger).to receive(:error).with(
|
||||
include(
|
||||
message: 'Webhook recursion detected and will be blocked in future',
|
||||
message: 'Recursive webhook blocked from executing',
|
||||
hook_id: project_hook.id,
|
||||
hook_type: 'ProjectHook',
|
||||
hook_name: 'push_hooks',
|
||||
|
|
Loading…
Reference in a new issue