Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-04-09 21:09:19 +00:00
parent 141902c049
commit 254ec28f54
47 changed files with 915 additions and 548 deletions

View file

@ -112,7 +112,6 @@ export default {
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
mergeRequestDiff: state => state.diffs.mergeRequestDiff,
commit: state => state.diffs.commit,
targetBranchName: state => state.diffs.targetBranchName,
renderOverflowWarning: state => state.diffs.renderOverflowWarning,
numTotalFiles: state => state.diffs.realSize,
numVisibleFiles: state => state.diffs.size,
@ -123,19 +122,9 @@ export default {
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
versionIndex: -1,
path: '',
};
},
canCurrentUserFork() {
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
},
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
renderDiffFiles() {
return (
this.diffFiles.length > 0 ||
@ -369,8 +358,6 @@ export default {
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch"
:is-limited-container="isLimitedContainer"
:diff-files-length="diffFilesLength"
/>

View file

@ -0,0 +1,78 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
Icon,
TimeAgo,
},
props: {
versions: {
type: Array,
required: true,
},
},
computed: {
selectedVersionName() {
return this.versions.find(x => x.selected)?.versionName || '';
},
},
};
</script>
<template>
<span class="dropdown inline">
<a
class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
<span> {{ selectedVersionName }} </span>
<icon :size="12" name="angle-down" class="position-absolute" />
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
<ul>
<li v-for="version in versions" :key="version.id">
<a :class="{ 'is-active': version.selected }" :href="version.href">
<div>
<strong>
{{ version.versionName }}
<template v-if="version.isHead">{{
s__('DiffsCompareBaseBranch|(HEAD)')
}}</template>
<template v-else-if="version.isBase">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
</div>
<div>
<small class="commit-sha"> {{ version.short_commit_sha }} </small>
</div>
<div>
<small>
<template v-if="version.commitsText">
{{ version.commitsText }}
</template>
<time-ago
v-if="version.created_at"
:time="version.created_at"
class="js-timeago"
/>
</small>
</div>
</a>
</li>
</ul>
</div>
</div>
</span>
</template>
<style>
.dropdown {
min-width: 0;
max-height: 170px;
}
</style>

View file

@ -4,14 +4,14 @@ import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitl
import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
CompareVersionsDropdown,
CompareDropdownLayout,
Icon,
GlLink,
GlDeprecatedButton,
@ -27,16 +27,6 @@ export default {
type: Array,
required: true,
},
mergeRequestDiff: {
type: Object,
required: false,
default: () => ({}),
},
targetBranch: {
type: Object,
required: false,
default: null,
},
isLimitedContainer: {
type: Boolean,
required: false,
@ -48,7 +38,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['hasCollapsedFile']),
...mapGetters('diffs', [
'hasCollapsedFile',
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapState('diffs', [
'commit',
'showTreeList',
@ -57,18 +51,12 @@ export default {
'addedLines',
'removedLines',
]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
toggleFileBrowserTitle() {
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
},
baseVersionPath() {
return this.mergeRequestDiff.base_version_path;
},
},
created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
@ -113,19 +101,14 @@ export default {
:message="s__('MergeRequest|Compare %{source} and %{target}')"
>
<template #source>
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
<compare-dropdown-layout
:versions="diffCompareDropdownSourceVersions"
class="mr-version-dropdown"
/>
</template>
<template #target>
<compare-versions-dropdown
:other-versions="comparableDiffs"
:base-version-path="baseVersionPath"
:start-version="startVersion"
:target-branch="targetBranch"
<compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown"
/>
</template>

View file

@ -1,162 +0,0 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
Icon,
TimeAgo,
},
props: {
otherVersions: {
type: Array,
required: false,
default: () => [],
},
mergeRequestVersion: {
type: Object,
required: false,
default: null,
},
startVersion: {
type: Object,
required: false,
default: null,
},
targetBranch: {
type: Object,
required: false,
default: null,
},
showCommitCount: {
type: Boolean,
required: false,
default: false,
},
baseVersionPath: {
type: String,
required: false,
default: null,
},
},
computed: {
targetVersions() {
if (this.mergeRequestVersion) {
return this.otherVersions;
}
return [...this.otherVersions, this.targetBranch];
},
selectedVersionName() {
const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
return this.versionName(selectedVersion);
},
},
methods: {
commitsText(version) {
return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
return this.baseVersionPath;
}
if (this.showCommitCount) {
return version.version_path;
}
return version.compare_path;
},
versionName(version) {
if (this.isLatest(version)) {
return __('latest version');
}
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
return false;
}
if (this.targetBranch) {
return (
(this.isBase(version) && !this.startVersion) ||
(this.startVersion && this.startVersion.version_index === version.version_index)
);
}
return version.version_index === this.mergeRequestVersion.version_index;
},
isBase(version) {
if (!version || !this.targetBranch) {
return false;
}
return version.versionIndex === -1;
},
isHead() {
return parseBoolean(getParameterByName('diff_head'));
},
isLatest(version) {
return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
);
},
},
};
</script>
<template>
<span class="dropdown inline">
<a
class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
<span> {{ selectedVersionName }} </span>
<icon :size="12" name="angle-down" class="position-absolute" />
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
<ul>
<li v-for="version in targetVersions" :key="version.id">
<a :class="{ 'is-active': isActive(version) }" :href="href(version)">
<div>
<strong>
{{ versionName(version) }}
<template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
<template v-else-if="isBase(version)">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
</div>
<div>
<small class="commit-sha"> {{ version.short_commit_sha }} </small>
</div>
<div>
<small>
<template v-if="showCommitCount">
{{ commitsText(version) }}
</template>
<time-ago
v-if="version.created_at"
:time="version.created_at"
class="js-timeago"
/>
</small>
</div>
</a>
</li>
</ul>
</div>
</div>
</span>
</template>
<style>
.dropdown {
min-width: 0;
max-height: 170px;
}
</style>

View file

@ -58,3 +58,5 @@ export const START_RENDERING_INDEX = 200;
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;

View file

@ -1,6 +1,8 @@
import { __, n__ } from '~/locale';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;

View file

@ -0,0 +1,43 @@
import { __, n__, sprintf } from '~/locale';
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '../constants';
export const selectedTargetIndex = state =>
state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX;
export const selectedSourceIndex = state => state.mergeRequestDiff.version_index;
export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
const baseVersion = {
versionName: state.targetBranchName,
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
href: state.mergeRequestDiff.base_version_path,
isBase: true,
selected: !state.startVersion,
};
// Appended properties here are to make the compare_dropdown_layout easier to reason about
const formatVersion = v => {
return {
href: v.compare_path,
versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedTargetIndex,
...v,
};
};
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
// Appended properties here are to make the compare_dropdown_layout easier to reason about
return state.mergeRequestDiffs.map((v, i) => ({
...v,
href: v.version_path,
commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count),
versionName:
i === 0
? __('latest version')
: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedSourceIndex,
}));
};

