Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-26 12:18:17 +00:00
parent 30b1000678
commit 613a8bc141
63 changed files with 374 additions and 874 deletions

View file

@ -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'

View file

@ -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

View file

@ -1 +1 @@
7a8fd30510a92d436a5144c3c20f6654ff3ec51d
66a4a9452e0ee27a29dd36fffe98ea04dab8ae24

View file

@ -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'

View file

@ -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)

View file

@ -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"

View file

@ -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')"

View file

@ -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

View file

@ -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')"

View file

@ -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;

View file

@ -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"
>

View file

@ -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

View file

@ -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)

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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,

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -0,0 +1 @@
5b5f47f1d7038518fef71dd8c0758b234bb890be9aab57b78918f7b2dc39bdc4

View file

@ -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;

View file

@ -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,

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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**.

View file

@ -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/

View file

@ -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

View file

@ -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

View file

@ -71,12 +71,8 @@ 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
end
def add_counter_to_list(project, operation, key)
CACHING.set_add(counter_list_key(project, operation), key)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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,
}),
);

View file

@ -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}

View file

@ -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', () => {

View file

@ -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} | ${''}

View 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

View file

@ -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'"
]

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',