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 @@ + + 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 @@ + + + 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 { - + 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)" /> { 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 } × - .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} · 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} · opened %{issuable_created} by %{author} · 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 | +| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 | -```mermaid -stateDiagram-v2 - [*] --> LoadBalancer - LoadBalancer --> ApplicationServer +```plantuml +@startuml 10k +card "**External Load Balancer**" as elb #6a9be7 +card "**Internal Load Balancer**" as ilb #9370DB - ApplicationServer --> BackgroundJobs - ApplicationServer --> Gitaly - ApplicationServer --> Redis_Cache - ApplicationServer --> Redis_Queues - ApplicationServer --> PgBouncer - PgBouncer --> Database - ApplicationServer --> ObjectStorage - BackgroundJobs --> ObjectStorage +together { + collections "**GitLab Rails** x3" as gitlab #32CD32 + collections "**Sidekiq** x4" as sidekiq #ff8dd1 +} - ApplicationMonitoring -->ApplicationServer - ApplicationMonitoring -->PgBouncer - ApplicationMonitoring -->Database - ApplicationMonitoring -->BackgroundJobs +together { + card "**Prometheus + Grafana**" as monitor #7FFFD4 + collections "**Consul** x3" as consul #e76a9b +} - ApplicationServer --> Consul +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 - Consul --> Database - Consul --> PgBouncer - Redis_Cache --> Consul - Redis_Queues --> Consul - BackgroundJobs --> Consul + praefect -[#FF8C00]-> gitaly + praefect -[#FF8C00]> praefect_postgres +} - state Consul { - "Consul_1..3" - } +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 - state Database { - "PG_Primary_Node" - "PG_Secondary_Node_1..2" - } + pgbouncer -[#4EA7FF]-> postgres_primary + postgres_primary .[#4EA7FF]> postgres_secondary +} - state Redis_Cache { - "R_Cache_Primary_Node" - "R_Cache_Replica_Node_1..2" - "R_Cache_Sentinel_1..3" - } +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 - state Redis_Queues { - "R_Queues_Primary_Node" - "R_Queues_Replica_Node_1..2" - "R_Queues_Sentinel_1..3" - } + redis_persistent <.[#FF6347]- redis_persistent_sentinel + redis_cache <.[#FF6347]- redis_cache_sentinel +} - state Gitaly { - "Gitaly_1..2" - } +cloud "**Object Storage**" as object_storage #white - state BackgroundJobs { - "Sidekiq_1..4" - } +elb -[#6a9be7]-> gitlab +elb -[#6a9be7]--> monitor - state ApplicationServer { - "GitLab_Rails_1..3" - } +gitlab -[#32CD32]> sidekiq +gitlab -[#32CD32]--> ilb +gitlab -[#32CD32]-> object_storage +gitlab -[#32CD32]---> redis +gitlab -[hidden]-> monitor +gitlab -[hidden]-> consul - state LoadBalancer { - "LoadBalancer_1" - } +sidekiq -[#ff8dd1]--> ilb +sidekiq -[#ff8dd1]-> object_storage +sidekiq -[#ff8dd1]---> redis +sidekiq -[hidden]-> monitor +sidekiq -[hidden]-> consul - state ApplicationMonitoring { - "Prometheus" - "Grafana" - } +ilb -[#9370DB]-> gitaly_cluster +ilb -[#9370DB]-> database - state PgBouncer { - "Internal_Load_Balancer" - "PgBouncer_1..3" - } +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`. +## 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. + +
+ + Back to setup components + +
+ ## Configure Consul The following IPs will be used as an example: @@ -662,52 +748,6 @@ The following IPs will be used as an example: -### 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. - -
- - Back to setup components - -
- ## 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: -## 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. -[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 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 ``. +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 `` + and confirmation. Use the value that is output by this command in the next + step as the value of ``: + + ```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'] = "" + + # 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). + + + +#### 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 ``. + +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 ``: + + ```shell + CREATE ROLE praefect WITH LOGIN CREATEDB 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; + ``` + + + +### 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: + +- ``: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this 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. +- ``: 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 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 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' => '', + 'primary' => true + }, + 'gitaly-2' => { + 'address' => 'tcp://10.6.0.92:8075', + 'token' => '' + }, + 'gitaly-3' => { + 'address' => 'tcp://10.6.0.93:8075', + '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 + +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. +- `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` -This section describes how to configure two Gitaly servers, with the following -IPs and domain names: +The following IPs will be used as an example: -- `10.6.0.91`: Gitaly 1 (`gitaly1.internal`) -- `10.6.0.92`: Gitaly 2 (`gitaly2.internal`) - -Assumptions about your servers include having the secret token be `gitalysecret`, -and that your GitLab installation has three repository storages: - -- `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: - - ```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'] = '' ``` 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" + } }) ``` - + - 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: - - ```shell - sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/ - ``` - 1. Edit `/etc/gitlab/gitlab.rb` and add: - - ```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" + 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. Delete `gitaly['listen_addr']` to allow only encrypted connections. +1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure). + +1. On the Praefect clients (including each Gitaly server), copy the certificates, + or their certificate authority, into `/etc/gitlab/trusted-certs`: + + ```shell + sudo cp cert.pem /etc/gitlab/trusted-certs/ + ``` + +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" => '' + } }) - 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'] = '' 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" => '' + } }) ## 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" => '' + } }) ``` 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} · opened %{issuable_created} by %{author}" +msgid " %{project_name}#%{issuable_iid} · opened %{issuable_created} by %{author} · 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,21 +11226,15 @@ msgstr "" msgid "Enter one or more user ID separated by commas" msgstr "" +msgid "Enter the %{name} description" +msgstr "" + +msgid "Enter the %{name} title" +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" -msgstr "" - -msgid "Enter the merge request description" -msgstr "" - -msgid "Enter the merge request title" -msgstr "" - msgid "Enter the name of your application, and we'll return a unique %{type}." msgstr "" @@ -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__} " 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__} " 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__} " 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 + page.find('[data-testid="issuable-email-modal-btn"]').click - wait_for_requests + page.within '#issuable-email-modal' do + previous_token = page.find('input[type="text"]').value + page.find('[data-testid="incoming-email-token-reset"]').click - 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 - ) + 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