summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-09 21:09:19 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-09 21:09:19 +0000
commit254ec28f5448f6f353cd98f637985de3d1405854 (patch)
tree1c84ed7b7dd32db96454af034cd6c7e90699e76d
parent141902c04943d5fb43c014b8cf42af60a3bc0cdf (diff)
downloadgitlab-ce-254ec28f5448f6f353cd98f637985de3d1405854.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/diffs/components/app.vue13
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue78
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue39
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue162
-rw-r--r--app/assets/javascripts/diffs/constants.js2
-rw-r--r--app/assets/javascripts/diffs/store/getters.js2
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js43
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue35
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js9
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue27
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/services/search_service.rb26
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--changelogs/unreleased/118788-add-nginx-500-percentage-metric.yml5
-rw-r--r--changelogs/unreleased/201886-refactor-search-service-redaction.yml5
-rw-r--r--changelogs/unreleased/29371-group-icon-missing-on-small-viewports-on-profile-page.yml5
-rw-r--r--changelogs/unreleased/38096-add-index-on-resource-milestone-events-created-at-pd.yml5
-rw-r--r--changelogs/unreleased/jc-replicas-rake.yml5
-rw-r--r--config/prometheus/common_metrics.yml10
-rw-r--r--db/migrate/20200406135648_add_index_to_created_at_on_resource_milestone_events.rb19
-rw-r--r--db/post_migrate/20200408110856_add_nginx_five_hundred_percentage_metric.rb13
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/gitaly/praefect.md40
-rw-r--r--doc/administration/logs.md37
-rw-r--r--doc/api/epics.md5
-rw-r--r--doc/development/pipelines.md12
-rw-r--r--doc/user/application_security/container_scanning/index.md13
-rw-r--r--doc/user/application_security/dependency_scanning/analyzers.md31
-rw-r--r--doc/user/application_security/dependency_scanning/index.md12
-rw-r--r--doc/user/application_security/sast/analyzers.md28
-rw-r--r--doc/user/application_security/sast/index.md7
-rw-r--r--doc/user/clusters/applications.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md1
-rw-r--r--lib/tasks/gitlab/praefect.rake54
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb48
-rw-r--r--spec/frontend/diffs/components/app_spec.js6
-rw-r--r--spec/frontend/diffs/components/compare_dropdown_layout_spec.js91
-rw-r--r--spec/frontend/diffs/components/compare_versions_dropdown_spec.js179
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js25
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js100
-rw-r--r--spec/frontend/notes/components/note_header_spec.js35
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb160
-rw-r--r--spec/tasks/gitlab/praefect_rake_spec.rb52
47 files changed, 915 insertions, 548 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 3ea2a2fbaee..072bcaaad97 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
new file mode 100644
index 00000000000..ed4edabd81c
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -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>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 34716c214c6..c680c3f4600 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -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>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
deleted file mode 100644
index cc4b2dacab3..00000000000
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 7521f3c950a..b07dfe5f33d 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -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;
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 3898974638f..047caed1e63 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -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;
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
new file mode 100644
index 00000000000..14c51602f28
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -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,
+ }));
+};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 81f1506260c..87938ababed 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -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: [],
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 16351baedb7..9cb592ceedb 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -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,19 +78,21 @@ export default {
{{ __('Toggle thread') }}
</button>
</div>
- <a
- v-if="hasAuthor"
- v-once
- :href="author.path"
- class="js-user-link"
- :data-user-id="author.id"
- :data-username="author.username"
- >
- <slot name="note-header-info"></slot>
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
- <span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ <template v-if="hasAuthor">
+ <a
+ v-once
+ :href="author.path"
+ class="js-user-link"
+ :data-user-id="author.id"
+ :data-username="author.username"
+ >
+ <slot name="note-header-info"></slot>
+ <span class="note-header-author-name bold">{{ author.name }}</span>
+ <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>
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 56016cb980c..49862b64c27 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -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();
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 8d779e04673..59d47ae4155 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -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);
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue b/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
new file mode 100644
index 00000000000..527cbd458e2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
@@ -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>
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index af512ac6f07..726ce8974c7 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -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
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 75cd6c78a52..c96599f9958 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -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
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index e47967ef622..60c9c076a70 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -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'
diff --git a/changelogs/unreleased/118788-add-nginx-500-percentage-metric.yml b/changelogs/unreleased/118788-add-nginx-500-percentage-metric.yml
new file mode 100644
index 00000000000..6a265250c8c
--- /dev/null
+++ b/changelogs/unreleased/118788-add-nginx-500-percentage-metric.yml
@@ -0,0 +1,5 @@
+---
+title: Add Nginx error percentage metric
+merge_request: 28983
+author:
+type: added
diff --git a/changelogs/unreleased/201886-refactor-search-service-redaction.yml b/changelogs/unreleased/201886-refactor-search-service-redaction.yml
new file mode 100644
index 00000000000..d86c55909e0
--- /dev/null
+++ b/changelogs/unreleased/201886-refactor-search-service-redaction.yml
@@ -0,0 +1,5 @@
+---
+title: Make search redaction more robust
+merge_request: 29166
+author:
+type: changed
diff --git a/changelogs/unreleased/29371-group-icon-missing-on-small-viewports-on-profile-page.yml b/changelogs/unreleased/29371-group-icon-missing-on-small-viewports-on-profile-page.yml
new file mode 100644
index 00000000000..5e9f3a3b006
--- /dev/null
+++ b/changelogs/unreleased/29371-group-icon-missing-on-small-viewports-on-profile-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing group icons on profile page when screen < 576px
+merge_request: 28973
+author:
+type: fixed
diff --git a/changelogs/unreleased/38096-add-index-on-resource-milestone-events-created-at-pd.yml b/changelogs/unreleased/38096-add-index-on-resource-milestone-events-created-at-pd.yml
new file mode 100644
index 00000000000..999be265544
--- /dev/null
+++ b/changelogs/unreleased/38096-add-index-on-resource-milestone-events-created-at-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add index for created_at of resource_milestone_events
+merge_request: 28929
+author:
+type: performance
diff --git a/changelogs/unreleased/jc-replicas-rake.yml b/changelogs/unreleased/jc-replicas-rake.yml
new file mode 100644
index 00000000000..2a5e7781061
--- /dev/null
+++ b/changelogs/unreleased/jc-replicas-rake.yml
@@ -0,0 +1,5 @@
+---
+title: Add Praefect rake task to print out replica checksums
+merge_request: 28369
+author:
+type: added
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 85833cc1968..4d0ea4a345d 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -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: "%"
+
diff --git a/db/migrate/20200406135648_add_index_to_created_at_on_resource_milestone_events.rb b/db/migrate/20200406135648_add_index_to_created_at_on_resource_milestone_events.rb
new file mode 100644
index 00000000000..6b8b981990d
--- /dev/null
+++ b/db/migrate/20200406135648_add_index_to_created_at_on_resource_milestone_events.rb
@@ -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
diff --git a/db/post_migrate/20200408110856_add_nginx_five_hundred_percentage_metric.rb b/db/post_migrate/20200408110856_add_nginx_five_hundred_percentage_metric.rb
new file mode 100644
index 00000000000..a99e0291fe8
--- /dev/null
+++ b/db/post_migrate/20200408110856_add_nginx_five_hundred_percentage_metric.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index d179c52ee6b..3faa5499f6c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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
\.
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 0a0e193658e..737deaa7f4e 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -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
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 889a8b2d6b0..c43406fb647 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -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
diff --git a/doc/api/epics.md b/doc/api/epics.md
index a0261aab605..bf6a18fcedc 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -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": {
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index c123722dc4e..aecfe3af9c4 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -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
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 5b755916fd8..3d6c9e0b0ba 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -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.
diff --git a/doc/user/application_security/dependency_scanning/analyzers.md b/doc/user/application_security/dependency_scanning/analyzers.md
index 3b4b341739b..26352f21cfb 100644
--- a/doc/user/application_security/dependency_scanning/analyzers.md
+++ b/doc/user/application_security/dependency_scanning/analyzers.md
@@ -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.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 7032ec8c75e..781bda47a43 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -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). |
diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md
index c27c61a52e2..3ce52a024bd 100644
--- a/doc/user/application_security/sast/analyzers.md
+++ b/doc/user/application_security/sast/analyzers.md
@@ -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
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 3f172285014..3a711138a76 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -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. |
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 1adbbf51397..f8fd07276c5 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -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)
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index fc8abc47c4c..d5f078f3ddf 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -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
diff --git a/lib/tasks/gitlab/praefect.rake b/lib/tasks/gitlab/praefect.rake
new file mode 100644
index 00000000000..a9b126ae379
--- /dev/null
+++ b/lib/tasks/gitlab/praefect.rake
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 68210a23b60..f4e75cefb67 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index d9345cfaced..9d243bf5a7f 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -789,25 +789,49 @@ describe Projects::PipelinesController do
end
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) }
+ context 'when junit_pipeline_screenshots_view is enabled' do
+ before do
+ stub_feature_flags(junit_pipeline_screenshots_view: { enabled: true, thing: project })
+ end
- it 'returns a test reports with attachment' do
- get_test_report_json(scope: 'with_attachment')
+ 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) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response["test_suites"]).to be_present
+ it 'returns a test reports with attachment' do
+ get_test_report_json(scope: 'with_attachment')
+
+ 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
+
+ context 'when test_report does not contain attachment and scope is with_attachment as a URL param' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'returns a test reports with empty values' do
+ get_test_report_json(scope: 'with_attachment')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["test_suites"]).to be_empty
+ end
end
end
- context 'when test_report does not contain attachment and scope is with_attachment as a URL param' do
- let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ context 'when junit_pipeline_screenshots_view is disabled' do
+ before do
+ stub_feature_flags(junit_pipeline_screenshots_view: { enabled: false, thing: project })
+ end
- it 'returns a test reports with empty values' do
- get_test_report_json(scope: 'with_attachment')
+ 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) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response["test_suites"]).to be_empty
+ 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
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 78e3ff4a60c..3a0354205f8 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -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,
}),
);
});
diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
new file mode 100644
index 00000000000..a163a43daf1
--- /dev/null
+++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
@@ -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,
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/compare_versions_dropdown_spec.js b/spec/frontend/diffs/components/compare_versions_dropdown_spec.js
deleted file mode 100644
index 5033bdd9044..00000000000
--- a/spec/frontend/diffs/components/compare_versions_dropdown_spec.js
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index ff92a12eaf6..7f69a6344c1 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -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;
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
new file mode 100644
index 00000000000..3e5ba66d5e4
--- /dev/null
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 6544ad3e1fe..642ab5138dc 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -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);
+ },
+ );
});
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 9fc0fdeb1bc..1eac580bc5e 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -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 }
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 10dafaebe85..97d7ca6e1ad 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -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
- before do
- allow(Ability).to receive(:allowed?).and_return(allowed)
- end
+ let(:search) { 'anything' }
- context 'when allowed' do
- let(:allowed) { true }
+ subject(:result) { search_service.search_objects }
- it 'does nothing' do
- expect(results).not_to be_empty
- expect(results).to all(be_an(model_class))
- end
- end
+ def found_blob(project)
+ Gitlab::Search::FoundBlob.new(project: project)
+ end
- context 'when disallowed' do
- let(:allowed) { false }
+ def found_wiki_page(project)
+ Gitlab::Search::FoundWikiPage.new(found_blob(project))
+ end
- it 'does nothing' do
- expect(results).to be_empty
- end
- end
+ before do
+ expect(search_service)
+ .to receive(:search_results)
+ .and_return(double('search results', objects: unredacted_results))
+ end
+
+ def ar_relation(klass, *objects)
+ klass.id_in(objects.map(&:id))
+ 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
diff --git a/spec/tasks/gitlab/praefect_rake_spec.rb b/spec/tasks/gitlab/praefect_rake_spec.rb
new file mode 100644
index 00000000000..d986a778c8c
--- /dev/null
+++ b/spec/tasks/gitlab/praefect_rake_spec.rb
@@ -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