View file

@ -15,7 +15,7 @@ export default () => ({
endpoint: '',
basePath: '',
commit: null,
startVersion: null,
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diffFiles: [],
coverageFiles: {},
mergeRequestDiffs: [],

View file

@ -1,10 +1,12 @@
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default {
components: {
timeAgoTooltip,
GitlabTeamMemberBadge,
},
props: {
author: {
@ -48,6 +50,9 @@ export default {
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
showGitlabTeamMemberBadge() {
return this.author?.is_gitlab_employee;
},
},
methods: {
...mapActions(['setTargetNoteHash']),
@ -73,8 +78,8 @@ export default {
{{ __('Toggle thread') }}
</button>
</div>
<template v-if="hasAuthor">
<a
v-if="hasAuthor"
v-once
:href="author.path"
class="js-user-link"
@ -86,6 +91,8 @@ export default {
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
</a>
<gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>

View file

@ -1,16 +1,17 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PrometheusMetrics from 'ee_else_ce/prometheus_metrics/prometheus_metrics';
import PrometheusAlerts from '~/prometheus_alerts';
import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
prometheusMetrics.init();
}
PrometheusAlerts();

View file

@ -28,6 +28,10 @@ export default class PrometheusMetrics {
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
init() {
this.loadActiveMetrics();
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);

View file

@ -0,0 +1,27 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
export default {
name: 'GitlabTeamMemberBadge',
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
};
</script>
<template>
<span
v-gl-tooltip.hover
:title="$options.gitlabTeamMemberLabel"
role="img"
:aria-label="$options.gitlabTeamMemberLabel"
class="d-inline-block align-middle"
>
<gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
</span>
</template>

View file

@ -180,7 +180,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: TestReportSerializer
.new(current_user: @current_user)
.represent(test_reports)
.represent(test_reports, project: project)
end
end
end

View file

@ -3,11 +3,6 @@
class SearchService
include Gitlab::Allowable
REDACTABLE_RESULTS = [
ActiveRecord::Relation,
Gitlab::Search::FoundBlob
].freeze
SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096
@ -68,10 +63,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
end
def redactable_results
REDACTABLE_RESULTS
end
private
def visible_result?(object)
@ -80,12 +71,9 @@ class SearchService
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
end
def redact_unauthorized_results(results)
return results unless redactable_results.any? { |redactable| results.is_a?(redactable) }
permitted_results = results.select do |object|
visible_result?(object)
end
def redact_unauthorized_results(results_collection)
results = results_collection.to_a
permitted_results = results.select { |object| visible_result?(object) }
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
@ -93,13 +81,13 @@ class SearchService
log_redacted_search_results(filtered_results.values) if filtered_results.any?
return results.id_not_in(filtered_results.keys) if results.is_a?(ActiveRecord::Relation)
return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
Kaminari.paginate_array(
permitted_results,
total_count: results.total_count,
limit: results.limit_value,
offset: results.offset_value
total_count: results_collection.total_count,
limit: results_collection.limit_value,
offset: results_collection.offset_value
)
end

View file

@ -16,7 +16,7 @@
.avatar-container.rect-avatar.s40
= link_to group do
= group_icon(group, class: "avatar s40 d-none d-sm-block")
= group_icon(group, class: "avatar s40")
.title
= link_to group.full_name, group, class: 'group-name'

View file

@ -0,0 +1,5 @@
---
title: Add Nginx error percentage metric
merge_request: 28983
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Make search redaction more robust
merge_request: 29166
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix missing group icons on profile page when screen < 576px
merge_request: 28973
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add index for created_at of resource_milestone_events
merge_request: 28929
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Add Praefect rake task to print out replica checksums
merge_request: 28369
author:
type: added

View file

@ -218,3 +218,13 @@ panel_groups:
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- title: "HTTP Error Rate"
type: "area-chart"
y_label: "HTTP Errors (%)"
weight: 1
metrics:
- id: response_metrics_nginx_http_error_percentage
query_range: 'sum(rate(nginx_server_requests{code=~"5.*", host="*", %{environment_filter}}[2m])) / sum(rate(nginx_server_requests{code="total", host="*", %{environment_filter}}[2m])) * 100'
label: 5xx Errors (%)
unit: "%"

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddIndexToCreatedAtOnResourceMilestoneEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_resource_milestone_events_created_at'
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :resource_milestone_events, :created_at, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :resource_milestone_events, INDEX_NAME
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddNginxFiveHundredPercentageMetric < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
# no-op
end
end

View file

@ -10036,6 +10036,8 @@ CREATE INDEX index_resource_label_events_on_merge_request_id ON public.resource_
CREATE INDEX index_resource_label_events_on_user_id ON public.resource_label_events USING btree (user_id);
CREATE INDEX index_resource_milestone_events_created_at ON public.resource_milestone_events USING btree (created_at);
CREATE INDEX index_resource_milestone_events_on_issue_id ON public.resource_milestone_events USING btree (issue_id);
CREATE INDEX index_resource_milestone_events_on_merge_request_id ON public.resource_milestone_events USING btree (merge_request_id);
@ -13070,7 +13072,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200403184110
20200403185127
20200403185422
20200406135648
20200407094005
20200407094923
20200408110856
\.

View file

@ -60,12 +60,18 @@ GitLab](https://about.gitlab.com/install/).
- 1 Praefect node (minimal storage required)
- 3 Gitaly nodes (high CPU, high memory, fast storage)
- 1 GitLab server
You will need the IP/host address for each node.
1. `POSTGRESQL_SERVER_ADDRESS`: the IP/host address of the PostgreSQL server
1. `PRAEFECT_SERVER_ADDRESS`: the IP/host address of the Praefect server
1. `GITALY_SERVER_ADDRESS`: the IP/host address of each Gitaly node
1. `PRAEFECT_HOST`: the IP/host address of the Praefect server
1. `GITALY_HOST`: the IP/host address of each Gitaly server
1. `GITLAB_HOST`: the IP/host address of the GitLab server
If you are using a cloud provider, you can look up the addresses for each server through your cloud provider's management console.
If you are using Google Cloud Platform, SoftLayer, or any other vendor that provides a virtual private cloud (VPC) you can use the private addresses for each cloud instance (corresponds to “internal address” for Google Cloud Platform) for `PRAEFECT_HOST`, `GITALY_HOST`, and `GITLAB_HOST`.
#### Secrets
@ -183,14 +189,18 @@ application server, or a Gitaly node.
1. Configure **Praefect** to listen on network interfaces by editing
`/etc/gitlab/gitlab.rb`:
You will need to replace:
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
```ruby
# Make Praefect accept connections on all network interfaces.
# Use firewalls to restrict access to this address/port.
praefect['listen_addr'] = '0.0.0.0:2305'
praefect['listen_addr'] = 'PRAEFECT_HOST:2305'
# Enable Prometheus metrics access to Praefect. You must use firewalls
# to restrict access to this address/port.
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
praefect['prometheus_listen_addr'] = 'PRAEFECT_HOST:9652'
```
1. Configure a strong `auth_token` for **Praefect** by editing
@ -357,14 +367,18 @@ documentation](index.md#3-gitaly-server-configuration).
1. Configure **Gitaly** to listen on network interfaces by editing
`/etc/gitlab/gitlab.rb`:
You will need to replace:
- `GITALY_HOST` with the IP address or hostname of the Gitaly node
```ruby
# Make Gitaly accept connections on all network interfaces.
# Use firewalls to restrict access to this address/port.
gitaly['listen_addr'] = '0.0.0.0:8075'
gitaly['listen_addr'] = 'GITALY_HOST:8075'
# Enable Prometheus metrics access to Gitaly. You must use firewalls
# to restrict access to this address/port.
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
gitaly['prometheus_listen_addr'] = 'GITALY_HOST:9236'
```
1. Configure a strong `auth_token` for **Gitaly** by editing
@ -387,7 +401,7 @@ documentation](index.md#3-gitaly-server-configuration).
# Configure the gitlab-shell API callback URL. Without this, `git push` will
# fail. This can be your front door GitLab URL or an internal load balancer.
# Examples: 'https://example.gitlab.com', 'http://1.2.3.4'
gitlab_rails['internal_api_url'] = 'GITLAB_SERVER_URL'
gitlab_rails['internal_api_url'] = 'http://GITLAB_HOST'
```
1. Configure the storage location for Git data by setting `git_data_dirs` in
@ -499,12 +513,13 @@ Particular attention should be shown to:
You will need to replace:
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
- `GITLAB_HOST` with the IP address or hostname of the GitLab server
- `PRAEFECT_EXTERNAL_TOKEN` with the real secret
```ruby
git_data_dirs({
"default" => {
"path" => "/var/opt/gitlab/git-data"
"gitaly_address" => "tcp://GITLAB_HOST:8075"
},
"praefect" => {
"gitaly_address" => "tcp://PRAEFECT_HOST:2305",
@ -513,6 +528,13 @@ Particular attention should be shown to:
})
```
1. Allow Gitaly to listen on a tcp port by editing
`/etc/gitlab/gitlab.rb`
```ruby
gitaly['listen_addr'] = 'tcp://GITLAB_HOST:8075'
```
1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
nodes during a `git push` are properly authenticated by editing
`/etc/gitlab/gitlab.rb`:
@ -526,7 +548,7 @@ Particular attention should be shown to:
1. Configure the `external_url` so that files could be served by GitLab
by proper endpoint access by editing `/etc/gitlab/gitlab.rb`:
You will need to replace `GITLAB_SERVER_URL` with the real URL on which
You will need to replace `GITLAB_SERVER_URL` with the real external facing URL on which
current GitLab instance is serving:
```ruby

View file

@ -635,6 +635,43 @@ Each line contains a JSON line that can be ingested by Elasticsearch. For exampl
}
```
## `geo.log`
> Introduced in 9.5.
Geo stores structured log messages in a `geo.log` file. For Omnibus installations, this file is at `/var/log/gitlab/gitlab-rails/geo.log`.
This file contains information about when Geo attempts to sync repositories and files. Each line in the file contains a separate JSON entry that can be ingested into. For example, Elasticsearch or Splunk.
For example:
```json
{"severity":"INFO","time":"2017-08-06T05:40:16.104Z","message":"Repository update","project_id":1,"source":"repository","resync_repository":true,"resync_wiki":true,"class":"Gitlab::Geo::LogCursor::Daemon","cursor_delay_s":0.038}
```
This message shows that Geo detected that a repository update was needed for project `1`.
## Registry Logs
For Omnibus installations, Container Registry logs reside in `/var/log/gitlab/registry/current`.
## NGINX Logs
For Omnibus installations, NGINX logs reside in:
- `/var/log/gitlab/nginx/gitlab_access.log` contains a log of requests made to GitLab.
- `/var/log/gitlab/nginx/gitlab_error.log` contains a log of NGINX errors for GitLab.
- `/var/log/gitlab/nginx/gitlab_pages_access.log` contains a log of requests made to Pages static sites.
- `/var/log/gitlab/nginx/gitlab_pages_error.log` contains a log of NGINX errors for Pages static sites.
- `/var/log/gitlab/nginx/gitlab_registry_access.log` contains a log of requests made to the Container Registry.
- `/var/log/gitlab/nginx/gitlab_registry_error.log` contains a log of NGINX errors for the Container Regsitry.
Below is the default GitLab NGINX access log format:
```plaintext
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
```
[repocheck]: repository_checks.md
[Rack Attack]: ../security/rack_attack.md
[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md

View file

@ -81,6 +81,7 @@ Example response:
"title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
"description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
"state": "opened",
"confidential": "false",
"web_url": "http://localhost:3001/groups/test/-/epics/4",
"reference": "&4",
"references": {
@ -240,6 +241,7 @@ POST /groups/:id/epics
| `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma separated list of labels |
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
@ -260,6 +262,7 @@ Example response:
"title": "Epic",
"description": "Epic description",
"state": "opened",
"confidential": "false",
"web_url": "http://localhost:3001/groups/test/-/epics/6",
"reference": "&6",
"references": {
@ -314,6 +317,7 @@ PUT /groups/:id/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic |
| `title` | string | no | The title of an epic |
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
| `labels` | string | no | The comma separated list of labels |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
@ -335,6 +339,7 @@ Example response:
"title": "New Title",
"description": "Epic description",
"state": "opened",
"confidential": "false",
"web_url": "http://localhost:3001/groups/test/-/epics/6",
"reference": "&6",
"references": {

View file

@ -149,6 +149,18 @@ otherwise.
If you want a running pipeline to finish even if you push new commits to a merge
request, be sure to start the `dont-interrupt-me` job before pushing.
## PostgreSQL versions testing
We follow [the PostgreSQL versions Omnibus support policy](https://gitlab.com/groups/gitlab-org/-/epics/2184#proposal):
| | 12.10 (April 2020) | 13.0 (May 2020) | 13.1 (June 2020) | 13.2 (July 2020) | 13.3 (August 2020) | 13.4, 13.5 | 13.6 (November 2020) | 14.0 (May 2021?) |
| ------ | ------------------ | --------------- | ---------------- | ---------------- | ------------------ | ------------ | -------------------- | -------------------- |
| PG9.6 | nightly | - | - | - | - | - | - | - |
| PG10 | `master` | - | - | - | - | - | - | - |
| PG11 | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | nightly | - |
| PG12 | - | - | - | - | `master` | `master` | MRs/`master` | `master` |
| PG13 | - | - | - | - | - | - | - | MRs/`master` |
## Directed acyclic graph
We're using the [`needs:`](../ci/yaml/README.md#needs) keyword to

View file

@ -303,10 +303,10 @@ it highlighted:
"version": "2.3",
"vulnerabilities": [
{
"id": "ac0997ad-1006-4c81-81fb-ee2bbe6e78e3",
"category": "container_scanning",
"message": "CVE-2019-3462 in apt",
"description": "Incorrect sanitation of the 302 redirect field in HTTP transport method of apt versions 1.4.8 and earlier can lead to content injection by a MITM attacker, potentially leading to remote code execution on the target machine.",
"cve": "debian:9:apt:CVE-2019-3462",
"severity": "High",
"confidence": "Unknown",
"solution": "Upgrade apt from 1.4.8 to 1.4.9",
@ -343,7 +343,7 @@ it highlighted:
{
"fixes": [
{
"cve": "debian:9:apt:CVE-2019-3462"
"id": "c0997ad-1006-4c81-81fb-ee2bbe6e78e3"
}
],
"summary": "Upgrade apt from 1.4.8 to 1.4.9",
@ -363,10 +363,11 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
| `vulnerabilities[].category` | Where this vulnerability belongs (for example, SAST or Container Scanning). For Container Scanning, it will always be `container_scanning`. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
@ -390,7 +391,8 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
| `remediations[].fixes[].cve` | A string value that describes a fixed vulnerability occurrence in the same format as `vulnerabilities[].cve`. |
| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
@ -414,7 +416,8 @@ Some vulnerabilities can be fixed by applying the solution that GitLab
automatically generates.
To enable remediation support, the scanning tool _must_ have access to the `Dockerfile` specified by
the `DOCKERFILE_PATH` environment variable. To ensure that the scanning tool has access to this
the [`DOCKERFILE_PATH`](#available-variables) environment variable. To ensure that the scanning tool
has access to this
file, it's necessary to set [`GIT_STRATEGY: fetch`](../../../ci/yaml/README.md#git-strategy) in
your `.gitlab-ci.yml` file by following the instructions described in this document's
[overriding the Container Scanning template](#overriding-the-container-scanning-template) section.

View file

@ -83,8 +83,11 @@ That's needed when one totally relies on [custom analyzers](#custom-analyzers).
## Custom analyzers
You can provide your own analyzers as a comma separated list of Docker images.
Here's how to add `analyzers/nugget` and `analyzers/perl` to the default images.
### Custom analyzers with Docker-in-Docker
When Docker-in-Docker for Dependency Scanning is enabled,
you can provide your own analyzers as a comma-separated list of Docker images.
Here's how to add `analyzers/nuget` and `analyzers/perl` to the default images.
In `.gitlab-ci.yml` define:
```yaml
@ -92,7 +95,7 @@ include:
template: Dependency-Scanning.gitlab-ci.yml
variables:
DS_ANALYZER_IMAGES: "my-docker-registry/analyzers/nugget,amy-docker-registry/nalyzers/perl"
DS_ANALYZER_IMAGES: "my-docker-registry/analyzers/nuget,amy-docker-registry/analyzers/perl"
```
The values must be the full path to the container registry images,
@ -103,6 +106,28 @@ This configuration doesn't benefit from the integrated detection step. Dependenc
Scanning has to fetch and spawn each Docker image to establish whether the
custom analyzer can scan the source code.
### Custom analyzers without Docker-in-Docker
When Docker-in-Docker for Dependency Scanning is disabled, you can provide your own analyzers by
defining CI jobs in your CI configuration. For consistency, you should suffix your custom Dependency
Scanning jobs with `-dependency_scanning`. Here's how to add a scanning job that's based on the
Docker image `my-docker-registry/analyzers/nuget` and generates a Dependency Scanning report
`gl-dependency-scanning-report.json` when `/analyzer run` is executed. Define the following in
`.gitlab-ci.yml`:
```yaml
nuget-dependency_scanning:
image:
name: "my-docker-registry/analyzers/nuget"
script:
- /analyzer run
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
```
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate custom security scanners into GitLab.
## Analyzers data
The following table lists the data available for each official analyzer.

View file

@ -259,11 +259,11 @@ it highlighted:
"version": "2.0",
"vulnerabilities": [
{
"id": "51e83874-0ff6-4677-a4c5-249060554eae",
"category": "dependency_scanning",
"name": "Regular Expression Denial of Service",
"message": "Regular Expression Denial of Service in debug",
"description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
"cve": "yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
"severity": "Unknown",
"solution": "Upgrade to latest versions.",
"scanner": {
@ -300,11 +300,11 @@ it highlighted:
]
},
{
"id": "5d681b13-e8fa-4668-957e-8d88f932ddc7",
"category": "dependency_scanning",
"name": "Authentication bypass via incorrect DOM traversal and canonicalization",
"message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
"description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
"cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
"severity": "Unknown",
"solution": "Upgrade to fixed version.\r\n",
"scanner": {
@ -351,7 +351,7 @@ it highlighted:
{
"fixes": [
{
"cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98"
"id": "5d681b13-e8fa-4668-957e-8d88f932ddc7",
}
],
"summary": "Upgrade saml2-js",
@ -371,11 +371,12 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|------------------------------------------------------|-------------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For Dependency Scanning, it will always be `dependency_scanning`. |
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
@ -398,7 +399,8 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
| `remediations[].fixes[].cve` | A string value that describes a fixed vulnerability occurrence in the same format as `vulnerabilities[].cve`. |
| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |

View file

@ -92,7 +92,10 @@ That's needed when one totally relies on [custom analyzers](#custom-analyzers).
## Custom Analyzers
You can provide your own analyzers as a comma separated list of Docker images.
### Custom analyzers with Docker-in-Docker
When Docker-in-Docker for SAST is enabled,
you can provide your own analyzers as a comma-separated list of Docker images.
Here's how to add `analyzers/csharp` and `analyzers/perl` to the default images:
In `.gitlab-ci.yml` define:
@ -112,8 +115,27 @@ This configuration doesn't benefit from the integrated detection step.
SAST has to fetch and spawn each Docker image to establish whether the
custom analyzer can scan the source code.
CAUTION: **Caution:**
Custom analyzers are not spawned automatically when [Docker In Docker](index.md#disabling-docker-in-docker-for-sast) is disabled.
### Custom analyzers without Docker-in-Docker
When Docker-in-Docker for SAST is disabled, you can provide your own analyzers by
defining CI jobs in your CI configuration. For consistency, you should suffix your custom
SAST jobs with `-sast`. Here's how to add a scanning job that's based on the
Docker image `my-docker-registry/analyzers/csharp` and generates a SAST report
`gl-sast-report.json` when `/analyzer run` is executed. Define the following in
`.gitlab-ci.yml`:
```yaml
csharp-sast:
image:
name: "my-docker-registry/analyzers/csharp"
script:
- /analyzer run
artifacts:
reports:
sast: gl-sast-report.json
```
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate custom security scanners into GitLab.
## Analyzers Data

View file

@ -368,11 +368,11 @@ it highlighted:
"version": "2.0",
"vulnerabilities": [
{
"id": "9e96e0ab-23da-4d7d-a09e-0acbaa5e83ca",
"category": "sast",
"name": "Predictable pseudorandom number generator",
"message": "Predictable pseudorandom number generator",
"description": "The use of java.util.Random is predictable",
"cve": "818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM",
"severity": "Medium",
"confidence": "Medium",
"scanner": {
@ -405,9 +405,9 @@ it highlighted:
]
},
{
"id": "e6dbf91f-4c07-46f7-a365-0169489c27d1",
"category": "sast",
"message": "Probable insecure usage of temp file/directory.",
"cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
"severity": "Medium",
"confidence": "Medium",
"scanner": {
@ -446,11 +446,12 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|-----------------------------------------|----------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For SAST, it will always be `sast`. |
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include the occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. Is used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |

View file

@ -312,7 +312,7 @@ From there, you can see tracked over time:
If a significant percentage of traffic is anomalous, it should be investigated
for potential threats, which can be done by
[examining the application logs](#web-application-firewall-modsecurity).
[examining the Web Application Firewall logs](#web-application-firewall-modsecurity).
![Threat Monitoring](img/threat_monitoring_v12_9.png)

View file

@ -17,6 +17,7 @@ NGINX server metrics are detected, which tracks the pages and content directly s
| Throughput (req/sec) | `sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)` |
| Latency (ms) | `avg(nginx_server_requestMsec{%{environment_filter}})` |
| HTTP Error Rate (HTTP Errors / sec) | `sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))` |
| HTTP Error (%)| `sum(rate(nginx_server_requests{code=~"5.*", host="*", %{environment_filter}}[2m])) / sum(rate(nginx_server_requests{code="total", host="*", %{environment_filter}}[2m])) * 100` |
## Configuring Prometheus to monitor for NGINX metrics

View file

@ -0,0 +1,54 @@
namespace :gitlab do
namespace :praefect do
def int?(string)
true if Integer(string) rescue false
end
def print_checksums(header, row)
header.each_with_index do |val, i|
width = [val.length, row[i].length].max
header[i] = header[i].ljust(width)
row[i] = row[i].ljust(width)
end
header_str = header.join(' | ')
puts header_str
puts '-' * header_str.length
puts row.join(' | ')
end
desc 'GitLab | Praefect | Check replicas'
task :replicas, [:project_id] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
unless int?(args.project_id)
puts 'argument must be a valid project_id'
next
end
project = Project.find_by_id(args.project_id)
if project.nil?
puts 'No project was found with that id'
next
end
begin
replicas_resp = project.repository.replicas
sorted_replicas = replicas_resp.replicas.sort_by { |r| r.repository.storage_name }
header = ['Project name'] << "#{replicas_resp.primary.repository.storage_name} (primary)"
header.concat(sorted_replicas.map { |r| r.repository.storage_name })
row = [project.name] << replicas_resp.primary.checksum
row.concat(sorted_replicas.map {|r| r.checksum})
rescue
puts 'Something went wrong when getting replicas.'
next
end
puts "\n"
print_checksums(header, row)
end
end
end

View file

@ -17951,6 +17951,9 @@ msgstr ""
msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
msgid "SecurityDashboard|There was an error while generating the report."
msgstr ""
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
msgstr ""
@ -23402,6 +23405,9 @@ msgstr ""
msgid "You can move around the graph by using the arrow keys."
msgstr ""
msgid "You can now export your security dashboard to a CSV report."
msgstr ""
msgid "You can now submit a merge request to get this change into the original branch."
msgstr ""

View file

@ -789,6 +789,11 @@ describe Projects::PipelinesController do
end
end
context 'when junit_pipeline_screenshots_view is enabled' do
before do
stub_feature_flags(junit_pipeline_screenshots_view: { enabled: true, thing: project })
end
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports_attachment, project: project) }
@ -797,6 +802,7 @@ describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["test_suites"]).to be_present
expect(json_response["test_suites"].first["test_cases"].first).to include("attachment_url")
end
end
@ -812,6 +818,24 @@ describe Projects::PipelinesController do
end
end
context 'when junit_pipeline_screenshots_view is disabled' do
before do
stub_feature_flags(junit_pipeline_screenshots_view: { enabled: false, thing: project })
end
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports_attachment, project: project) }
it 'returns a test reports without attachment_url' do
get_test_report_json(scope: 'with_attachment')
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["test_suites"].first["test_cases"].first).not_to include("attachment_url")
end
end
end
end
context 'when feature is disabled' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }

View file

@ -613,13 +613,7 @@ describe('diffs/components/app', () => {
expect(wrapper.contains(CompareVersions)).toBe(true);
expect(wrapper.find(CompareVersions).props()).toEqual(
expect.objectContaining({
targetBranch: {
branchName: 'target-branch',
versionIndex: -1,
path: '',
},
mergeRequestDiffs: diffsMockData,
mergeRequestDiff,
}),
);
});

View file

@ -0,0 +1,91 @@
import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import CompareDropdownLayout from '~/diffs/components/compare_dropdown_layout.vue';
const TEST_COMMIT_TEXT = '1 commit';
const TEST_CREATED_AT = '2018-10-23T11:49:16.611Z';
describe('CompareDropdownLayout', () => {
let wrapper;
const createVersion = ({ id, isHead, isBase, selected, commitsText, createdAt }) => ({
id,
href: `version/${id}`,
versionName: `version ${id}`,
isHead,
isBase,
short_commit_sha: `abcdef${id}`,
commitsText,
created_at: createdAt,
selected,
});
const createComponent = (propsData = {}) => {
wrapper = shallowMount(CompareDropdownLayout, {
propsData: {
...propsData,
},
});
};
const findListItems = () => wrapper.findAll('li');
const findListItemsData = () =>
findListItems().wrappers.map(listItem => ({
href: listItem.find('a').attributes('href'),
text: trimText(listItem.text()),
createdAt: listItem.findAll(TimeAgo).wrappers[0]?.props('time'),
isActive: listItem.find('a.is-active').exists(),
}));
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with versions', () => {
beforeEach(() => {
const versions = [
createVersion({
id: 1,
isHead: false,
isBase: true,
selected: true,
commitsText: TEST_COMMIT_TEXT,
createdAt: TEST_CREATED_AT,
}),
createVersion({ id: 2, isHead: true, isBase: false, selected: false }),
createVersion({ id: 3, isHead: false, isBase: false, selected: false }),
];
createComponent({ versions });
});
it('renders the selected version name', () => {
expect(wrapper.text()).toContain('version 1');
});
it('renders versions in order', () => {
expect(findListItemsData()).toEqual([
{
href: 'version/1',
text: 'version 1 (base) abcdef1 1 commit',
createdAt: TEST_CREATED_AT,
isActive: true,
},
{
href: 'version/2',
text: 'version 2 (HEAD) abcdef2',
createdAt: undefined,
isActive: false,
},
{
href: 'version/3',
text: 'version 3 abcdef3',
createdAt: undefined,
isActive: false,
},
]);
});
});
});

View file

@ -1,179 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
import diffsMockData from '../mock_data/merge_request_diffs';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { TEST_HOST } from 'helpers/test_constants';
const localVue = createLocalVue();
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
const startVersion = { version_index: 4 };
const mergeRequestVersion = {
version_path: '123',
};
const baseVersionPath = '/gnuwget/wget2/-/merge_requests/6/diffs?diff_id=37';
describe('CompareVersionsDropdown', () => {
let wrapper;
const findSelectedVersion = () => wrapper.find('.dropdown-menu-toggle');
const findVersionsListElements = () => wrapper.findAll('li');
const findLinkElement = index =>
findVersionsListElements()
.at(index)
.find('a');
const findLastLink = () => findLinkElement(findVersionsListElements().length - 1);
const createComponent = (props = {}) => {
wrapper = shallowMount(localVue.extend(CompareVersionsDropdown), {
localVue,
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
describe('selected version name', () => {
it('shows latest version when latest is selected', () => {
createComponent({
mergeRequestVersion,
startVersion,
otherVersions: diffsMockData,
});
expect(findSelectedVersion().text()).toBe('latest version');
});
it('shows target branch name for base branch', () => {
createComponent({
targetBranch,
});
expect(findSelectedVersion().text()).toBe('tmp-wine-dev');
});
it('shows correct version for non-base and non-latest branches', () => {
createComponent({
startVersion,
targetBranch,
});
expect(findSelectedVersion().text()).toBe(`version ${startVersion.version_index}`);
});
});
describe('target versions list', () => {
it('should have the same length as otherVersions if merge request version is present', () => {
createComponent({
mergeRequestVersion,
otherVersions: diffsMockData,
});
expect(findVersionsListElements().length).toEqual(diffsMockData.length);
});
it('should have an otherVersions length plus 1 if no merge request version is present', () => {
createComponent({
targetBranch,
otherVersions: diffsMockData,
});
expect(findVersionsListElements().length).toEqual(diffsMockData.length + 1);
});
it('should have base branch link as active on base branch', () => {
createComponent({
targetBranch,
otherVersions: diffsMockData,
});
expect(findLastLink().classes()).toContain('is-active');
});
it('should have correct branch link as active if start version present', () => {
createComponent({
targetBranch,
startVersion,
otherVersions: diffsMockData,
});
expect(findLinkElement(0).classes()).toContain('is-active');
});
it('should render a correct base version link', () => {
createComponent({
baseVersionPath,
otherVersions: diffsMockData.slice(1),
targetBranch,
});
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).toContain('(base)');
expect(findLastLink().text()).not.toContain('(HEAD)');
});
it('should render a correct head version link', () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { href: `${TEST_HOST}?diff_head=true` },
});
createComponent({
baseVersionPath,
otherVersions: diffsMockData.slice(1),
targetBranch,
});
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).not.toContain('(base)');
expect(findLastLink().text()).toContain('(HEAD)');
});
it('should not render commits count if no showCommitsCount is passed', () => {
createComponent({
otherVersions: diffsMockData,
targetBranch,
});
const commitsCount = diffsMockData[0].commits_count;
expect(findLinkElement(0).text()).not.toContain(`${commitsCount} commit`);
});
it('should render correct commits count if showCommitsCount is passed', () => {
createComponent({
otherVersions: diffsMockData,
targetBranch,
showCommitCount: true,
});
const commitsCount = diffsMockData[0].commits_count;
expect(findLinkElement(0).text()).toContain(`${commitsCount} commit`);
});
it('should render correct commit sha', () => {
createComponent({
otherVersions: diffsMockData,
targetBranch,
});
const commitShaElement = findLinkElement(0).find('.commit-sha');
expect(commitShaElement.text()).toBe(diffsMockData[0].short_commit_sha);
});
it('should render correct time-ago ', () => {
createComponent({
otherVersions: diffsMockData,
targetBranch,
});
const timeAgoElement = findLinkElement(0).find(TimeAgo);
expect(timeAgoElement.exists()).toBe(true);
expect(timeAgoElement.props('time')).toBe(diffsMockData[0].created_at);
});
});
});

View file

@ -12,23 +12,25 @@ localVue.use(Vuex);
describe('CompareVersions', () => {
let wrapper;
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
const targetBranchName = 'tmp-wine-dev';
const createWrapper = props => {
const store = createStore();
const mergeRequestDiff = diffsMockData[0];
store.state.diffs.addedLines = 10;
store.state.diffs.removedLines = 20;
store.state.diffs.diffFiles.push('test');
store.state.diffs.targetBranchName = targetBranchName;
store.state.diffs.mergeRequestDiff = mergeRequestDiff;
store.state.diffs.mergeRequestDiffs = diffsMockData;
wrapper = mount(CompareVersionsComponent, {
localVue,
store,
propsData: {
mergeRequestDiffs: diffsMockData,
mergeRequestDiff: diffsMockData[0],
diffFilesLength: 0,
targetBranch,
...props,
},
});
@ -59,7 +61,7 @@ describe('CompareVersions', () => {
expect(sourceDropdown.exists()).toBe(true);
expect(targetDropdown.exists()).toBe(true);
expect(sourceDropdown.find('a span').html()).toContain('latest version');
expect(targetDropdown.find('a span').html()).toContain(targetBranch.branchName);
expect(targetDropdown.find('a span').html()).toContain(targetBranchName);
});
it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
@ -119,21 +121,6 @@ describe('CompareVersions', () => {
});
});
describe('comparableDiffs', () => {
it('should not contain the first item in the mergeRequestDiffs property', () => {
const { comparableDiffs } = wrapper.vm;
const comparableDiffsMock = diffsMockData.slice(1);
expect(comparableDiffs).toEqual(comparableDiffsMock);
});
});
describe('baseVersionPath', () => {
it('should be set correctly from mergeRequestDiff', () => {
expect(wrapper.vm.baseVersionPath).toEqual(wrapper.vm.mergeRequestDiff.base_version_path);
});
});
describe('commit', () => {
beforeEach(done => {
wrapper.vm.$store.state.diffs.commit = getDiffWithCommit().commit;

View file

@ -0,0 +1,100 @@
import * as getters from '~/diffs/store/getters';
import state from '~/diffs/store/modules/diff_state';
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '~/diffs/constants';
import diffsMockData from '../mock_data/merge_request_diffs';
describe('Compare diff version dropdowns', () => {
let localState;
beforeEach(() => {
localState = state();
localState.mergeRequestDiff = {
base_version_path: 'basePath',
head_version_path: 'headPath',
version_index: 1,
};
localState.targetBranchName = 'baseVersion';
localState.mergeRequestDiffs = diffsMockData;
});
describe('selectedTargetIndex', () => {
it('without startVersion', () => {
expect(getters.selectedTargetIndex(localState)).toEqual(DIFF_COMPARE_BASE_VERSION_INDEX);
});
it('with startVersion', () => {
const startVersion = { version_index: 1 };
localState.startVersion = startVersion;
expect(getters.selectedTargetIndex(localState)).toEqual(startVersion.version_index);
});
});
it('selectedSourceIndex', () => {
expect(getters.selectedSourceIndex(localState)).toEqual(
localState.mergeRequestDiff.version_index,
);
});
describe('diffCompareDropdownTargetVersions', () => {
// diffCompareDropdownTargetVersions slices the array at the first position
// and appends a "base" version which is why we use diffsMockData[1] below
// This is to display "base" at the end of the target dropdown
const expectedFirstVersion = {
...diffsMockData[1],
href: expect.any(String),
versionName: expect.any(String),
};
const expectedBaseVersion = {
versionName: 'baseVersion',
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
href: 'basePath',
isBase: true,
};
it('base version selected', () => {
expectedFirstVersion.selected = false;
expectedBaseVersion.selected = true;
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, {
selectedTargetIndex: DIFF_COMPARE_BASE_VERSION_INDEX,
});
const lastVersion = targetVersions[targetVersions.length - 1];
expect(targetVersions[0]).toEqual(expectedFirstVersion);
expect(lastVersion).toEqual(expectedBaseVersion);
});
it('first version selected', () => {
expectedFirstVersion.selected = true;
expectedBaseVersion.selected = false;
localState.startVersion = expectedFirstVersion;
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, {
selectedTargetIndex: expectedFirstVersion.version_index,
});
const lastVersion = targetVersions[targetVersions.length - 1];
expect(targetVersions[0]).toEqual(expectedFirstVersion);
expect(lastVersion).toEqual(expectedBaseVersion);
});
});
it('diffCompareDropdownSourceVersions', () => {
const firstDiff = localState.mergeRequestDiffs[0];
const expectedShape = {
...firstDiff,
href: firstDiff.version_path,
commitsText: `${firstDiff.commits_count} commits,`,
versionName: 'latest version',
selected: true,
};
const sourceVersions = getters.diffCompareDropdownSourceVersions(localState, {
selectedSourceIndex: expectedShape.version_index,
});
expect(sourceVersions[0]).toEqual(expectedShape);
expect(sourceVersions[1].selected).toBe(false);
});
});

View file

@ -1,6 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import NoteHeader from '~/notes/components/note_header.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -17,6 +18,15 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
const author = {
avatar_url: null,
id: 1,
name: 'Root',
path: '/root',
state: 'active',
username: 'root',
};
const createComponent = props => {
wrapper = shallowMount(NoteHeader, {
localVue,
@ -83,16 +93,7 @@ describe('NoteHeader component', () => {
});
it('renders an author link if author is passed to props', () => {
createComponent({
author: {
avatar_url: null,
id: 1,
name: 'Root',
path: '/root',
state: 'active',
username: 'root',
},
});
createComponent({ author });
expect(wrapper.find('.js-user-link').exists()).toBe(true);
});
@ -138,4 +139,18 @@ describe('NoteHeader component', () => {
expect(actions.setTargetNoteHash).toHaveBeenCalled();
});
});
test.each`
props | expected | message1 | message2
${{ author: { ...author, is_gitlab_employee: true } }} | ${true} | ${'renders'} | ${'true'}
${{ author: { ...author, is_gitlab_employee: false } }} | ${false} | ${"doesn't render"} | ${'false'}
${{ author }} | ${false} | ${"doesn't render"} | ${'undefined'}
`(
'$message1 GitLab team member badge when `is_gitlab_employee` is $message2',
({ props, expected }) => {
createComponent(props);
expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
},
);
});

View file

@ -6,7 +6,7 @@ def match_mr1_note(content_regex)
MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{content_regex}/)}.first
end
describe Gitlab::ImportExport::Project::TreeRestorer do
describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'https://gitlab.com/gitlab-org/gitlab/-/issues/213793' } do
include ImportExport::CommonUtil
let(:shared) { project.import_export_shared }

View file

@ -3,16 +3,16 @@
require 'spec_helper'
describe SearchService do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let(:accessible_group) { create(:group, :private) }
let(:inaccessible_group) { create(:group, :private) }
let!(:group_member) { create(:group_member, group: accessible_group, user: user) }
let_it_be(:accessible_group) { create(:group, :private) }
let_it_be(:inaccessible_group) { create(:group, :private) }
let_it_be(:group_member) { create(:group_member, group: accessible_group, user: user) }
let!(:accessible_project) { create(:project, :private, name: 'accessible_project') }
let(:note) { create(:note_on_issue, project: accessible_project) }
let_it_be(:accessible_project) { create(:project, :repository, :private, name: 'accessible_project') }
let_it_be(:note) { create(:note_on_issue, project: accessible_project) }
let!(:inaccessible_project) { create(:project, :private, name: 'inaccessible_project') }
let_it_be(:inaccessible_project) { create(:project, :repository, :private, name: 'inaccessible_project') }
let(:snippet) { create(:snippet, author: user) }
let(:group_project) { create(:project, group: accessible_group, name: 'group_project') }
@ -298,67 +298,129 @@ describe SearchService do
end
context 'redacting search results' do
shared_examples 'it redacts incorrect results' do
let(:search) { 'anything' }
subject(:result) { search_service.search_objects }
def found_blob(project)
Gitlab::Search::FoundBlob.new(project: project)
end
def found_wiki_page(project)
Gitlab::Search::FoundWikiPage.new(found_blob(project))
end
before do
allow(Ability).to receive(:allowed?).and_return(allowed)
expect(search_service)
.to receive(:search_results)
.and_return(double('search results', objects: unredacted_results))
end
context 'when allowed' do
let(:allowed) { true }
it 'does nothing' do
expect(results).not_to be_empty
expect(results).to all(be_an(model_class))
end
def ar_relation(klass, *objects)
klass.id_in(objects.map(&:id))
end
context 'when disallowed' do
let(:allowed) { false }
it 'does nothing' do
expect(results).to be_empty
end
end
def kaminari_array(*objects)
Kaminari.paginate_array(objects).page(1).per(20)
end
context 'issues' do
let(:issue) { create(:issue, project: accessible_project) }
let(:readable) { create(:issue, project: accessible_project) }
let(:unreadable) { create(:issue, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
let(:scope) { 'issues' }
let(:model_class) { Issue }
let(:ability) { :read_issue }
let(:search) { issue.title }
let(:results) { subject.search_objects }
it_behaves_like 'it redacts incorrect results'
it 'redacts the inaccessible issue' do
expect(result).to contain_exactly(readable)
end
end
context 'notes' do
let(:note) { create(:note_on_commit, project: accessible_project) }
let(:readable) { create(:note_on_commit, project: accessible_project) }
let(:unreadable) { create(:note_on_commit, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(Note, readable, unreadable) }
let(:scope) { 'notes' }
let(:model_class) { Note }
let(:ability) { :read_note }
let(:search) { note.note }
let(:results) do
described_class.new(
user,
project_id: accessible_project.id,
scope: scope,
search: note.note
).search_objects
end
it_behaves_like 'it redacts incorrect results'
it 'redacts the inaccessible note' do
expect(result).to contain_exactly(readable)
end
end
context 'merge_requests' do
let(:readable) { create(:merge_request, source_project: accessible_project, author: user) }
let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
let(:scope) { 'merge_requests' }
let(:model_class) { MergeRequest }
let(:ability) { :read_merge_request }
let(:merge_request) { create(:merge_request, source_project: accessible_project, author: user) }
let(:search) { merge_request.title }
let(:results) { subject.search_objects }
it_behaves_like 'it redacts incorrect results'
it 'redacts the inaccessible merge request' do
expect(result).to contain_exactly(readable)
end
end
context 'project repository blobs' do
let(:readable) { found_blob(accessible_project) }
let(:unreadable) { found_blob(inaccessible_project) }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'blobs' }
it 'redacts the inaccessible blob' do
expect(result).to contain_exactly(readable)
end
end
context 'project wiki blobs' do
let(:readable) { found_wiki_page(accessible_project) }
let(:unreadable) { found_wiki_page(inaccessible_project) }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'wiki_blobs' }
it 'redacts the inaccessible blob' do
expect(result).to contain_exactly(readable)
end
end
context 'project snippets' do
let(:readable) { create(:project_snippet, project: accessible_project) }
let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
let(:scope) { 'snippet_blobs' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
end
end
context 'personal snippets' do
let(:readable) { create(:personal_snippet, :private, author: user) }
let(:unreadable) { create(:personal_snippet, :private) }
let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
let(:scope) { 'snippet_blobs' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
end
end
context 'commits' do
let(:readable) { accessible_project.commit }
let(:unreadable) { inaccessible_project.commit }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'commits' }
it 'redacts the inaccessible commit' do
expect(result).to contain_exactly(readable)
end
end
context 'users' do
let(:other_user) { create(:user) }
let(:unredacted_results) { ar_relation(User, user, other_user) }
let(:scope) { 'users' }
it 'passes the users through' do
# Users are always visible to everyone
expect(result).to contain_exactly(user, other_user)
end
end
end
end

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'rake_helper'
describe 'gitlab:praefect:replicas' do
before do
Rake.application.rake_require 'tasks/gitlab/praefect'
end
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
describe 'replicas', :praefect do
context 'when a valid project id is used as the argument' do
let(:project_arg) { project.id }
it "calls praefect info service's replicas method" do
expect_any_instance_of(Gitlab::GitalyClient::PraefectInfoService).to receive(:replicas).and_call_original
run_rake_task('gitlab:praefect:replicas', project_arg)
end
it 'prints out the expected row' do
row = /#{project.name}\s+\| #{project.repository.checksum}/
expect { run_rake_task('gitlab:praefect:replicas', project_arg) }.to output(row).to_stdout
end
end
context 'when a non existent project id is used as the argument' do
let(:project_arg) { '2' }
it "does not call praefect info service's replicas method" do
expect_any_instance_of(Gitlab::GitalyClient::PraefectInfoService).not_to receive(:replicas)
run_rake_task('gitlab:praefect:replicas', project_arg)
end
end
context 'when replicas throws an exception' do
before do
allow_next_instance_of(Gitlab::GitalyClient::PraefectInfoService) do |instance|
expect(instance).to receive(:replicas).and_raise("error")
end
end
it 'aborts with the correct error message' do
expect { run_rake_task('gitlab:praefect:replicas', project.id) }.to output("Something went wrong when getting replicas.\n").to_stdout
end
end
end
end