summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.haml-lint_todo.yml1
-rw-r--r--.rubocop_manual_todo.yml47
-rw-r--r--Gemfile2
-rwxr-xr-xRakefile2
-rw-r--r--app/assets/javascripts/feature_highlight/constants.js1
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_popover.vue101
-rw-r--r--app/assets/javascripts/feature_highlight/index.js28
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue169
-rw-r--r--app/assets/javascripts/issuable/init_issuable_by_email.js35
-rw-r--r--app/assets/javascripts/issuable_index.js28
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue9
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js9
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss21
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss54
-rw-r--r--app/assets/stylesheets/pages/issues.scss12
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/projects/alert_management_helper.rb3
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/note.rb4
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/issues/_issue.atom.builder2
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby2
-rw-r--r--app/views/layouts/xml.atom.builder2
-rw-r--r--app/views/projects/_issuable_by_email.html.haml49
-rw-r--r--app/views/projects/commits/_commit.atom.builder2
-rw-r--r--app/views/projects/commits/show.atom.builder2
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/index.html.haml4
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/tags/_tag.atom.builder2
-rw-r--r--app/views/projects/tags/index.atom.builder2
-rw-r--r--app/views/search/results/_issuable.html.haml2
-rw-r--r--app/views/users/show.atom.builder2
-rwxr-xr-xbin/secpick16
-rw-r--r--changelogs/unreleased/254256-replace-bootstrap-modal-in-app-views-projects-_issuable_by_email-h.yml5
-rw-r--r--changelogs/unreleased/273574-fix-bad-data-in-projects-has_external_issue_tracker.yml5
-rw-r--r--changelogs/unreleased/295311-add-updated-at-to-search-results.yml5
-rw-r--r--changelogs/unreleased/300220-add-security-orchestration-policy-configuration-model.yml5
-rw-r--r--changelogs/unreleased/300546-add-new-plans.yml5
-rw-r--r--changelogs/unreleased/fj-fix-bug-rendering-snippet-activity.yml5
-rw-r--r--danger/changes_size/Dangerfile2
-rw-r--r--danger/metadata/Dangerfile2
-rw-r--r--db/migrate/20210209160510_create_security_orchestration_policy_configurations.rb25
-rw-r--r--db/post_migrate/20210105030125_cleanup_projects_with_bad_has_external_wiki_data.rb88
-rw-r--r--db/post_migrate/20210205104425_add_new_post_eoa_plans.rb14
-rw-r--r--db/schema_migrations/202101050301251
-rw-r--r--db/schema_migrations/202102051044251
-rw-r--r--db/schema_migrations/202102091605101
-rw-r--r--db/structure.sql34
-rw-r--r--doc/administration/logs.md2
-rw-r--r--doc/administration/reference_architectures/10k_users.md819
-rw-r--r--doc/administration/reference_architectures/1k_users.md1
-rw-r--r--doc/administration/reference_architectures/25k_users.md2
-rw-r--r--doc/administration/reference_architectures/2k_users.md56
-rw-r--r--doc/administration/reference_architectures/3k_users.md2
-rw-r--r--doc/administration/reference_architectures/50k_users.md2
-rw-r--r--doc/administration/reference_architectures/5k_users.md2
-rw-r--r--doc/administration/reference_architectures/index.md3
-rw-r--r--doc/raketasks/cleanup.md2
-rw-r--r--doc/user/gitlab_com/index.md18
-rw-r--r--lib/gitlab/instrumentation/elasticsearch_transport.rb15
-rw-r--r--lib/gitlab/instrumentation_helper.rb2
-rw-r--r--locale/gitlab.pot41
-rwxr-xr-xscripts/flaky_examples/detect-new-flaky-examples1
-rwxr-xr-xscripts/flaky_examples/prune-old-flaky-examples1
-rwxr-xr-xscripts/gather-test-memory-data1
-rwxr-xr-xscripts/generate-gems-memory-metrics-static1
-rwxr-xr-xscripts/generate-gems-size-metrics-static1
-rwxr-xr-xscripts/generate-memory-metrics-on-boot1
-rwxr-xr-xscripts/generate-test-mapping1
-rwxr-xr-xscripts/gitaly-test-build1
-rwxr-xr-xscripts/gitaly-test-spawn1
-rw-r--r--scripts/gitaly_test.rb4
-rwxr-xr-xscripts/insert-rspec-profiling-data1
-rwxr-xr-xscripts/lint-rugged1
-rwxr-xr-xscripts/merge-html-reports1
-rwxr-xr-xscripts/merge-reports1
-rwxr-xr-xscripts/merge-simplecov1
-rwxr-xr-xscripts/no-ee-check2
-rwxr-xr-xscripts/pack-test-mapping1
-rwxr-xr-xscripts/static-analysis1
-rwxr-xr-xscripts/sync-reports1
-rwxr-xr-xscripts/unpack-test-mapping1
-rwxr-xr-xscripts/update-feature-categories1
-rwxr-xr-xscripts/used-feature-flags1
-rwxr-xr-xscripts/verify-tff-mapping1
-rw-r--r--spec/features/boards/boards_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_by_email_spec.rb4
-rw-r--r--spec/features/issues/user_resets_their_incoming_email_token_spec.rb24
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js4
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_popover_spec.js80
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js164
-rw-r--r--spec/frontend/issuable_spec.js42
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js33
-rw-r--r--spec/helpers/events_helper_spec.rb8
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb1
-rw-r--r--spec/migrations/add_new_post_eoa_plans_spec.rb32
-rw-r--r--spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb89
-rw-r--r--spec/models/event_spec.rb52
-rw-r--r--spec/models/note_spec.rb10
113 files changed, 1842 insertions, 570 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 7f1a7ff4cb6..5b92863b91e 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -160,7 +160,6 @@ linters:
- 'app/views/projects/_gitlab_import_modal.html.haml'
- 'app/views/projects/_home_panel.html.haml'
- 'app/views/projects/_import_project_pane.html.haml'
- - 'app/views/projects/_issuable_by_email.html.haml'
- 'app/views/projects/_readme.html.haml'
- 'app/views/projects/artifacts/_artifact.html.haml'
- 'app/views/projects/artifacts/_tree_file.html.haml'
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 96437f47758..477d9adec40 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -2473,50 +2473,3 @@ Gitlab/NamespacedClass:
- 'spec/support/sidekiq_middleware.rb'
- 'spec/tasks/gitlab/task_helpers_spec.rb'
- 'spec/uploaders/object_storage_spec.rb'
-
-# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/299105
-Style/FrozenStringLiteralComment:
- Exclude:
- - 'Gemfile'
- - 'Rakefile'
- - 'app/views/dashboard/issues.atom.builder'
- - 'app/views/dashboard/projects/index.atom.builder'
- - 'app/views/events/_event.atom.builder'
- - 'app/views/groups/issues.atom.builder'
- - 'app/views/groups/show.atom.builder'
- - 'app/views/issues/_issue.atom.builder'
- - 'app/views/issues/_issues_calendar.ics.ruby'
- - 'app/views/layouts/xml.atom.builder'
- - 'app/views/projects/commits/_commit.atom.builder'
- - 'app/views/projects/commits/show.atom.builder'
- - 'app/views/projects/issues/index.atom.builder'
- - 'app/views/projects/show.atom.builder'
- - 'app/views/projects/tags/_tag.atom.builder'
- - 'app/views/projects/tags/index.atom.builder'
- - 'app/views/users/show.atom.builder'
- - 'bin/secpick'
- - 'danger/changes_size/Dangerfile'
- - 'danger/metadata/Dangerfile'
- - 'scripts/flaky_examples/detect-new-flaky-examples'
- - 'scripts/flaky_examples/prune-old-flaky-examples'
- - 'scripts/gather-test-memory-data'
- - 'scripts/generate-gems-memory-metrics-static'
- - 'scripts/generate-gems-size-metrics-static'
- - 'scripts/generate-memory-metrics-on-boot'
- - 'scripts/generate-test-mapping'
- - 'scripts/gitaly-test-build'
- - 'scripts/gitaly-test-spawn'
- - 'scripts/gitaly_test.rb'
- - 'scripts/insert-rspec-profiling-data'
- - 'scripts/lint-rugged'
- - 'scripts/merge-html-reports'
- - 'scripts/merge-reports'
- - 'scripts/merge-simplecov'
- - 'scripts/no-ee-check'
- - 'scripts/pack-test-mapping'
- - 'scripts/static-analysis'
- - 'scripts/sync-reports'
- - 'scripts/unpack-test-mapping'
- - 'scripts/update-feature-categories'
- - 'scripts/used-feature-flags'
- - 'scripts/verify-tff-mapping'
diff --git a/Gemfile b/Gemfile
index 96a123294e4..66ca052dada 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org'
gem 'rails', '~> 6.0.3.1'
diff --git a/Rakefile b/Rakefile
index de0d6695c7b..445542e5c00 100755
--- a/Rakefile
+++ b/Rakefile
@@ -1,4 +1,6 @@
#!/usr/bin/env rake
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
diff --git a/app/assets/javascripts/feature_highlight/constants.js b/app/assets/javascripts/feature_highlight/constants.js
new file mode 100644
index 00000000000..3e4cd11f7d5
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/constants.js
@@ -0,0 +1 @@
+export const POPOVER_TARGET_ID = 'feature-highlight-trigger';
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
new file mode 100644
index 00000000000..879427eef96
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
@@ -0,0 +1,101 @@
+<script>
+import {
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import clusterPopover from '@gitlab/svgs/dist/illustrations/cluster_popover.svg';
+import { __ } from '~/locale';
+import { dismiss } from './feature_highlight_helper';
+import { POPOVER_TARGET_ID } from './constants';
+
+export default {
+ components: {
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
+ highlightId: {
+ type: String,
+ required: true,
+ },
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dismissed: false,
+ triggerHidden: false,
+ };
+ },
+ methods: {
+ dismiss() {
+ dismiss(this.dismissEndpoint, this.highlightId);
+ this.$refs.popover.$emit('close');
+ this.dismissed = true;
+ },
+ hideTrigger() {
+ if (this.dismissed) {
+ this.triggerHidden = true;
+ }
+ },
+ },
+ clusterPopover,
+ targetId: POPOVER_TARGET_ID,
+ i18n: {
+ highlightMessage: __('Allows you to add and manage Kubernetes clusters.'),
+ autoDevopsProTipMessage: __(
+ 'Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!',
+ ),
+ dismissButtonLabel: __('Got it!'),
+ },
+};
+</script>
+<template>
+ <div class="gl-ml-3">
+ <span v-if="!triggerHidden" :id="$options.targetId" class="feature-highlight"></span>
+ <gl-popover
+ ref="popover"
+ :target="$options.targetId"
+ :css-classes="['feature-highlight-popover']"
+ triggers="hover"
+ container="body"
+ placement="right"
+ boundary="viewport"
+ @hidden="hideTrigger"
+ >
+ <span
+ v-safe-html="$options.clusterPopover"
+ class="feature-highlight-illustration gl-display-flex gl-justify-content-center gl-py-4 gl-w-full"
+ ></span>
+ <div class="gl-px-4 gl-py-5">
+ <p>
+ {{ $options.i18n.highlightMessage }}
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.autoDevopsProTipMessage">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="autoDevopsHelpPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button size="small" icon="thumb-up" variant="confirm" @click="dismiss">
+ {{ $options.i18n.dismissButtonLabel }}
+ </gl-button>
+ </div>
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/feature_highlight/index.js b/app/assets/javascripts/feature_highlight/index.js
new file mode 100644
index 00000000000..3a8b211b3c5
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/index.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+
+const init = async () => {
+ const el = document.querySelector('.js-feature-highlight');
+
+ if (!el) {
+ return null;
+ }
+
+ const { autoDevopsHelpPath, highlight: highlightId, dismissEndpoint } = el.dataset;
+ const { default: FeatureHighlight } = await import(
+ /* webpackChunkName: 'feature_highlight' */ './feature_highlight_popover.vue'
+ );
+
+ return new Vue({
+ el,
+ render: (h) =>
+ h(FeatureHighlight, {
+ props: {
+ autoDevopsHelpPath,
+ highlightId,
+ dismissEndpoint,
+ },
+ }),
+ });
+};
+
+export default init;
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
new file mode 100644
index 00000000000..330118ce8a6
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -0,0 +1,169 @@
+<script>
+import {
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+ GlSprintf,
+ GlLink,
+ GlFormInputGroup,
+ GlIcon,
+} from '@gitlab/ui';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { sprintf, __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ name: 'IssuableByEmail',
+ components: {
+ GlButton,
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlFormInputGroup,
+ GlIcon,
+ ModalCopyButton,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ initialEmail: {
+ default: null,
+ },
+ issuableType: {
+ default: '',
+ },
+ emailsHelpPagePath: {
+ default: '',
+ },
+ quickActionsHelpPath: {
+ default: '',
+ },
+ markdownHelpPath: {
+ default: '',
+ },
+ resetPath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ email: this.initialEmail,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ issuableName: this.issuableType === 'issue' ? 'issue' : 'merge request',
+ };
+ },
+ computed: {
+ mailToLink() {
+ const subject = sprintf(__('Enter the %{name} title'), {
+ name: this.issuableName,
+ });
+ const body = sprintf(__('Enter the %{name} description'), {
+ name: this.issuableName,
+ });
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `mailto:${this.email}?subject=${subject}&body=${body}`;
+ },
+ },
+ methods: {
+ async resetIncomingEmailToken() {
+ try {
+ const {
+ data: { new_address: newAddress },
+ } = await axios.put(this.resetPath);
+ this.email = newAddress;
+ } catch {
+ this.$toast.show(__('There was an error when reseting email token.'), { type: 'error' });
+ }
+ },
+ cancelHandler() {
+ this.$refs.modal.hide();
+ },
+ },
+ modalId: 'issuable-email-modal',
+};
+</script>
+
+<template>
+ <div>
+ <gl-button v-gl-modal="$options.modalId" variant="link" data-testid="issuable-email-modal-btn"
+ ><gl-sprintf :message="__('Email a new %{name} to this project')"
+ ><template #name>{{ issuableName }}</template></gl-sprintf
+ ></gl-button
+ >
+ <gl-modal ref="modal" :modal-id="$options.modalId">
+ <template #modal-title>
+ <gl-sprintf :message="__('Create new %{name} by email')">
+ <template #name>{{ issuableName }}</template>
+ </gl-sprintf>
+ </template>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'You can create a new %{name} inside this project by sending an email to the following email address:',
+ )
+ "
+ >
+ <template #name>{{ issuableName }}</template>
+ </gl-sprintf>
+ </p>
+ <gl-form-input-group :value="email" readonly select-on-click class="gl-mb-4">
+ <template #append>
+ <modal-copy-button :text="email" :title="__('Copy')" :modal-id="$options.modalId" />
+ <gl-button
+ v-gl-tooltip.hover
+ :href="mailToLink"
+ :title="__('Send email')"
+ icon="mail"
+ data-testid="mail-to-btn"
+ />
+ </template>
+ </gl-form-input-group>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported.',
+ )
+ "
+ >
+ <template #quickActionsLink="{ content }">
+ <gl-link :href="quickActionsHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #markdownLink="{ content }">
+ <gl-link :href="markdownHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens.',
+ )
+ "
+ >
+ <template #helpIcon>
+ <gl-link :href="emailsHelpPagePath" target="_blank"
+ ><gl-icon class="gl-text-blue-600" name="question-o"
+ /></gl-link>
+ </template>
+ <template #resetLink="{ content }">
+ <gl-button
+ variant="link"
+ data-testid="incoming-email-token-reset"
+ @click="resetIncomingEmailToken"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ </p>
+ <template #modal-footer>
+ <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/init_issuable_by_email.js b/app/assets/javascripts/issuable/init_issuable_by_email.js
new file mode 100644
index 00000000000..1fed55c3d8e
--- /dev/null
+++ b/app/assets/javascripts/issuable/init_issuable_by_email.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
+import IssuableByEmail from './components/issuable_by_email.vue';
+
+Vue.use(GlToast);
+
+export default () => {
+ const el = document.querySelector('.js-issueable-by-email');
+
+ if (!el) return null;
+
+ const {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ },
+ render(h) {
+ return h(IssuableByEmail);
+ },
+ });
+};
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index 4f31d26ab5d..4856f9781ce 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,35 +1,7 @@
-import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from './flash';
-import { s__, __ } from './locale';
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix) {
issuableInitBulkUpdateSidebar.init(pagePrefix);
- IssuableIndex.resetIncomingEmailToken();
- }
-
- static resetIncomingEmailToken() {
- const $resetToken = $('.incoming-email-token-reset');
-
- $resetToken.on('click', (e) => {
- e.preventDefault();
-
- $resetToken.text(s__('EmailToken|resetting...'));
-
- axios
- .put($resetToken.attr('href'))
- .then(({ data }) => {
- $('#issuable_email').val(data.new_address).focus();
-
- $resetToken.text(s__('EmailToken|reset it'));
- })
- .catch(() => {
- flash(__('There was an error when reseting email token.'));
-
- $resetToken.text(s__('EmailToken|reset it'));
- });
- });
}
}
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 5956933fd99..25790a27888 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -9,6 +9,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
+import initIssuableByEmail from '~/issuable/init_issuable_by_email';
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -24,3 +25,4 @@ new UsersSelect();
initManualOrdering();
initIssuablesList();
+initIssuableByEmail();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index 94a12cc2706..e9138d6ab4d 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -6,6 +6,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initIssuableByEmail from '~/issuable/init_issuable_by_email';
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
@@ -19,3 +20,5 @@ initFilteredSearch({
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
+
+initIssuableByEmail();
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
index 0539d318471..ba03fccdb03 100644
--- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
+++ b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
@@ -1,3 +1,3 @@
import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
-document.addEventListener('DOMContentLoaded', () => initActivityCharts());
+initActivityCharts();
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 673c6f4a1eb..d1cfa84248c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -83,6 +83,9 @@ export default {
alertId: {
default: '',
},
+ isThreatMonitoringPage: {
+ default: false,
+ },
projectId: {
default: '',
},
@@ -364,7 +367,11 @@ export default {
</alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
+ <gl-tab
+ v-if="isThreatMonitoringPage"
+ :data-testid="$options.tabsConfig[1].id"
+ :title="$options.tabsConfig[1].title"
+ >
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
index 12c58b582c5..4584023380c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -19,6 +19,10 @@ export default {
projectId: {
default: '',
},
+ // TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717
+ isThreatMonitoringPage: {
+ default: false,
+ },
},
props: {
alert: {
@@ -62,6 +66,7 @@ export default {
@alert-error="$emit('alert-error', $event)"
/>
<sidebar-status
+ v-if="!isThreatMonitoringPage"
:project-path="projectPath"
:alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')"
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
index 56f79410064..2ab5160534c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/constants.js
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -9,11 +9,10 @@ export const SEVERITY_LEVELS = {
UNKNOWN: s__('severity|Unknown'),
};
-export const DEFAULT_PAGE = 'OPERATIONS';
-
/* eslint-disable @gitlab/require-i18n-strings */
export const PAGE_CONFIG = {
OPERATIONS: {
+ TITLE: 'OPERATIONS',
// Tracks snowplow event when user views alert details
TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
category: 'Alert Management',
@@ -26,4 +25,7 @@ export const PAGE_CONFIG = {
label: 'Status',
},
},
+ THREAT_MONITORING: {
+ TITLE: 'THREAT_MONITORING',
+ },
};
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 643d6b3a3fe..eba8adf416a 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -6,13 +6,13 @@ import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
import createRouter from './router';
-import { DEFAULT_PAGE, PAGE_CONFIG } from './constants';
+import { PAGE_CONFIG } from './constants';
Vue.use(VueApollo);
export default (selector) => {
const domEl = document.querySelector(selector);
- const { alertId, projectPath, projectIssuesPath, projectId, page = DEFAULT_PAGE } = domEl.dataset;
+ const { alertId, projectPath, projectIssuesPath, projectId, page } = domEl.dataset;
const router = createRouter();
const resolvers = {
@@ -52,16 +52,19 @@ export default (selector) => {
const provide = {
projectPath,
alertId,
+ page,
projectIssuesPath,
projectId,
};
- if (page === DEFAULT_PAGE) {
+ if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
page
];
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ } else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
+ provide.isThreatMonitoringPage = true;
}
// eslint-disable-next-line no-new
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 093cba3560f..8d34f35502e 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -5,7 +5,7 @@
$bs-input-focus-border: #80bdff;
$bs-input-focus-box-shadow: rgba(0, 123, 255, 0.25);
- a:not(.btn):not(.gl-tab-nav-item),
+ a:not(.btn),
.gl-button.btn-link,
.gl-button.btn-link:hover,
.gl-button.btn-link:focus,
@@ -34,6 +34,7 @@
.ide-pipeline .top-bar .controllers .controllers-buttons,
.controllers-buttons svg,
.nav-links li a.active,
+ .gl-tabs-nav li a.gl-tab-nav-item-active,
.md-area.is-focused {
color: var(--ide-text-color, $gl-text-color);
}
@@ -44,13 +45,15 @@
}
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
+ .gl-tabs-nav li a,
.dropdown-menu-inner-content,
.file-row .file-row-icon svg,
.file-row:hover .file-row-icon svg {
color: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
- .nav-links:not(.quick-links) li:not(.md-header-toolbar) {
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar),
+ .gl-tabs-nav li {
&:hover a,
&.active a,
a:hover,
@@ -61,6 +64,10 @@
border-color: var(--ide-input-border, $gray-darkest);
}
}
+
+ a.gl-tab-nav-item-active {
+ box-shadow: inset 0 -2px 0 0 var(--ide-input-border, $gray-darkest);
+ }
}
.drag-handle:hover {
@@ -142,6 +149,7 @@
.md table:not(.code) tbody td,
.md table:not(.code) tr th,
.nav-links:not(.quick-links),
+ .gl-tabs-nav,
.common-note-form .md-area.is-focused .nav-links {
border-color: var(--ide-border-color-alt, $white-dark);
}
@@ -175,6 +183,10 @@
border-color: var(--ide-highlight-accent, $gl-text-color);
}
+ .gl-tabs-nav li a.gl-tab-nav-item-active {
+ box-shadow: inset 0 -2px 0 0 var(--ide-highlight-accent, $gl-text-color);
+ }
+
// for other themes, suppress different avatar default colors for simplicity
.avatar-container {
&,
@@ -304,6 +316,11 @@
border-color: var(--ide-dropdown-hover-background, $border-color);
}
+ .gl-tabs-nav {
+ background-color: var(--ide-dropdown-hover-background, $white);
+ box-shadow: inset 0 -2px 0 0 var(--ide-dropdown-hover-background, $border-color);
+ }
+
.divider {
background-color: var(--ide-dropdown-hover-background, $gray-100);
border-color: var(--ide-dropdown-hover-background, $gray-100);
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index b5b34c0a64e..7c4d51ab677 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -97,7 +97,8 @@ $ide-commit-header-height: 48px;
border-right: 1px solid var(--ide-border-color, $white-dark);
border-bottom: 1px solid var(--ide-border-color, $white-dark);
- &.active {
+ &.active,
+ .gl-tab-nav-item-active {
background-color: var(--ide-highlight-background, $white);
border-bottom-color: transparent;
}
@@ -114,6 +115,42 @@ $ide-commit-header-height: 48px;
}
}
}
+
+ .gl-tab-content {
+ padding: 0;
+ }
+
+ .gl-tabs-nav {
+ border-width: 0;
+
+ li {
+ padding: 0 !important;
+ background: transparent !important;
+ border: 0 !important;
+
+ a {
+ display: flex;
+ align-items: center;
+ padding: $grid-size $gl-padding !important;
+ box-shadow: none !important;
+ font-weight: normal !important;
+
+ background-color: var(--ide-background-hover, $gray-normal);
+ border-right: 1px solid var(--ide-border-color, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color, $white-dark);
+
+ &.gl-tab-nav-item-active {
+ background-color: var(--ide-highlight-background, $white);
+ border-color: var(--ide-border-color, $white-dark);
+ border-bottom-color: transparent;
+ }
+
+ .multi-file-tab-close svg {
+ top: 0;
+ }
+ }
+ }
+ }
}
.multi-file-tab {
@@ -634,7 +671,8 @@ $ide-commit-header-height: 48px;
height: 100%;
}
- .nav-links {
+ .nav-links,
+ .gl-tabs-nav {
height: 30px;
}
@@ -976,17 +1014,25 @@ $ide-commit-header-height: 48px;
}
.ide-nav-form {
- .nav-links li {
+ .nav-links li,
+ .gl-tabs-nav li {
width: 50%;
padding-left: 0;
padding-right: 0;
a {
text-align: center;
+ font-size: 14px;
+ line-height: 30px;
- &:not(.active) {
+ &:not(.active),
+ &:not(.gl-tab-nav-item-active) {
background-color: var(--ide-dropdown-background, $gray-light);
}
+
+ &.gl-tab-nav-item-active {
+ font-weight: bold;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index fb483ade107..2a8a86615f6 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -108,18 +108,6 @@ ul.related-merge-requests > li {
}
}
-.issuable-email-modal-btn {
- padding: 0;
- color: $blue-600;
- background-color: transparent;
- border: 0;
- outline: 0;
-
- &:hover {
- text-decoration: underline;
- }
-}
-
.email-modal-input-group {
margin-bottom: 10px;
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index e6603237676..52b8ac915f1 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -178,8 +178,8 @@ module EventsHelper
def event_note_target_url(event)
if event.commit_note?
project_commit_url(event.project, event.note_target, anchor: dom_id(event.target))
- elsif event.project_snippet_note?
- project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target))
+ elsif event.snippet_note?
+ gitlab_snippet_url(event.note_target, anchor: dom_id(event.target))
elsif event.issue_note?
project_issue_url(event.project, id: event.note_target, anchor: dom_id(event.target))
elsif event.merge_request_note?
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index 5fad38bd32c..b705258f133 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -20,7 +20,8 @@ module Projects::AlertManagementHelper
'alert-id' => alert_id,
'project-path' => project.full_path,
'project-id' => project.id,
- 'project-issues-path' => project_issues_path(project)
+ 'project-issues-path' => project_issues_path(project),
+ 'page' => 'OPERATIONS'
}
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 671def16151..401dfc4cb02 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -294,10 +294,14 @@ class Event < ApplicationRecord
note? && target && target.for_merge_request?
end
- def project_snippet_note?
+ def snippet_note?
note? && target && target.for_snippet?
end
+ def project_snippet_note?
+ note? && target && target.for_project_snippet?
+ end
+
def personal_snippet_note?
note? && target && target.for_personal_snippet?
end
diff --git a/app/models/note.rb b/app/models/note.rb
index d169d88149b..fdc972d9726 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -259,6 +259,10 @@ class Note < ApplicationRecord
noteable_type == 'AlertManagement::Alert'
end
+ def for_project_snippet?
+ noteable.is_a?(ProjectSnippet)
+ end
+
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 6034389b897..729e966e48a 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{current_user.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index 747c53b440e..85709266548 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "Activity"
xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 406e8a93194..17bf43a4590 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
return unless event.visible_to_user?(current_user)
event = event.present
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 2fd96c9d158..6f29a4e439c 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{@group.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index 0f67b15c301..5610746ad01 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "#{@group.name} activity"
xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 94c32df7c60..e2ab360a3e4 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.entry do
xml.id project_issue_url(issue.project, issue)
xml.link href: project_issue_url(issue.project, issue)
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
index 94c3099ace2..c21c4dac9f0 100644
--- a/app/views/issues/_issues_calendar.ics.ruby
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
cal = Icalendar::Calendar.new
cal.prodid = '-//GitLab//NONSGML GitLab//EN'
cal.x_wr_calname = 'GitLab Issues'
diff --git a/app/views/layouts/xml.atom.builder b/app/views/layouts/xml.atom.builder
index 4ee09cb87a1..7144b6305a2 100644
--- a/app/views/layouts/xml.atom.builder
+++ b/app/views/layouts/xml.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.feed 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:media' => 'http://search.yahoo.com/mrss/' do
xml << yield
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
deleted file mode 100644
index c11ee765cca..00000000000
--- a/app/views/projects/_issuable_by_email.html.haml
+++ /dev/null
@@ -1,49 +0,0 @@
-- name = issuable_type == 'issue' ? 'issue' : 'merge request'
-
-.issuable-footer.text-center
- %button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
- Email a new #{name} to this project
-
-#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
- .modal-dialog{ role: "document" }
- .modal-content
- .modal-header
- %h4.modal-title
- Create new #{name} by email
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- %p
- You can create a new #{name} inside this project by sending an email to the following email address:
- .email-modal-input-group.input-group
- = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
-
- - if issuable_type == 'issue'
- - enter_title_text = _('Enter the issue title')
- - enter_description_text = _('Enter the issue description')
- - else
- - enter_title_text = _('Enter the merge request title')
- - enter_description_text = _('Enter the merge request description')
- = mail_to email, class: 'btn btn-clipboard btn-transparent',
- subject: enter_title_text,
- body: enter_description_text,
- title: _('Send email'),
- data: { toggle: 'tooltip', placement: 'bottom' } do
- = sprite_icon('mail')
-
- %p
- = render 'by_email_description'
- %p
- This is a private email address
- %span<
- = link_to help_page_path('development/emails', anchor: 'email-namespace'), target: '_blank', rel: 'noopener', aria: { label: 'Learn more about incoming email addresses' } do
- = sprite_icon('question-o')
-
- generated just for you.
-
- Anyone who gets ahold of it can create issues or merge requests as if they were you.
- You should
- = link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
- if that ever happens.
diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder
index 640b5ecf99e..8a27649af50 100644
--- a/app/views/projects/commits/_commit.atom.builder
+++ b/app/views/projects/commits/_commit.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.entry do
xml.id project_commit_url(@project, id: commit.id)
xml.link href: project_commit_url(@project, id: commit.id)
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index a9b77631474..a4db3c47f59 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "#{@project.name}:#{@ref} commits"
xml.link href: project_commits_url(@project, @ref, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: project_commits_url(@project, @ref), rel: "alternate", type: "text/html"
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 6566866be82..4de9c0ed34b 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{@project.name} issues"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 842b3432991..dd66e00b813 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -3,6 +3,7 @@
- page_title _("Issues")
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- add_page_specific_style 'page_bundles/issues_list'
+- issuable_type = 'issue'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
@@ -24,7 +25,8 @@
.issues-holder
= render 'issues'
- if new_issue_email
- = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
+ .issuable-footer.text-center
+ .js-issueable-by-email{ data: { initial_email: new_issue_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
- new_project_issue_button_path = @project.archived? ? false : new_project_issue_path(@project)
= render 'shared/empty_states/issues', new_project_issue_button_path: new_project_issue_button_path, show_import_button: true
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 36b1cf0796f..62a251c7015 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,6 +1,7 @@
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
+- issuable_type = 'merge_request'
- page_title _("Merge Requests")
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
@@ -21,6 +22,7 @@
.merge-requests-holder
= render 'merge_requests'
- if new_merge_request_email
- = render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
+ .issuable-footer.text-center
+ .js-issueable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 39f8cb9a0e0..95b49aa0406 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "#{@project.name} activity"
xml.link href: project_url(@project, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: project_url(@project), rel: "alternate", type: "text/html"
diff --git a/app/views/projects/tags/_tag.atom.builder b/app/views/projects/tags/_tag.atom.builder
index e4b2428d267..1d5b6832357 100644
--- a/app/views/projects/tags/_tag.atom.builder
+++ b/app/views/projects/tags/_tag.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
commit = @repository.commit(tag.dereferenced_target)
release = @releases.find { |r| r.tag == tag.name }
tag_url = project_tag_url(@project, tag.name)
diff --git a/app/views/projects/tags/index.atom.builder b/app/views/projects/tags/index.atom.builder
index b9b58b7beaa..68d51ebc89c 100644
--- a/app/views/projects/tags/index.atom.builder
+++ b/app/views/projects/tags/index.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "#{@project.name} tags"
xml.link href: project_tags_url(@project, @ref, rss_url_options), rel: 'self', type: 'application/atom+xml'
xml.link href: project_tags_url(@project, @ref), rel: 'alternate', type: 'text/html'
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index 288ac53a954..8aad4848aa2 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -5,6 +5,6 @@
= link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
.gl-text-gray-500.gl-my-3
- = sprintf(s_(' %{project_name}#%{issuable_iid} &middot; opened %{issuable_created} by %{author}'), { project_name: issuable.project.full_name, issuable_iid: issuable.iid, issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
+ = sprintf(s_(' %{project_name}#%{issuable_iid} &middot; opened %{issuable_created} by %{author} &middot; updated %{issuable_updated}'), { project_name: issuable.project.full_name, issuable_iid: issuable.iid, issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), issuable_updated: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
.description.term.col-sm-10.gl-px-0
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index e95814875f1..43e67347cd0 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.title "#{@user.name} activity"
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
diff --git a/bin/secpick b/bin/secpick
index 517465d3f5d..07bac9270c9 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
-# frozen_string_literal: false
+# frozen_string_literal: true
require 'active_support/core_ext/object/to_query'
require 'optparse'
@@ -9,12 +9,12 @@ require 'rainbow/refinement'
using Rainbow
module Secpick
- BRANCH_PREFIX = 'security'.freeze
- STABLE_SUFFIX = 'stable'.freeze
+ BRANCH_PREFIX = 'security'
+ STABLE_SUFFIX = 'stable'
- DEFAULT_REMOTE = 'security'.freeze
+ DEFAULT_REMOTE = 'security'
- SECURITY_MR_URL = 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/new'.freeze
+ SECURITY_MR_URL = 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/new'
class SecurityFix
def initialize
@@ -27,12 +27,12 @@ module Secpick
def source_branch
branch = "#{@options[:branch]}-#{@options[:version]}"
- branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-")
- branch.freeze
+ branch = "#{BRANCH_PREFIX}-#{branch}" unless branch.start_with?("#{BRANCH_PREFIX}-")
+ branch
end
def stable_branch
- "#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze
+ "#{@options[:version]}-#{STABLE_SUFFIX}-ee"
end
def git_commands
diff --git a/changelogs/unreleased/254256-replace-bootstrap-modal-in-app-views-projects-_issuable_by_email-h.yml b/changelogs/unreleased/254256-replace-bootstrap-modal-in-app-views-projects-_issuable_by_email-h.yml
new file mode 100644
index 00000000000..f544eda8eb0
--- /dev/null
+++ b/changelogs/unreleased/254256-replace-bootstrap-modal-in-app-views-projects-_issuable_by_email-h.yml
@@ -0,0 +1,5 @@
+---
+title: Replace bootstrap modal in issuable_by_email HAML template
+merge_request: 53599
+author:
+type: changed
diff --git a/changelogs/unreleased/273574-fix-bad-data-in-projects-has_external_issue_tracker.yml b/changelogs/unreleased/273574-fix-bad-data-in-projects-has_external_issue_tracker.yml
new file mode 100644
index 00000000000..74bd2ee5f79
--- /dev/null
+++ b/changelogs/unreleased/273574-fix-bad-data-in-projects-has_external_issue_tracker.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup incorrect data in projects.has_external_wiki
+merge_request: 53790
+author:
+type: fixed
diff --git a/changelogs/unreleased/295311-add-updated-at-to-search-results.yml b/changelogs/unreleased/295311-add-updated-at-to-search-results.yml
new file mode 100644
index 00000000000..b43e37e0481
--- /dev/null
+++ b/changelogs/unreleased/295311-add-updated-at-to-search-results.yml
@@ -0,0 +1,5 @@
+---
+title: Add updated_at output to search results
+merge_request: 53958
+author:
+type: changed
diff --git a/changelogs/unreleased/300220-add-security-orchestration-policy-configuration-model.yml b/changelogs/unreleased/300220-add-security-orchestration-policy-configuration-model.yml
new file mode 100644
index 00000000000..6cd82ca949a
--- /dev/null
+++ b/changelogs/unreleased/300220-add-security-orchestration-policy-configuration-model.yml
@@ -0,0 +1,5 @@
+---
+title: Add Security Orchestration Policy Configuration
+merge_request: 53743
+author:
+type: added
diff --git a/changelogs/unreleased/300546-add-new-plans.yml b/changelogs/unreleased/300546-add-new-plans.yml
new file mode 100644
index 00000000000..156302c05a2
--- /dev/null
+++ b/changelogs/unreleased/300546-add-new-plans.yml
@@ -0,0 +1,5 @@
+---
+title: Migration to add new Premium and Ultimate plan records
+merge_request: 53465
+author:
+type: added
diff --git a/changelogs/unreleased/fj-fix-bug-rendering-snippet-activity.yml b/changelogs/unreleased/fj-fix-bug-rendering-snippet-activity.yml
new file mode 100644
index 00000000000..53e7633953d
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-bug-rendering-snippet-activity.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug rendering snippet activity
+merge_request: 53993
+author:
+type: fixed
diff --git a/danger/changes_size/Dangerfile b/danger/changes_size/Dangerfile
index 2c1d59427af..f37632ced33 100644
--- a/danger/changes_size/Dangerfile
+++ b/danger/changes_size/Dangerfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# FIXME: git.info_for_file raises the following error
# /usr/local/bundle/gems/git-1.4.0/lib/git/lib.rb:956:in `command': (Danger::DSLError)
# [!] Invalid `Dangerfile` file:
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
index d46750f5bc2..d74c4f4a5af 100644
--- a/danger/metadata/Dangerfile
+++ b/danger/metadata/Dangerfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop:disable Style/SignalException
THROUGHPUT_LABELS = [
diff --git a/db/migrate/20210209160510_create_security_orchestration_policy_configurations.rb b/db/migrate/20210209160510_create_security_orchestration_policy_configurations.rb
new file mode 100644
index 00000000000..896593c803f
--- /dev/null
+++ b/db/migrate/20210209160510_create_security_orchestration_policy_configurations.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateSecurityOrchestrationPolicyConfigurations < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_PREFIX = 'index_sop_configs_'
+
+ def up
+ table_comment = { owner: 'group::container security', description: 'Configuration used to store relationship between project and security policy repository' }
+
+ create_table_with_constraints :security_orchestration_policy_configurations, comment: table_comment.to_json do |t|
+ t.references :project, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }, index: { name: INDEX_PREFIX + 'on_project_id', unique: true }
+ t.references :security_policy_management_project, null: false, foreign_key: { to_table: :projects, on_delete: :restrict }, index: { name: INDEX_PREFIX + 'on_security_policy_management_project_id', unique: true }
+
+ t.timestamps_with_timezone
+ end
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :security_orchestration_policy_configurations, force: :cascade
+ end
+ end
+end
diff --git a/db/post_migrate/20210105030125_cleanup_projects_with_bad_has_external_wiki_data.rb b/db/post_migrate/20210105030125_cleanup_projects_with_bad_has_external_wiki_data.rb
new file mode 100644
index 00000000000..73725062bb3
--- /dev/null
+++ b/db/post_migrate/20210105030125_cleanup_projects_with_bad_has_external_wiki_data.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+class CleanupProjectsWithBadHasExternalWikiData < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX_NAME = 'tmp_index_projects_on_id_where_has_external_wiki_is_true'.freeze
+ BATCH_SIZE = 100
+
+ disable_ddl_transaction!
+
+ class Service < ActiveRecord::Base
+ include EachBatch
+ belongs_to :project
+
+ self.table_name = 'services'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ update_projects_with_active_external_wikis
+ update_projects_without_active_external_wikis
+ end
+
+ def down
+ # no-op : can't go back to incorrect data
+ end
+
+ private
+
+ def update_projects_with_active_external_wikis
+ # 11 projects are scoped in this query on GitLab.com.
+ scope = Service.where(active: true, type: 'ExternalWikiService').where.not(project_id: nil)
+
+ scope.each_batch(of: BATCH_SIZE) do |relation|
+ scope_with_projects = relation
+ .joins(:project)
+ .select('project_id')
+ .merge(Project.where(has_external_wiki: false).where(pending_delete: false).where(archived: false))
+
+ execute(<<~SQL)
+ WITH project_ids_to_update (id) AS (
+ #{scope_with_projects.to_sql}
+ )
+ UPDATE projects SET has_external_wiki = true WHERE id IN (SELECT id FROM project_ids_to_update)
+ SQL
+ end
+ end
+
+ def update_projects_without_active_external_wikis
+ # Add a temporary index to speed up the scoping of projects.
+ index_where = <<~SQL
+ (
+ "projects"."has_external_wiki" = TRUE
+ )
+ AND "projects"."pending_delete" = FALSE
+ AND "projects"."archived" = FALSE
+ SQL
+
+ add_concurrent_index(:projects, :id, where: index_where, name: TMP_INDEX_NAME)
+
+ services_sub_query = Service
+ .select('1')
+ .where('services.project_id = projects.id')
+ .where(type: 'ExternalWikiService')
+ .where(active: true)
+
+ # 322 projects are scoped in this query on GitLab.com.
+ Project.where(index_where).each_batch(of: BATCH_SIZE) do |relation|
+ relation_with_exists_query = relation.where('NOT EXISTS (?)', services_sub_query)
+ execute(<<~SQL)
+ WITH project_ids_to_update (id) AS (
+ #{relation_with_exists_query.select(:id).to_sql}
+ )
+ UPDATE projects SET has_external_wiki = false WHERE id IN (SELECT id FROM project_ids_to_update)
+ SQL
+ end
+
+ # Drop the temporary index.
+ remove_concurrent_index_by_name(:projects, TMP_INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20210205104425_add_new_post_eoa_plans.rb b/db/post_migrate/20210205104425_add_new_post_eoa_plans.rb
new file mode 100644
index 00000000000..d1a5afbd314
--- /dev/null
+++ b/db/post_migrate/20210205104425_add_new_post_eoa_plans.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddNewPostEoaPlans < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ execute "INSERT INTO plans (name, title, created_at, updated_at) VALUES ('premium', 'Premium (Formerly Silver)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
+ execute "INSERT INTO plans (name, title, created_at, updated_at) VALUES ('ultimate', 'Ultimate (Formerly Gold)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
+ end
+
+ def down
+ execute "DELETE FROM plans WHERE name IN ('premium', 'ultimate')"
+ end
+end
diff --git a/db/schema_migrations/20210105030125 b/db/schema_migrations/20210105030125
new file mode 100644
index 00000000000..d2495a23b63
--- /dev/null
+++ b/db/schema_migrations/20210105030125
@@ -0,0 +1 @@
+c5a780e5b5e62043fb04e77ebf89f7d04dfc9bfdc70df8d89c16a3f3fd960ea3 \ No newline at end of file
diff --git a/db/schema_migrations/20210205104425 b/db/schema_migrations/20210205104425
new file mode 100644
index 00000000000..c51f201b5a6
--- /dev/null
+++ b/db/schema_migrations/20210205104425
@@ -0,0 +1 @@
+4df2229fca7652cb836c867b621da91f1194899c50bb0a8be2fbae58f29dc788 \ No newline at end of file
diff --git a/db/schema_migrations/20210209160510 b/db/schema_migrations/20210209160510
new file mode 100644
index 00000000000..3aa90658d4e
--- /dev/null
+++ b/db/schema_migrations/20210209160510
@@ -0,0 +1 @@
+601d67a2911c461881064ec18a2246ef9e5b2835eb0fdf40e701c9360e19eca4 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3088c2d03aa..4691ff0fa62 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16971,6 +16971,25 @@ CREATE SEQUENCE security_findings_id_seq
ALTER SEQUENCE security_findings_id_seq OWNED BY security_findings.id;
+CREATE TABLE security_orchestration_policy_configurations (
+ id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ security_policy_management_project_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+COMMENT ON TABLE security_orchestration_policy_configurations IS '{"owner":"group::container security","description":"Configuration used to store relationship between project and security policy repository"}';
+
+CREATE SEQUENCE security_orchestration_policy_configurations_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE security_orchestration_policy_configurations_id_seq OWNED BY security_orchestration_policy_configurations.id;
+
CREATE TABLE security_scans (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -19289,6 +19308,8 @@ ALTER TABLE ONLY scim_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('s
ALTER TABLE ONLY security_findings ALTER COLUMN id SET DEFAULT nextval('security_findings_id_seq'::regclass);
+ALTER TABLE ONLY security_orchestration_policy_configurations ALTER COLUMN id SET DEFAULT nextval('security_orchestration_policy_configurations_id_seq'::regclass);
+
ALTER TABLE ONLY security_scans ALTER COLUMN id SET DEFAULT nextval('security_scans_id_seq'::regclass);
ALTER TABLE ONLY self_managed_prometheus_alert_events ALTER COLUMN id SET DEFAULT nextval('self_managed_prometheus_alert_events_id_seq'::regclass);
@@ -20794,6 +20815,9 @@ ALTER TABLE ONLY scim_oauth_access_tokens
ALTER TABLE ONLY security_findings
ADD CONSTRAINT security_findings_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY security_orchestration_policy_configurations
+ ADD CONSTRAINT security_orchestration_policy_configurations_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY security_scans
ADD CONSTRAINT security_scans_pkey PRIMARY KEY (id);
@@ -23353,6 +23377,10 @@ CREATE INDEX index_software_licenses_on_spdx_identifier ON software_licenses USI
CREATE UNIQUE INDEX index_software_licenses_on_unique_name ON software_licenses USING btree (name);
+CREATE UNIQUE INDEX index_sop_configs_on_project_id ON security_orchestration_policy_configurations USING btree (project_id);
+
+CREATE UNIQUE INDEX index_sop_configs_on_security_policy_management_project_id ON security_orchestration_policy_configurations USING btree (security_policy_management_project_id);
+
CREATE INDEX index_sprints_on_description_trigram ON sprints USING gin (description gin_trgm_ops);
CREATE INDEX index_sprints_on_due_date ON sprints USING btree (due_date);
@@ -24780,6 +24808,9 @@ ALTER TABLE ONLY ci_subscriptions_projects
ALTER TABLE ONLY trending_projects
ADD CONSTRAINT fk_rails_09feecd872 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY security_orchestration_policy_configurations
+ ADD CONSTRAINT fk_rails_0a22dcd52d FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY project_deploy_tokens
ADD CONSTRAINT fk_rails_0aca134388 FOREIGN KEY (deploy_token_id) REFERENCES deploy_tokens(id) ON DELETE CASCADE;
@@ -25119,6 +25150,9 @@ ALTER TABLE ONLY epic_issues
ALTER TABLE ONLY ci_refs
ADD CONSTRAINT fk_rails_4249db8cc3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY security_orchestration_policy_configurations
+ ADD CONSTRAINT fk_rails_42ed6c25ec FOREIGN KEY (security_policy_management_project_id) REFERENCES projects(id) ON DELETE RESTRICT;
+
ALTER TABLE ONLY ci_resources
ADD CONSTRAINT fk_rails_430336af2d FOREIGN KEY (resource_group_id) REFERENCES ci_resource_groups(id) ON DELETE CASCADE;
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 17ecb324417..5c36f1af960 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -93,8 +93,6 @@ which correspond to:
1. `elasticsearch_calls`: total number of calls to Elasticsearch
1. `elasticsearch_duration_s`: total time taken by Elasticsearch calls
-1. `elasticsearch_timed_out_count`: total number of calls to Elasticsearch that
- timed out and therefore returned partial results
ActionCable connection and subscription events are also logged to this file and they follow the same
format above. The `method`, `path`, and `format` fields are not applicable, and are always empty.
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 60234ed67bd..ff1443a4ae3 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -12,100 +12,110 @@ full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 10,000
-> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
+> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
+> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
-| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 |
-| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 |
-| PostgreSQL | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 |
-| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 |
-| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 |
-| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 |
-| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 |
-| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS |
-| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS |
-| Gitaly | 2 (minimum) | 16 vCPU, 60 GB memory | n1-standard-16 | `m5.4xlarge` | D16s v3 |
-| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 |
-| GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | `c5.9xlarge` | F32s v2 |
-| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 |
+| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
+| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
+| Gitaly Cluster | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
+| Praefect | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
+| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a |
-| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 |
-
-```mermaid
-stateDiagram-v2
- [*] --> LoadBalancer
- LoadBalancer --> ApplicationServer
-
- ApplicationServer --> BackgroundJobs
- ApplicationServer --> Gitaly
- ApplicationServer --> Redis_Cache
- ApplicationServer --> Redis_Queues
- ApplicationServer --> PgBouncer
- PgBouncer --> Database
- ApplicationServer --> ObjectStorage
- BackgroundJobs --> ObjectStorage
-
- ApplicationMonitoring -->ApplicationServer
- ApplicationMonitoring -->PgBouncer
- ApplicationMonitoring -->Database
- ApplicationMonitoring -->BackgroundJobs
-
- ApplicationServer --> Consul
-
- Consul --> Database
- Consul --> PgBouncer
- Redis_Cache --> Consul
- Redis_Queues --> Consul
- BackgroundJobs --> Consul
-
- state Consul {
- "Consul_1..3"
- }
-
- state Database {
- "PG_Primary_Node"
- "PG_Secondary_Node_1..2"
- }
-
- state Redis_Cache {
- "R_Cache_Primary_Node"
- "R_Cache_Replica_Node_1..2"
- "R_Cache_Sentinel_1..3"
- }
-
- state Redis_Queues {
- "R_Queues_Primary_Node"
- "R_Queues_Replica_Node_1..2"
- "R_Queues_Sentinel_1..3"
- }
-
- state Gitaly {
- "Gitaly_1..2"
- }
-
- state BackgroundJobs {
- "Sidekiq_1..4"
- }
-
- state ApplicationServer {
- "GitLab_Rails_1..3"
- }
-
- state LoadBalancer {
- "LoadBalancer_1"
- }
-
- state ApplicationMonitoring {
- "Prometheus"
- "Grafana"
- }
-
- state PgBouncer {
- "Internal_Load_Balancer"
- "PgBouncer_1..3"
- }
+| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+
+```plantuml
+@startuml 10k
+card "**External Load Balancer**" as elb #6a9be7
+card "**Internal Load Balancer**" as ilb #9370DB
+
+together {
+ collections "**GitLab Rails** x3" as gitlab #32CD32
+ collections "**Sidekiq** x4" as sidekiq #ff8dd1
+}
+
+together {
+ card "**Prometheus + Grafana**" as monitor #7FFFD4
+ collections "**Consul** x3" as consul #e76a9b
+}
+
+card "Gitaly Cluster" as gitaly_cluster {
+ collections "**Praefect** x3" as praefect #FF8C00
+ collections "**Gitaly** x3" as gitaly #FF8C00
+ card "**Praefect PostgreSQL***\n//Non fault-tolerant//" as praefect_postgres #FF8C00
+
+ praefect -[#FF8C00]-> gitaly
+ praefect -[#FF8C00]> praefect_postgres
+}
+
+card "Database" as database {
+ collections "**PGBouncer** x3" as pgbouncer #4EA7FF
+ card "**PostgreSQL** (Primary)" as postgres_primary #4EA7FF
+ collections "**PostgreSQL** (Secondary) x2" as postgres_secondary #4EA7FF
+
+ pgbouncer -[#4EA7FF]-> postgres_primary
+ postgres_primary .[#4EA7FF]> postgres_secondary
+}
+
+card "redis" as redis {
+ collections "**Redis Persistent** x3" as redis_persistent #FF6347
+ collections "**Redis Cache** x3" as redis_cache #FF6347
+ collections "**Redis Persistent Sentinel** x3" as redis_persistent_sentinel #FF6347
+ collections "**Redis Cache Sentinel** x3"as redis_cache_sentinel #FF6347
+
+ redis_persistent <.[#FF6347]- redis_persistent_sentinel
+ redis_cache <.[#FF6347]- redis_cache_sentinel
+}
+
+cloud "**Object Storage**" as object_storage #white
+
+elb -[#6a9be7]-> gitlab
+elb -[#6a9be7]--> monitor
+
+gitlab -[#32CD32]> sidekiq
+gitlab -[#32CD32]--> ilb
+gitlab -[#32CD32]-> object_storage
+gitlab -[#32CD32]---> redis
+gitlab -[hidden]-> monitor
+gitlab -[hidden]-> consul
+
+sidekiq -[#ff8dd1]--> ilb
+sidekiq -[#ff8dd1]-> object_storage
+sidekiq -[#ff8dd1]---> redis
+sidekiq -[hidden]-> monitor
+sidekiq -[hidden]-> consul
+
+ilb -[#9370DB]-> gitaly_cluster
+ilb -[#9370DB]-> database
+
+consul .[#e76a9b]u-> gitlab
+consul .[#e76a9b]u-> sidekiq
+consul .[#e76a9b]> monitor
+consul .[#e76a9b]-> database
+consul .[#e76a9b]-> gitaly_cluster
+consul .[#e76a9b,norank]--> redis
+
+monitor .[#7FFFD4]u-> gitlab
+monitor .[#7FFFD4]u-> sidekiq
+monitor .[#7FFFD4]> consul
+monitor .[#7FFFD4]-> database
+monitor .[#7FFFD4]-> gitaly_cluster
+monitor .[#7FFFD4,norank]--> redis
+monitor .[#7FFFD4]> ilb
+monitor .[#7FFFD4,norank]u--> elb
+
+@enduml
```
The Google Cloud Platform (GCP) architectures were built and tested using the
@@ -120,19 +130,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object-
is recommended instead of using NFS. Using an object storage service also
doesn't require you to provision and maintain a node.
+It's also worth noting that at this time [Praefect requires its own database server](../gitaly/praefect.md#postgresql) and
+that to achieve full High Availability a third party PostgreSQL database solution will be required.
+We hope to offer a built in solutions for these restrictions in the future but in the meantime a non HA PostgreSQL server
+can be set up via Omnibus GitLab, which the above specs reflect. Refer to the following issues for more information: [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919) & [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398)
+
## Setup components
To set up GitLab and its components to accommodate up to 10,000 users:
-1. [Configure the external load balancing node](#configure-the-external-load-balancer)
+1. [Configure the external load balancer](#configure-the-external-load-balancer)
to handle the load balancing of the GitLab application services nodes.
+1. [Configure the internal load balancer](#configure-the-internal-load-balancer).
+ to handle the load balancing of GitLab application internal connections.
1. [Configure Consul](#configure-consul).
1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab.
1. [Configure PgBouncer](#configure-pgbouncer).
-1. [Configure the internal load balancing node](#configure-the-internal-load-balancer).
1. [Configure Redis](#configure-redis).
-1. [Configure Gitaly](#configure-gitaly),
- which provides access to the Git repositories.
+1. [Configure Gitaly Cluster](#configure-gitaly-cluster),
+ provides access to the Git repositories.
1. [Configure Sidekiq](#configure-sidekiq).
1. [Configure the main GitLab Rails application](#configure-gitlab-rails)
to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend
@@ -178,6 +194,11 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.83`: Sentinel - Queues 3
- `10.6.0.91`: Gitaly 1
- `10.6.0.92`: Gitaly 2
+- `10.6.0.93`: Gitaly 3
+- `10.6.0.131`: Praefect 1
+- `10.6.0.132`: Praefect 2
+- `10.6.0.133`: Praefect 3
+- `10.6.0.141`: Praefect PostgreSQL 1 (non HA)
- `10.6.0.101`: Sidekiq 1
- `10.6.0.102`: Sidekiq 2
- `10.6.0.103`: Sidekiq 3
@@ -308,6 +329,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
</a>
</div>
+## Configure the internal load balancer
+
+The Internal Load Balancer is used to balance any internal connections the GitLab environment requires
+such as connections to [PgBouncer](#configure-pgbouncer) and [Praefect](#configure-praefect) (Gitaly Cluster).
+
+Note that it's a separate node from the External Load Balancer and shouldn't have any access externally.
+
+The following IP will be used as an example:
+
+- `10.6.0.40`: Internal Load Balancer
+
+Here's how you could do it with [HAProxy](https://www.haproxy.org/):
+
+```plaintext
+global
+ log /dev/log local0
+ log localhost local1 notice
+ log stdout format raw local0
+
+defaults
+ log global
+ default-server inter 10s fall 3 rise 2
+ balance leastconn
+
+frontend internal-pgbouncer-tcp-in
+ bind *:6432
+ mode tcp
+ option tcplog
+
+ default_backend pgbouncer
+
+frontend internal-praefect-tcp-in
+ bind *:2305
+ mode tcp
+ option tcplog
+ option clitcpka
+
+ default_backend praefect
+
+backend pgbouncer
+ mode tcp
+ option tcp-check
+
+ server pgbouncer1 10.6.0.21:6432 check
+ server pgbouncer2 10.6.0.22:6432 check
+ server pgbouncer3 10.6.0.23:6432 check
+
+backend praefect
+ mode tcp
+ option tcp-check
+ option srvtcpka
+
+ server praefect1 10.6.0.131:2305 check
+ server praefect2 10.6.0.132:2305 check
+ server praefect3 10.6.0.133:2305 check
+```
+
+Refer to your preferred Load Balancer's documentation for further guidance.
+
+<div align="right">
+ <a type="button" class="btn btn-default" href="#setup-components">
+ Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
+ </a>
+</div>
+
## Configure Consul
The following IPs will be used as an example:
@@ -662,52 +748,6 @@ The following IPs will be used as an example:
</a>
</div>
-### Configure the internal load balancer
-
-If you're running more than one PgBouncer node as recommended, then at this time you'll need to set
-up a TCP internal load balancer to serve each correctly.
-
-The following IP will be used as an example:
-
-- `10.6.0.40`: Internal Load Balancer
-
-Here's how you could do it with [HAProxy](https://www.haproxy.org/):
-
-```plaintext
-global
- log /dev/log local0
- log localhost local1 notice
- log stdout format raw local0
-
-defaults
- log global
- default-server inter 10s fall 3 rise 2
- balance leastconn
-
-frontend internal-pgbouncer-tcp-in
- bind *:6432
- mode tcp
- option tcplog
-
- default_backend pgbouncer
-
-backend pgbouncer
- mode tcp
- option tcp-check
-
- server pgbouncer1 10.6.0.21:6432 check
- server pgbouncer2 10.6.0.22:6432 check
- server pgbouncer3 10.6.0.23:6432 check
-```
-
-Refer to your preferred Load Balancer's documentation for further guidance.
-
-<div align="right">
- <a type="button" class="btn btn-default" href="#setup-components">
- Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
- </a>
-</div>
-
## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
@@ -1302,19 +1342,283 @@ To configure the Sentinel Queues server:
</a>
</div>
-## Configure Gitaly
+## Configure Gitaly Cluster
-NOTE:
-[Gitaly Cluster](../gitaly/praefect.md) support
-for the Reference Architectures is being
-worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
-some Architecture specs will likely change as a result to support the new
-and improved designed.
+[Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
+In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
+
+The recommended cluster setup includes the following components:
+
+- 3 Gitaly nodes: Replicated storage of Git repositories.
+- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
+- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
+ is required for Praefect database connections to be made highly available.
+- 1 load balancer: A load balancer is required for Praefect. The
+ [internal load balancer](#configure-the-internal-load-balancer) will be used.
+
+This section will detail how to configure the recommended standard setup in order.
+For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
+
+### Configure Praefect PostgreSQL
+
+Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
+
+If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
+A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
+
+#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
+
+The following IPs will be used as an example:
+
+- `10.6.0.141`: Praefect PostgreSQL
+
+First, make sure to [install](https://about.gitlab.com/install/)
+the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
+install the necessary dependencies from step 1, and add the
+GitLab package repository from step 2. When installing GitLab
+in the second step, do not supply the `EXTERNAL_URL` value.
+
+1. SSH in to the Praefect PostgreSQL node.
+1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as `<praefect_postgresql_password>`.
+1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
+ username of `praefect` (recommended). The command will request the password `<praefect_postgresql_password>`
+ and confirmation. Use the value that is output by this command in the next
+ step as the value of `<praefect_postgresql_password_hash>`:
+
+ ```shell
+ sudo gitlab-ctl pg-password-md5 praefect
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+
+ ```ruby
+ # Disable all components except PostgreSQL and Consul
+ roles ['postgres_role']
+ repmgr['enable'] = false
+ patroni['enable'] = false
+
+ # PostgreSQL configuration
+ postgresql['listen_address'] = '0.0.0.0'
+ postgresql['max_connections'] = 200
+
+ gitlab_rails['auto_migrate'] = false
+
+ # Configure the Consul agent
+ consul['enable'] = true
+ ## Enable service discovery for Prometheus
+ consul['monitoring_service_discovery'] = true
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ #
+ # Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
+ postgresql['sql_user_password'] = "<praefect_postgresql_password_hash>"
+
+ # Replace XXX.XXX.XXX.XXX/YY with Network Address
+ postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
+
+ # Set the network addresses that the exporters will listen on for monitoring
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ postgres_exporter['listen_address'] = '0.0.0.0:9187'
+
+ ## The IPs of the Consul server nodes
+ ## You can also use FQDNs and intermix them with IPs
+ consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
+ }
+ #
+ # END user configuration
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+1. Follow the [post configuration](#praefect-postgresql-post-configuration).
+
+<div align="right">
+ <a type="button" class="btn btn-default" href="#setup-components">
+ Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
+ </a>
+</div>
+
+#### Praefect HA PostgreSQL third-party solution
+
+[As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
+Praefect's database is recommended if aiming for full High Availability.
+
+There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
+
+- A static IP for all connections that doesn't change on failover.
+- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
+
+Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
+
+Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
+
+#### Praefect PostgreSQL post-configuration
+
+After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
+
+We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
+The password for the user is the same as the one you configured earlier as `<praefect_postgresql_password>`.
+
+This is how this would work with a Omnibus GitLab PostgreSQL setup:
+
+1. SSH in to the Praefect PostgreSQL node.
+1. Connect to the PostgreSQL server with administrative access.
+ The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
+ The database `template1` is used because it is created by default on all PostgreSQL servers.
+
+ ```shell
+ /opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
+ ```
+
+1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
+
+ ```shell
+ CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
+ ```
+
+1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
+
+ ```shell
+ /opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
+ ```
+
+1. Create a new database `praefect_production`:
+
+ ```shell
+ CREATE DATABASE praefect_production WITH ENCODING=UTF8;
+ ```
+
+<div align="right">
+ <a type="button" class="btn btn-default" href="#setup-components">
+ Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
+ </a>
+</div>
+
+### Configure Praefect
+
+Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
+it. This section details how to configure it.
+
+Praefect requires several secret tokens to secure communications across the Cluster:
+
+- `<praefect_external_token>`: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
+- `<praefect_internal_token>`: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
+- `<praefect_postgresql_password>`: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
+
+Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
+the details of each Gitaly node that makes up the cluster. Each storage is also given a name
+and this name is used in several areas of the config. In this guide, the name of the storage will be
+`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
+to use Gitaly Cluster, you may need to use a different name.
+Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
+
+The following IPs will be used as an example:
+
+- `10.6.0.131`: Praefect 1
+- `10.6.0.132`: Praefect 2
+- `10.6.0.133`: Praefect 3
+
+To configure the Praefect nodes, on each one:
+
+1. SSH in to the Praefect server.
+1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
+ package of your choice. Be sure to follow _only_ installation steps 1 and 2
+ on the page.
+1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
+
+ ```ruby
+ # Avoid running unnecessary services on the Gitaly server
+ postgresql['enable'] = false
+ redis['enable'] = false
+ nginx['enable'] = false
+ puma['enable'] = false
+ unicorn['enable'] = false
+ sidekiq['enable'] = false
+ gitlab_workhorse['enable'] = false
+ grafana['enable'] = false
+
+ # If you run a separate monitoring node you can disable these services
+ alertmanager['enable'] = false
+ prometheus['enable'] = false
+
+ # Praefect Configuration
+ praefect['enable'] = true
+ praefect['listen_addr'] = '0.0.0.0:2305'
+
+ gitlab_rails['rake_cache_clear'] = false
+ gitlab_rails['auto_migrate'] = false
+
+ # Configure the Consul agent
+ consul['enable'] = true
+ ## Enable service discovery for Prometheus
+ consul['monitoring_service_discovery'] = true
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ #
+
+ # Praefect External Token
+ # This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
+ praefect['auth_token'] = '<praefect_external_token>'
+
+ # Praefect Database Settings
+ praefect['database_host'] = '10.6.0.141'
+ praefect['database_port'] = 5432
+ # `no_proxy` settings must always be a direct connection for caching
+ praefect['database_host_no_proxy'] = '10.6.0.141'
+ praefect['database_port_no_proxy'] = 5432
+ praefect['database_dbname'] = 'praefect_production'
+ praefect['database_user'] = 'praefect'
+ praefect['database_password'] = '<praefect_postgresql_password>'
+
+ # Praefect Virtual Storage config
+ # Name of storage hash must match storage name in git_data_dirs on GitLab
+ # server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
+ praefect['virtual_storages'] = {
+ 'default' => {
+ 'gitaly-1' => {
+ 'address' => 'tcp://10.6.0.91:8075',
+ 'token' => '<praefect_internal_token>',
+ 'primary' => true
+ },
+ 'gitaly-2' => {
+ 'address' => 'tcp://10.6.0.92:8075',
+ 'token' => '<praefect_internal_token>'
+ },
+ 'gitaly-3' => {
+ 'address' => 'tcp://10.6.0.93:8075',
+ 'token' => '<praefect_internal_token>'
+ },
+ }
+ }
+
+ # Set the network addresses that the exporters will listen on for monitoring
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
+
+ ## The IPs of the Consul server nodes
+ ## You can also use FQDNs and intermix them with IPs
+ consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
+ }
+ #
+ # END user configuration
+ ```
+
+ 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
+ then replace the file of the same name on this server. If that file isn't on
+ this server, add the file from your Consul server to this server.
+
+ 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+### Configure Gitaly
-[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
-specifically the number of projects and those projects' sizes. It's recommended
-that a Gitaly server node stores no more than 5 TB of data. Depending on your
-repository storage requirements, you may require additional Gitaly server nodes.
+The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
+requirements that are dependent on data, specifically the number of projects
+and those projects' sizes. It's recommended that a Gitaly Cluster stores
+no more than 5 TB of data on each node. Depending on your
+repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
@@ -1325,36 +1629,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly.
-Be sure to note the following items:
+Gitaly servers must not be exposed to the public internet, as Gitaly's network
+traffic is unencrypted by default. The use of a firewall is highly recommended
+to restrict access to the Gitaly server. Another option is to
+[use TLS](#gitaly-cluster-tls-support).
-- The GitLab Rails application shards repositories into
- [repository storage paths](../repository_storage_paths.md).
-- A Gitaly server can host one or more storage paths.
-- A GitLab server can use one or more Gitaly server nodes.
-- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
- clients.
-- Gitaly servers must not be exposed to the public internet, as Gitaly's network
- traffic is unencrypted by default. The use of a firewall is highly recommended
- to restrict access to the Gitaly server. Another option is to
- [use TLS](#gitaly-tls-support).
+For configuring Gitaly you should note the following:
-NOTE:
-The token referred to throughout the Gitaly documentation is an arbitrary
-password selected by the administrator. This token is unrelated to tokens
-created for the GitLab API or other similar web API tokens.
-
-This section describes how to configure two Gitaly servers, with the following
-IPs and domain names:
-
-- `10.6.0.91`: Gitaly 1 (`gitaly1.internal`)
-- `10.6.0.92`: Gitaly 2 (`gitaly2.internal`)
+- `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
+- `auth_token` should be the same as `praefect_internal_token`
-Assumptions about your servers include having the secret token be `gitalysecret`,
-and that your GitLab installation has three repository storages:
+The following IPs will be used as an example:
-- `default` on Gitaly 1
-- `storage1` on Gitaly 1
-- `storage2` on Gitaly 2
+- `10.6.0.91`: Gitaly 1
+- `10.6.0.92`: Gitaly 2
+- `10.6.0.93`: Gitaly 3
On each node:
@@ -1364,21 +1653,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token:
- <!--
- updates to following example must also be made at
- https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
- -->
-
```ruby
# /etc/gitlab/gitlab.rb
- # Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
- # to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
- # The following two values must be the same as their respective values
- # of the GitLab Rails application setup
- gitaly['auth_token'] = 'gitalysecret'
- gitlab_shell['secret_token'] = 'shellsecret'
-
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
@@ -1407,36 +1684,42 @@ On each node:
# firewalls to restrict access to this address/port.
# Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075"
+
+ # Gitaly Auth Token
+ # Should be the same as praefect_internal_token
+ gitaly['auth_token'] = '<praefect_internal_token>'
```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- - On `gitaly1.internal`:
+ - On Gitaly node 1:
```ruby
git_data_dirs({
- 'default' => {
- 'path' => '/var/opt/gitlab/git-data'
- },
- 'storage1' => {
- 'path' => '/mnt/gitlab/git-data'
- },
+ "gitaly-1" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
})
```
- - On `gitaly2.internal`:
+ - On Gitaly node 2:
```ruby
git_data_dirs({
- 'storage2' => {
- 'path' => '/mnt/gitlab/git-data'
- },
+ "gitaly-2" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
})
```
- <!--
- updates to following example must also be made at
- https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
- -->
+ - On Gitaly node 3:
+
+ ```ruby
+ git_data_dirs({
+ "gitaly-3" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
+ })
+ ```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
@@ -1444,34 +1727,44 @@ On each node:
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-### Gitaly TLS support
+### Gitaly Cluster TLS support
-Gitaly supports TLS encryption. To be able to communicate
-with a Gitaly instance that listens for secure connections you will need to use `tls://` URL
-scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
+Praefect supports TLS encryption. To communicate with a Praefect instance that listens
+for secure connections, you must:
-You will need to bring your own certificates as this isn't provided automatically.
-The certificate, or its certificate authority, must be installed on all Gitaly
-nodes (including the Gitaly node using the certificate) and on all client nodes
-that communicate with it following the procedure described in
-[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
+- Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
+ in the GitLab configuration.
+- Bring your own certificates because this isn't provided automatically. The certificate
+ corresponding to each Praefect server must be installed on that Praefect server.
-NOTE:
-The self-signed certificate must specify the address you use to access the
-Gitaly server. If you are addressing the Gitaly server by a hostname, you can
-either use the Common Name field for this, or add it as a Subject Alternative
-Name. If you are addressing the Gitaly server by its IP address, you must add it
-as a Subject Alternative Name to the certificate.
-[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
+Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
+and on all Praefect clients that communicate with it following the procedure described in
+[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
-It's possible to configure Gitaly servers with both an unencrypted listening
-address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
-at the same time. This allows you to do a gradual transition from unencrypted to
-encrypted traffic, if necessary.
+Note the following:
-To configure Gitaly with TLS:
+- The certificate must specify the address you use to access the Praefect server. If
+ addressing the Praefect server by:
-1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+ - Hostname, you can either use the Common Name field for this, or add it as a Subject
+ Alternative Name.
+ - IP address, you must add it as a Subject Alternative Name to the certificate.
+
+- You can configure Praefect servers with both an unencrypted listening address
+ `listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
+ This allows you to do a gradual transition from unencrypted to encrypted traffic, if
+ necessary.
+
+- The Internal Load Balancer will also access to the certificates and need to be configured
+ to allow for TLS passthrough.
+ Refer to the load balancers documentation on how to configure this.
+
+To configure Praefect with TLS:
+
+1. Create certificates for Praefect servers.
+
+1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
+ and certificate there:
```shell
sudo mkdir -p /etc/gitlab/ssl
@@ -1480,27 +1773,34 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem
```
-1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when
- calling into itself:
+1. Edit `/etc/gitlab/gitlab.rb` and add:
- ```shell
- sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/
+ ```ruby
+ praefect['tls_listen_addr'] = "0.0.0.0:3305"
+ praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
+ praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
```
-1. Edit `/etc/gitlab/gitlab.rb` and add:
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
- <!--
- updates to following example must also be made at
- https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
- -->
+1. On the Praefect clients (including each Gitaly server), copy the certificates,
+ or their certificate authority, into `/etc/gitlab/trusted-certs`:
- ```ruby
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
- gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
+ ```shell
+ sudo cp cert.pem /etc/gitlab/trusted-certs/
```
-1. Delete `gitaly['listen_addr']` to allow only encrypted connections.
+1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
+ `/etc/gitlab/gitlab.rb` as follows:
+
+ ```ruby
+ git_data_dirs({
+ "default" => {
+ "gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
+ "gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
+ }
+ })
+ ```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
@@ -1586,17 +1886,20 @@ To configure the Sidekiq nodes, on each one:
### Gitaly ###
#######################################
+ # git_data_dirs get configured for the Praefect virtual storage
+ # Address is Internal Load Balancer for Praefect
+ # Token is praefect_external_token
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
+ "default" => {
+ "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => '<praefect_external_token>'
+ }
})
- gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
#######################################
### Postgres ###
#######################################
- gitlab_rails['db_host'] = '10.6.0.20' # internal load balancer IP
+ gitlab_rails['db_host'] = '10.6.0.40' # internal load balancer IP
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = '<postgresql_user_password>'
gitlab_rails['db_adapter'] = 'postgresql'
@@ -1669,17 +1972,14 @@ On each node perform the following:
```ruby
external_url 'https://gitlab.example.com'
- # Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
- # to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
- # The following two values must be the same as their respective values
- # of the Gitaly setup
- gitlab_rails['gitaly_token'] = 'gitalysecret'
- gitlab_shell['secret_token'] = 'shellsecret'
-
+ # git_data_dirs get configured for the Praefect virtual storage
+ # Address is Interal Load Balancer for Praefect
+ # Token is praefect_external_token
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
- 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
+ "default" => {
+ "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => '<praefect_external_token>'
+ }
})
## Disable components that will not be on the GitLab application server
@@ -1739,14 +2039,15 @@ On each node perform the following:
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the
+1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
- 'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
- 'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' },
+ "default" => {
+ "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
+ "gitaly_token" => '<praefect_external_token>'
+ }
})
```
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index 8f09d9b469c..bed584e94bd 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -18,6 +18,7 @@ many organizations .
> - **Supported users (approximate):** 1,000
> - **High Availability:** No. For a highly-available environment, you can
> follow the [3K reference architecture](3k_users.md).
+> - **Test requests per second (RPS) rates:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
| Users | Configuration | GCP | AWS | Azure |
|--------------|-------------------------|----------------|-----------------|----------------|
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 66a9c4adbac..12519f47cce 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -13,7 +13,7 @@ full list of reference architectures, see
> - **Supported users (approximate):** 25,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
+> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 1d11f972c2a..353160bea9a 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -14,7 +14,7 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 2,000
> - **High Availability:** No. For a highly-available environment, you can
> follow the [3K reference architecture](3k_users.md).
-> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
+> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|------------------------------------------|--------|-------------------------|----------------|--------------|---------|
@@ -27,44 +27,32 @@ For a full list of reference architectures, see
| Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 |
-```mermaid
-stateDiagram-v2
- [*] --> LoadBalancer
- LoadBalancer --> ApplicationServer
+```plantuml
+@startuml 2k
+card "**External Load Balancer**" as elb #6a9be7
- ApplicationServer --> Gitaly
- ApplicationServer --> Redis
- ApplicationServer --> Database
- ApplicationServer --> ObjectStorage
+collections "**GitLab Rails** x3" as gitlab #32CD32
+card "**Prometheus + Grafana**" as monitor #7FFFD4
+card "**Gitaly**" as gitaly #FF8C00
+card "**PostgreSQL**" as postgres #4EA7FF
+card "**Redis**" as redis #FF6347
+cloud "**Object Storage**" as object_storage #white
- ApplicationMonitoring -->ApplicationServer
- ApplicationMonitoring -->Redis
- ApplicationMonitoring -->Database
+elb -[#6a9be7]-> gitlab
+elb -[#6a9be7]--> monitor
+gitlab -[#32CD32]--> gitaly
+gitlab -[#32CD32]--> postgres
+gitlab -[#32CD32]-> object_storage
+gitlab -[#32CD32]--> redis
- state Database {
- "PG_Node"
- }
- state Redis {
- "Redis_Node"
- }
+monitor .[#7FFFD4]u-> gitlab
+monitor .[#7FFFD4]-> gitaly
+monitor .[#7FFFD4]-> postgres
+monitor .[#7FFFD4,norank]--> redis
+monitor .[#7FFFD4,norank]u--> elb
- state Gitaly {
- "Gitaly"
- }
-
- state ApplicationServer {
- "AppServ_1..2"
- }
-
- state LoadBalancer {
- "LoadBalancer"
- }
-
- state ApplicationMonitoring {
- "Prometheus"
- "Grafana"
- }
+@enduml
```
The Google Cloud Platform (GCP) architectures were built and tested using the
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 0fc48657c05..d6344d0fa4e 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -21,7 +21,7 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 3,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS
+> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-----------------------|----------------|-------------|---------|
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 7eb56f62774..cd23d1d82c6 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -13,7 +13,7 @@ full list of reference architectures, see
> - **Supported users (approximate):** 50,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
+> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index d617920e2e4..e00456b9be1 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -20,7 +20,7 @@ costly-to-operate environment by using the
> - **Supported users (approximate):** 5,000
> - **High Availability:** Yes
-> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
+> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|----------------|-------------|----------|
diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md
index 3be7b350469..b149cbc6e9d 100644
--- a/doc/administration/reference_architectures/index.md
+++ b/doc/administration/reference_architectures/index.md
@@ -29,7 +29,8 @@ per 1,000 users:
- API: 20 RPS
- Web: 2 RPS
-- Git: 2 RPS
+- Git (Pull): 2 RPS
+- Git (Push): 0.4 RPS (rounded to nearest integer)
For GitLab instances with less than 2,000 users, it's recommended that you use
the [default setup](#automated-backups) by
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 82b947746de..9f79d3cc675 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -173,7 +173,7 @@ delete the files:
sudo gitlab-rake gitlab:cleanup:orphan_job_artifact_files DRY_RUN=false
```
-You can also limit the number of files to delete with `LIMIT`:
+You can also limit the number of files to delete with `LIMIT` (default `100`):
```shell
sudo gitlab-rake gitlab:cleanup:orphan_job_artifact_files LIMIT=100
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 60fb9a7d6ea..f988d825c9f 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -534,14 +534,16 @@ limiting responses](#rate-limiting-responses).
The following table describes the rate limits for GitLab.com, both before and
after the limits change in January, 2021:
-| Rate limit | Before 2021-01-18 | From 2021-01-18 |
-|:--------------------------------------------------------------------------|:----------------------------|:------------------------------|
-| **Protected paths** (for a given **IP address**) | **10** requests per minute | **10** requests per minute |
-| **Raw endpoint** traffic (for a given **project, commit, and file path**) | **300** requests per minute | **300** requests per minute |
-| **Unauthenticated** traffic (from a given **IP address**) | No specific limit | **500** requests per minute |
-| **Authenticated** API traffic (for a given **user**) | No specific limit | **2,000** requests per minute |
-| **Authenticated** non-API HTTP traffic (for a given **user**) | No specific limit | **1,000** requests per minute |
-| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute |
+| Rate limit | Before 2021-01-18 | From 2021-01-18 | From 2021-02-12 |
+|:--------------------------------------------------------------------------|:----------------------------|:------------------------------|:------------------------------|
+| **Protected paths** (for a given **IP address**) | **10** requests per minute | **10** requests per minute | **10** requests per minute |
+| **Raw endpoint** traffic (for a given **project, commit, and file path**) | **300** requests per minute | **300** requests per minute | **300** requests per minute |
+| **Unauthenticated** traffic (from a given **IP address**) | No specific limit | **500** requests per minute | **500** requests per minute |
+| **Authenticated** API traffic (for a given **user**) | No specific limit | **2,000** requests per minute | **2,000** requests per minute |
+| **Authenticated** non-API HTTP traffic (for a given **user**) | No specific limit | **1,000** requests per minute | **1,000** requests per minute |
+| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute |
+| **Issue creation** | | **300** requests per minute | **300** requests per minute |
+| **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute |
More details are available on the rate limits for [protected
paths](#protected-paths-throttle) and [raw
diff --git a/lib/gitlab/instrumentation/elasticsearch_transport.rb b/lib/gitlab/instrumentation/elasticsearch_transport.rb
index a1c5bb32fb5..56179eda22d 100644
--- a/lib/gitlab/instrumentation/elasticsearch_transport.rb
+++ b/lib/gitlab/instrumentation/elasticsearch_transport.rb
@@ -9,15 +9,12 @@ module Gitlab
start = Time.now
headers = (headers || {})
.reverse_merge({ 'X-Opaque-Id': Labkit::Correlation::CorrelationId.current_or_new_id })
- response = super
+ super
ensure
if ::Gitlab::SafeRequestStore.active?
duration = (Time.now - start)
::Gitlab::Instrumentation::ElasticsearchTransport.increment_request_count
-
- ::Gitlab::Instrumentation::ElasticsearchTransport.increment_timed_out_count if response&.body&.dig('timed_out')
-
::Gitlab::Instrumentation::ElasticsearchTransport.add_duration(duration)
::Gitlab::Instrumentation::ElasticsearchTransport.add_call_details(duration, method, path, params, body)
end
@@ -28,7 +25,6 @@ module Gitlab
ELASTICSEARCH_REQUEST_COUNT = :elasticsearch_request_count
ELASTICSEARCH_CALL_DURATION = :elasticsearch_call_duration
ELASTICSEARCH_CALL_DETAILS = :elasticsearch_call_details
- ELASTICSEARCH_TIMED_OUT_COUNT = :elasticsearch_timed_out_count
def self.get_request_count
::Gitlab::SafeRequestStore[ELASTICSEARCH_REQUEST_COUNT] || 0
@@ -53,15 +49,6 @@ module Gitlab
::Gitlab::SafeRequestStore[ELASTICSEARCH_CALL_DURATION] += duration
end
- def self.increment_timed_out_count
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] ||= 0
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] += 1
- end
-
- def self.get_timed_out_count
- ::Gitlab::SafeRequestStore[ELASTICSEARCH_TIMED_OUT_COUNT] || 0
- end
-
def self.add_call_details(duration, method, path, params, body)
return unless Gitlab::PerformanceBar.enabled_for_request?
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 61de6b02453..0d1831ebf9d 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -15,7 +15,6 @@ module Gitlab
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
- :elasticsearch_timed_out_count,
*::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
*::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,
@@ -80,7 +79,6 @@ module Gitlab
payload[:elasticsearch_calls] = elasticsearch_calls
payload[:elasticsearch_duration_s] = Gitlab::Instrumentation::ElasticsearchTransport.query_time
- payload[:elasticsearch_timed_out_count] = Gitlab::Instrumentation::ElasticsearchTransport.get_timed_out_count
end
def instrument_external_http(payload)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4d3a62b20a1..3b948e7fdda 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16,7 +16,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-msgid " %{project_name}#%{issuable_iid} &middot; opened %{issuable_created} by %{author}"
+msgid " %{project_name}#%{issuable_iid} &middot; opened %{issuable_created} by %{author} &middot; updated %{issuable_updated}"
msgstr ""
msgid " %{start} to %{end}"
@@ -8481,6 +8481,9 @@ msgstr ""
msgid "Create new"
msgstr ""
+msgid "Create new %{name} by email"
+msgstr ""
+
msgid "Create new Value Stream"
msgstr ""
@@ -10863,6 +10866,9 @@ msgstr ""
msgid "Email Notification"
msgstr ""
+msgid "Email a new %{name} to this project"
+msgstr ""
+
msgid "Email address to use for Support Desk"
msgstr ""
@@ -10935,12 +10941,6 @@ msgstr ""
msgid "EmailParticipantsWarning|and %{moreCount} more"
msgstr ""
-msgid "EmailToken|reset it"
-msgstr ""
-
-msgid "EmailToken|resetting..."
-msgstr ""
-
msgid "Emails"
msgstr ""
@@ -11226,19 +11226,13 @@ msgstr ""
msgid "Enter one or more user ID separated by commas"
msgstr ""
-msgid "Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes."
-msgstr ""
-
-msgid "Enter the issue description"
-msgstr ""
-
-msgid "Enter the issue title"
+msgid "Enter the %{name} description"
msgstr ""
-msgid "Enter the merge request description"
+msgid "Enter the %{name} title"
msgstr ""
-msgid "Enter the merge request title"
+msgid "Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes."
msgstr ""
msgid "Enter the name of your application, and we'll return a unique %{type}."
@@ -23934,6 +23928,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!"
+msgstr ""
+
msgid "Protocol"
msgstr ""
@@ -29320,6 +29317,9 @@ msgstr ""
msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr ""
+msgid "The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported."
+msgstr ""
+
msgid "The tag name can't be changed for an existing release."
msgstr ""
@@ -29926,6 +29926,9 @@ msgstr ""
msgid "This is a merge train pipeline"
msgstr ""
+msgid "This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens."
+msgstr ""
+
msgid "This is a security log of important events involving your account."
msgstr ""
@@ -30223,6 +30226,9 @@ msgstr ""
msgid "Threat Monitoring"
msgstr ""
+msgid "ThreatMonitoring|Alert Details"
+msgstr ""
+
msgid "ThreatMonitoring|Alerts"
msgstr ""
@@ -33349,6 +33355,9 @@ msgstr ""
msgid "You can create a new %{link}."
msgstr ""
+msgid "You can create a new %{name} inside this project by sending an email to the following email address:"
+msgstr ""
+
msgid "You can create a new Personal Access Token by visiting %{link}"
msgstr ""
diff --git a/scripts/flaky_examples/detect-new-flaky-examples b/scripts/flaky_examples/detect-new-flaky-examples
index 3bee4f9a34b..4805c5054a5 100755
--- a/scripts/flaky_examples/detect-new-flaky-examples
+++ b/scripts/flaky_examples/detect-new-flaky-examples
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
diff --git a/scripts/flaky_examples/prune-old-flaky-examples b/scripts/flaky_examples/prune-old-flaky-examples
index 4df49c6d8fa..8c09c4cc860 100755
--- a/scripts/flaky_examples/prune-old-flaky-examples
+++ b/scripts/flaky_examples/prune-old-flaky-examples
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# lib/rspec_flaky/flaky_examples_collection.rb is requiring
# `active_support/hash_with_indifferent_access`, and we install the `activesupport`
diff --git a/scripts/gather-test-memory-data b/scripts/gather-test-memory-data
index 9992a83e6a6..3156365ac19 100755
--- a/scripts/gather-test-memory-data
+++ b/scripts/gather-test-memory-data
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'csv'
diff --git a/scripts/generate-gems-memory-metrics-static b/scripts/generate-gems-memory-metrics-static
index aa7ce3615bf..42191f078f1 100755
--- a/scripts/generate-gems-memory-metrics-static
+++ b/scripts/generate-gems-memory-metrics-static
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} <memory_bundle_objects_file_name>" unless ARGV.length == 1
memory_bundle_objects_file_name = ARGV.first
diff --git a/scripts/generate-gems-size-metrics-static b/scripts/generate-gems-size-metrics-static
index ceec8aaccf1..2406e720916 100755
--- a/scripts/generate-gems-size-metrics-static
+++ b/scripts/generate-gems-size-metrics-static
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} <memory_bundle_mem_file_name>" unless ARGV.length == 1
memory_bundle_mem_file_name = ARGV.first
diff --git a/scripts/generate-memory-metrics-on-boot b/scripts/generate-memory-metrics-on-boot
index 5197a8fcdcd..945661aa057 100755
--- a/scripts/generate-memory-metrics-on-boot
+++ b/scripts/generate-memory-metrics-on-boot
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort "usage: #{__FILE__} <memory_bundle_mem_file_name>" unless ARGV.length == 1
memory_bundle_mem_file_name = ARGV.first
diff --git a/scripts/generate-test-mapping b/scripts/generate-test-mapping
index eabe6a5b513..c4d0dfea4d8 100755
--- a/scripts/generate-test-mapping
+++ b/scripts/generate-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_generator'
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index 62d3dbda911..bb561e2906a 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'fileutils'
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index caa41a9a0c3..8547d0b13e4 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# This script is used both in CI and in local development 'rspec' runs.
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index c7b3f72d590..2262870eb96 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file contains environment settings for gitaly when it's running
# as part of the gitlab-ce/ee test suite.
#
@@ -52,7 +54,7 @@ module GitalyTest
if ENV['CI']
bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__)
- env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}"
+ env_hash['BUNDLE_FLAGS'] += " --path=#{bundle_path}"
end
env_hash
diff --git a/scripts/insert-rspec-profiling-data b/scripts/insert-rspec-profiling-data
index 3af5fe763a2..b2011858558 100755
--- a/scripts/insert-rspec-profiling-data
+++ b/scripts/insert-rspec-profiling-data
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'csv'
require 'rspec_profiling'
diff --git a/scripts/lint-rugged b/scripts/lint-rugged
index d7af5499e1c..038fd5199c2 100755
--- a/scripts/lint-rugged
+++ b/scripts/lint-rugged
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
ALLOWED = [
# https://gitlab.com/gitlab-org/gitaly/issues/760
diff --git a/scripts/merge-html-reports b/scripts/merge-html-reports
index 7d1e15186c8..de300851990 100755
--- a/scripts/merge-html-reports
+++ b/scripts/merge-html-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'nokogiri'
diff --git a/scripts/merge-reports b/scripts/merge-reports
index 3a421f1f1fc..a1164495f2f 100755
--- a/scripts/merge-reports
+++ b/scripts/merge-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov
index c00dae81c4d..38dd2dfe2e9 100755
--- a/scripts/merge-simplecov
+++ b/scripts/merge-simplecov
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require_relative '../spec/simplecov_env'
SimpleCovEnv.configure_profile
diff --git a/scripts/no-ee-check b/scripts/no-ee-check
index 29d319dc822..a878a4424e9 100755
--- a/scripts/no-ee-check
+++ b/scripts/no-ee-check
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
ee_path = File.join(File.expand_path(__dir__), '../ee')
if Dir.exist?(ee_path)
diff --git a/scripts/pack-test-mapping b/scripts/pack-test-mapping
index 58ace3eca67..b5148cd1882 100755
--- a/scripts/pack-test-mapping
+++ b/scripts/pack-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 9103a9c14af..febfdbd1da4 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
# We don't have auto-loading here
require_relative '../lib/gitlab'
diff --git a/scripts/sync-reports b/scripts/sync-reports
index 5ed65e78005..73afd276e6c 100755
--- a/scripts/sync-reports
+++ b/scripts/sync-reports
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'rubygems'
require 'fog/aws'
diff --git a/scripts/unpack-test-mapping b/scripts/unpack-test-mapping
index c0f706c3f9f..7176f9cecea 100755
--- a/scripts/unpack-test-mapping
+++ b/scripts/unpack-test-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
diff --git a/scripts/update-feature-categories b/scripts/update-feature-categories
index ed5d8dccdd6..88520b9f95f 100755
--- a/scripts/update-feature-categories
+++ b/scripts/update-feature-categories
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'uri'
require 'net/http'
diff --git a/scripts/used-feature-flags b/scripts/used-feature-flags
index 7ef3dbafd36..aebd007dda9 100755
--- a/scripts/used-feature-flags
+++ b/scripts/used-feature-flags
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'set'
diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping
index 8bf25ea3b5f..9931e14008a 100755
--- a/scripts/verify-tff-mapping
+++ b/scripts/verify-tff-mapping
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'set'
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 6c89b08d9c0..2d6b669f28b 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -18,9 +18,9 @@ RSpec.describe 'Issue Boards', :js do
project.add_maintainer(user)
project.add_maintainer(user2)
- set_cookie('sidebar_collapsed', 'true')
-
sign_in(user)
+
+ set_cookie('sidebar_collapsed', 'true')
end
context 'no lists' do
diff --git a/spec/features/issues/user_creates_issue_by_email_spec.rb b/spec/features/issues/user_creates_issue_by_email_spec.rb
index 5a0036170ab..c47f24ab836 100644
--- a/spec/features/issues/user_creates_issue_by_email_spec.rb
+++ b/spec/features/issues/user_creates_issue_by_email_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Issues > User creates issue by email' do
project.add_developer(user)
end
- describe 'new issue by email' do
+ describe 'new issue by email', :js do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
@@ -28,7 +28,7 @@ RSpec.describe 'Issues > User creates issue by email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
- expect(page).to have_selector("input[value='#{email}']")
+ expect(page.find('input[type="text"]').value).to eq email
end
end
end
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
index a20f65abebf..2b1c25174c2 100644
--- a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -16,17 +16,17 @@ RSpec.describe 'Issues > User resets their incoming email token' do
end
it 'changes incoming email address token', :js do
- find('.issuable-email-modal-btn').click
- previous_token = find('input#issuable_email').value
- find('.incoming-email-token-reset').click
-
- wait_for_requests
-
- expect(page).to have_no_field('issuable_email', with: previous_token)
- new_token = project.new_issuable_address(user.reload, 'issue')
- expect(page).to have_field(
- 'issuable_email',
- with: new_token
- )
+ page.find('[data-testid="issuable-email-modal-btn"]').click
+
+ page.within '#issuable-email-modal' do
+ previous_token = page.find('input[type="text"]').value
+ page.find('[data-testid="incoming-email-token-reset"]').click
+
+ wait_for_requests
+
+ expect(page.find('input[type="text"]').value).not_to eq previous_token
+ new_token = project.new_issuable_address(user.reload, 'issue')
+ expect(page.find('input[type="text"]').value).to eq new_token
+ end
end
end
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 7cdecefab05..ecd67247362 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -38,7 +38,9 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
required: false,
default: () => [],
},
- ...Object.fromEntries(['target', 'triggers', 'placement'].map((prop) => [prop, {}])),
+ ...Object.fromEntries(
+ ['target', 'triggers', 'placement', 'boundary', 'container'].map((prop) => [prop, {}]),
+ ),
},
render(h) {
return h(
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
new file mode 100644
index 00000000000..0730cfd453e
--- /dev/null
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -0,0 +1,80 @@
+import { mount } from '@vue/test-utils';
+import { GlPopover, GlLink, GlButton } from '@gitlab/ui';
+import FeatureHighlightPopover from '~/feature_highlight/feature_highlight_popover.vue';
+import { dismiss } from '~/feature_highlight/feature_highlight_helper';
+import { POPOVER_TARGET_ID } from '~/feature_highlight/constants';
+
+jest.mock('~/feature_highlight/feature_highlight_helper');
+
+describe('feature_highlight/feature_highlight_popover', () => {
+ let wrapper;
+ const props = {
+ autoDevopsHelpPath: '/help/autodevops',
+ highlightId: '123',
+ dismissEndpoint: '/api/dismiss',
+ };
+
+ const buildWrapper = (propsData = props) => {
+ wrapper = mount(FeatureHighlightPopover, {
+ propsData,
+ });
+ };
+ const findPopoverTarget = () => wrapper.find(`#${POPOVER_TARGET_ID}`);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findAutoDevopsHelpLink = () => wrapper.findComponent(GlLink);
+ const findDismissButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders popover target', () => {
+ expect(findPopoverTarget().exists()).toBe(true);
+ });
+
+ it('renders popover', () => {
+ expect(findPopover().props()).toMatchObject({
+ target: POPOVER_TARGET_ID,
+ cssClasses: ['feature-highlight-popover'],
+ triggers: 'hover',
+ container: 'body',
+ placement: 'right',
+ boundary: 'viewport',
+ });
+ });
+
+ it('renders link that points to the autodevops help page', () => {
+ expect(findAutoDevopsHelpLink().attributes().href).toBe(props.autoDevopsHelpPath);
+ expect(findAutoDevopsHelpLink().text()).toBe('Auto DevOps');
+ });
+
+ it('renders dismiss button', () => {
+ expect(findDismissButton().props()).toMatchObject({
+ size: 'small',
+ icon: 'thumb-up',
+ variant: 'confirm',
+ });
+ });
+
+ it('dismisses popover when dismiss button is clicked', async () => {
+ await findDismissButton().trigger('click');
+
+ expect(findPopover().emitted('close')).toHaveLength(1);
+ expect(dismiss).toHaveBeenCalledWith(props.dismissEndpoint, props.highlightId);
+ });
+
+ describe('when popover is dismissed and hidden', () => {
+ it('hides the popover target', async () => {
+ await findDismissButton().trigger('click');
+ findPopover().vm.$emit('hidden');
+ await wrapper.vm.$nextTick();
+
+ expect(findPopoverTarget().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
new file mode 100644
index 00000000000..7e40b903754
--- /dev/null
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -0,0 +1,164 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlSprintf, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+
+const initialEmail = 'user@gitlab.com';
+
+const mockToastShow = jest.fn();
+
+describe('IssuableByEmail', () => {
+ let wrapper;
+ let mockAxios;
+ let glModalDirective;
+
+ function createComponent(injectedProperties = {}) {
+ glModalDirective = jest.fn();
+
+ return extendedWrapper(
+ shallowMount(IssuableByEmail, {
+ stubs: {
+ GlModal,
+ GlSprintf,
+ GlFormInputGroup,
+ GlButton,
+ },
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ provide: {
+ issuableType: 'issue',
+ initialEmail,
+ ...injectedProperties,
+ },
+ }),
+ );
+ }
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ const findFormInputGroup = () => wrapper.find(GlFormInputGroup);
+
+ const clickResetEmail = async () => {
+ wrapper.findByTestId('incoming-email-token-reset').vm.$emit('click');
+
+ await waitForPromises();
+ };
+
+ describe('modal button', () => {
+ it.each`
+ issuableType | buttonText
+ ${'issue'} | ${'Email a new issue to this project'}
+ ${'merge_request'} | ${'Email a new merge request to this project'}
+ `(
+ 'renders a link with "$buttonText" when type is "$issuableType"',
+ ({ issuableType, buttonText }) => {
+ wrapper = createComponent({ issuableType });
+ expect(wrapper.findByTestId('issuable-email-modal-btn').text()).toBe(buttonText);
+ },
+ );
+
+ it('opens the modal when the user clicks the button', () => {
+ wrapper = createComponent();
+
+ wrapper.findByTestId('issuable-email-modal-btn').vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+ });
+
+ describe('modal', () => {
+ it('renders a read-only email input field', () => {
+ wrapper = createComponent();
+
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+
+ it.each`
+ issuableType | subject | body
+ ${'issue'} | ${'Enter the issue title'} | ${'Enter the issue description'}
+ ${'merge_request'} | ${'Enter the merge request title'} | ${'Enter the merge request description'}
+ `('renders a mailto button when type is "$issuableType"', ({ issuableType, subject, body }) => {
+ wrapper = createComponent({
+ issuableType,
+ initialEmail,
+ });
+
+ expect(wrapper.findByTestId('mail-to-btn').attributes('href')).toBe(
+ `mailto:${initialEmail}?subject=${subject}&body=${body}`,
+ );
+ });
+
+ describe('reset email', () => {
+ const resetPath = 'gitlab-test/new_issuable_address?issuable_type=issue';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+ it('should send request to reset email token', async () => {
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(axios.put).toHaveBeenCalledWith(resetPath);
+ });
+
+ it('should update the email when the request succeeds', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.OK, { new_address: 'foo@bar.com' });
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(findFormInputGroup().props('value')).toBe('foo@bar.com');
+ });
+
+ it('should show a toast message when the request fails', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.NOT_FOUND, {});
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ 'There was an error when reseting email token.',
+ { type: 'error' },
+ );
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable_spec.js b/spec/frontend/issuable_spec.js
index 6712b8bfd34..9c8f1e04609 100644
--- a/spec/frontend/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -1,6 +1,3 @@
-import $ from 'jquery';
-import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
@@ -22,43 +19,4 @@ describe('Issuable', () => {
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
});
});
-
- describe('resetIncomingEmailToken', () => {
- let mock;
-
- beforeEach(() => {
- const element = document.createElement('a');
- element.classList.add('incoming-email-token-reset');
- element.setAttribute('href', 'foo');
- document.body.appendChild(element);
-
- const input = document.createElement('input');
- input.setAttribute('id', 'issuable_email');
- document.body.appendChild(input);
-
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- mock = new MockAdaptor(axios);
-
- mock.onPut('foo').reply(200, {
- new_address: 'testing123',
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should send request to reset email token', (done) => {
- jest.spyOn(axios, 'put');
- document.querySelector('.incoming-email-token-reset').click();
-
- setImmediate(() => {
- expect(axios.put).toHaveBeenCalledWith('foo');
- expect($('#issuable_email').val()).toBe('testing123');
-
- done();
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 40af8e60008..d2ab5878172 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -90,6 +90,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable);
+ const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -175,6 +176,15 @@ describe('AlertDetails', () => {
});
});
+ describe('Threat Monitoring details', () => {
+ it('should not render the metrics tab', () => {
+ mountComponent({
+ data: { alert: mockAlert, provide: { isThreatMonitoringPage: true } },
+ });
+ expect(findMetricsTab().exists()).toBe(false);
+ });
+ });
+
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index c2df37821d3..70cf2597963 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import SidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
- function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
+ function mountComponent({
+ mountMethod = shallowMount,
+ stubs = {},
+ alert = {},
+ provide = {},
+ } = {}) {
wrapper = mountMethod(AlertSidebar, {
data() {
return {
@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => {
provide: {
projectPath: 'projectPath',
projectId: '1',
+ ...provide,
},
stubs,
mocks: {
@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => {
});
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
});
+
+ it('should render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(true);
+ });
+ });
+
+ describe('the sidebar renders for threat monitoring', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mountComponent();
+ });
+
+ it('should not render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ provide: { isThreatMonitoringPage: true },
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(false);
+ });
});
});
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c629643e248..264bad92d56 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -200,7 +200,13 @@ RSpec.describe EventsHelper do
it 'returns a project snippet note url' do
event.target = create(:note_on_project_snippet, note: 'keep going')
- expect(subject).to eq("#{project_base_url}/-/snippets/#{event.note_target.id}#note_#{event.target.id}")
+ expect(subject).to eq("#{project_snippet_url(event.note_target.project, event.note_target)}#note_#{event.target.id}")
+ end
+
+ it 'returns a personal snippet note url' do
+ event.target = create(:note_on_personal_snippet, note: 'keep going')
+
+ expect(subject).to eq("#{snippet_url(event.note_target)}#note_#{event.target.id}")
end
it 'returns a project issue url' do
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 5afa693b400..0df194e460a 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -113,7 +113,8 @@ RSpec.describe Projects::AlertManagementHelper do
'alert-id' => alert_id,
'project-path' => project_path,
'project-id' => project_id,
- 'project-issues-path' => issues_path
+ 'project-issues-path' => issues_path,
+ 'page' => 'OPERATIONS'
)
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 69b1499d63c..d0282e14d5f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -564,6 +564,7 @@ project:
- incident_management_oncall_rotations
- debian_distributions
- merge_request_metrics
+- security_orchestration_policy_configuration
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index a5c9cde4c37..5cc911accbb 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe Gitlab::InstrumentationHelper do
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
- :elasticsearch_timed_out_count,
:mem_objects,
:mem_bytes,
:mem_mallocs,
diff --git a/spec/migrations/add_new_post_eoa_plans_spec.rb b/spec/migrations/add_new_post_eoa_plans_spec.rb
new file mode 100644
index 00000000000..5ae227a6617
--- /dev/null
+++ b/spec/migrations/add_new_post_eoa_plans_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210205104425_add_new_post_eoa_plans.rb')
+
+RSpec.describe AddNewPostEoaPlans do
+ let(:plans) { table(:plans) }
+
+ subject(:migration) { described_class.new }
+
+ describe '#up' do
+ it 'creates the two new records' do
+ expect { migration.up }.to change { plans.count }.by(2)
+
+ new_plans = plans.last(2)
+ expect(new_plans.map(&:name)).to contain_exactly('premium', 'ultimate')
+ end
+ end
+
+ describe '#down' do
+ it 'removes these two new records' do
+ plans.create!(name: 'premium', title: 'Premium (Formerly Silver)')
+ plans.create!(name: 'ultimate', title: 'Ultimate (Formerly Gold)')
+
+ expect { migration.down }.to change { plans.count }.by(-2)
+
+ expect(plans.find_by(name: 'premium')).to be_nil
+ expect(plans.find_by(name: 'ultimate')).to be_nil
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
new file mode 100644
index 00000000000..ee1f718849f
--- /dev/null
+++ b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe CleanupProjectsWithBadHasExternalWikiData, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
+ let(:projects) { table(:projects) }
+ let(:services) { table(:services) }
+
+ def create_projects!(num)
+ Array.new(num) do
+ projects.create!(namespace_id: namespace.id)
+ end
+ end
+
+ def create_active_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ def create_disabled_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: false)
+ end
+ end
+
+ def create_active_other_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'NotAnExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ it 'sets `projects.has_external_wiki` correctly' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_without_external_wiki_1,
+ project_without_external_wiki_2 = create_projects!(6)
+
+ create_active_external_wiki_integrations!(
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ )
+
+ create_disabled_external_wiki_integrations!(
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ )
+
+ create_active_other_integrations!(
+ project_without_external_wiki_1,
+ project_without_external_wiki_2
+ )
+
+ # PG triggers on the services table added in a previous migration
+ # will have set the `has_external_wiki` columns to correct data when
+ # the services records were created above.
+ #
+ # We set the `has_external_wiki` columns for projects to incorrect
+ # data manually below to emulate projects in a state before the PG
+ # triggers were added.
+ project_with_external_wiki_2.update!(has_external_wiki: false)
+ project_with_disabled_external_wiki_2.update!(has_external_wiki: true)
+ project_without_external_wiki_2.update!(has_external_wiki: true)
+
+ migrate!
+
+ expected_true = [
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expected_not_true = [
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expect(expected_true).to all(eq(true))
+ expect(expected_not_true).to all(be_falsey)
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 47492715c11..a726c6e6513 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -907,6 +907,58 @@ RSpec.describe Event do
end
end
+ context 'with snippet note' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:note_on_project_snippet) { create(:note_on_project_snippet, author: user) }
+ let_it_be(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: user) }
+ let_it_be(:other_note) { create(:note_on_issue, author: user)}
+ let_it_be(:personal_snippet_event) { create(:event, :commented, project: nil, target: note_on_personal_snippet, author: user) }
+ let_it_be(:project_snippet_event) { create(:event, :commented, project: note_on_project_snippet.project, target: note_on_project_snippet, author: user) }
+ let_it_be(:other_event) { create(:event, :commented, project: other_note.project, target: other_note, author: user) }
+
+ describe '#snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.snippet_note?).to be false
+ end
+ end
+
+ describe '#personal_snippet_note?' do
+ it 'returns false for a project snippet event' do
+ expect(project_snippet_event.personal_snippet_note?).to be false
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.personal_snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.personal_snippet_note?).to be false
+ end
+ end
+
+ describe '#project_snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.project_snippet_note?).to be true
+ end
+
+ it 'returns false for a personal snippet event' do
+ expect(personal_snippet_event.project_snippet_note?).to be false
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.project_snippet_note?).to be false
+ end
+ end
+ end
+
describe '#action_name' do
it 'handles all valid design events' do
created, updated, destroyed, archived = %i[created updated destroyed archived].map do |trait|
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 5c14a3db54e..364b80e8601 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -837,6 +837,16 @@ RSpec.describe Note do
end
end
+ describe '#for_project_snippet?' do
+ it 'returns true for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_project_snippet?).to be true
+ end
+
+ it 'returns false for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_project_snippet?).to be false
+ end
+ end
+
describe '#for_personal_snippet?' do
it 'returns false for a project snippet note' do
expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy