diff options
106 files changed, 1716 insertions, 887 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a1f074cd973..86d611f659a 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5 +22654ba48106412ca6680366afe6a47389458720 @@ -482,7 +482,7 @@ gem 'gitaly', '~> 13.11.0.pre.rc1' gem 'grpc', '~> 1.30.2' -gem 'google-protobuf', '~> 3.14.0' +gem 'google-protobuf', '~> 3.15.8' gem 'toml-rb', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index be72969c089..369ae157e0e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -518,7 +518,7 @@ GEM signet (~> 0.12) google-cloud-env (1.4.0) faraday (>= 0.17.3, < 2.0) - google-protobuf (3.14.0) + google-protobuf (3.15.8) googleapis-common-protos-types (1.0.5) google-protobuf (~> 3.11) googleauth (0.14.0) @@ -1464,7 +1464,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.4.0) google-api-client (~> 0.33) - google-protobuf (~> 3.14.0) + google-protobuf (~> 3.15.8) gpgme (~> 2.0.19) grape (~> 1.5.2) grape-entity (~> 0.7.1) diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 3ccf982ef01..560f691eb4e 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -36,8 +36,10 @@ import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/c import { __ } from '~/locale'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; +import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; +import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import eventHub from '../eventhub'; import IssueCardTimeInfo from './issue_card_time_info.vue'; @@ -88,6 +90,9 @@ export default { hasIssues: { default: false, }, + hasIssueWeightsFeature: { + default: false, + }, initialEmail: { default: '', }, @@ -103,6 +108,9 @@ export default { newIssuePath: { default: '', }, + projectIterationsPath: { + default: '', + }, projectLabelsPath: { default: '', }, @@ -155,7 +163,7 @@ export default { return convertToSearchQuery(this.filterTokens) || undefined; }, searchTokens() { - return [ + const tokens = [ { type: 'author_username', title: __('Author'), @@ -216,6 +224,30 @@ export default { ], }, ]; + + if (this.projectIterationsPath) { + tokens.push({ + type: 'iteration', + title: __('Iteration'), + icon: 'iteration', + token: IterationToken, + unique: true, + defaultIterations: [], + fetchIterations: this.fetchIterations, + }); + } + + if (this.hasIssueWeightsFeature) { + tokens.push({ + type: 'weight', + title: __('Weight'), + icon: 'weight', + token: WeightToken, + unique: true, + }); + } + + return tokens; }, showPaginationControls() { return this.issues.length > 0; @@ -273,6 +305,9 @@ export default { fetchMilestones(search) { return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true); }, + fetchIterations(search) { + return axios.get(this.projectIterationsPath, { params: { search } }); + }, fetchUsers(search) { return axios.get(this.autocompleteUsersPath, { params: { search } }); }, diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index 60a211ec8c0..38a0d953835 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -334,4 +334,24 @@ export const filters = { [OPERATOR_IS]: 'confidential', }, }, + iteration: { + apiParam: { + [OPERATOR_IS]: 'iteration_title', + [OPERATOR_IS_NOT]: 'not[iteration_title]', + }, + urlParam: { + [OPERATOR_IS]: 'iteration_title', + [OPERATOR_IS_NOT]: 'not[iteration_title]', + }, + }, + weight: { + apiParam: { + [OPERATOR_IS]: 'weight', + [OPERATOR_IS_NOT]: 'not[weight]', + }, + urlParam: { + [OPERATOR_IS]: 'weight', + [OPERATOR_IS_NOT]: 'not[weight]', + }, + }, }; diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 06c50a02ada..0318c2c2484 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -98,6 +98,7 @@ export function initIssuesListApp() { maxAttachmentSize, newIssuePath, projectImportJiraPath, + projectIterationsPath, projectLabelsPath, projectMilestonesPath, projectPath, @@ -128,6 +129,7 @@ export function initIssuesListApp() { issuesPath, jiraIntegrationPath, newIssuePath, + projectIterationsPath, projectLabelsPath, projectMilestonesPath, projectPath, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 3d8afd162cb..e2868879425 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -1,18 +1,16 @@ /* eslint-disable @gitlab/require-i18n-strings */ import { __ } from '~/locale'; +export const DEBOUNCE_DELAY = 200; + const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') }; export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') }; export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') }; +export const DEFAULT_LABEL_CURRENT = { value: 'Current', text: __('Current') }; -export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL]; +export const DEFAULT_ITERATIONS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEFAULT_LABEL_CURRENT]; -export const DEBOUNCE_DELAY = 200; - -export const SortDirection = { - descending: 'descending', - ascending: 'ascending', -}; +export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL]; export const DEFAULT_MILESTONES = [ DEFAULT_LABEL_NONE, @@ -21,4 +19,8 @@ export const DEFAULT_MILESTONES = [ { value: 'Started', text: __('Started') }, ]; +export const SortDirection = { + descending: 'descending', + ascending: 'ascending', +}; /* eslint-enable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue new file mode 100644 index 00000000000..7b6a590279a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue @@ -0,0 +1,110 @@ +<script> +import { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; + +export default { + components: { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + iterations: this.config.initialIterations || [], + defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS, + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data; + }, + activeIteration() { + return this.iterations.find((iteration) => iteration.title === this.currentValue); + }, + }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.iterations.length) { + this.fetchIterationBySearchTerm(this.currentValue); + } + }, + }, + }, + methods: { + fetchIterationBySearchTerm(searchTerm) { + const fetchPromise = this.config.fetchPath + ? this.config.fetchIterations(this.config.fetchPath, searchTerm) + : this.config.fetchIterations(searchTerm); + + this.loading = true; + + fetchPromise + .then((response) => { + this.iterations = Array.isArray(response) ? response : response.data; + }) + .catch(() => createFlash({ message: __('There was a problem fetching iterations.') })) + .finally(() => { + this.loading = false; + }); + }, + searchIterations: debounce(function debouncedSearch({ data }) { + this.fetchIterationBySearchTerm(data); + }, DEBOUNCE_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchIterations" + > + <template #view="{ inputValue }"> + {{ activeIteration ? activeIteration.title : inputValue }} + </template> + <template #suggestions> + <gl-filtered-search-suggestion + v-for="iteration in defaultIterations" + :key="iteration.value" + :value="iteration.value" + > + {{ iteration.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultIterations.length" /> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="iteration in iterations" + :key="iteration.title" + :value="iteration.title" + > + {{ iteration.title }} + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue new file mode 100644 index 00000000000..cfad79b9afa --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue @@ -0,0 +1,58 @@ +<script> +import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui'; +import { DEFAULT_LABEL_ANY, DEFAULT_LABEL_NONE } from '../constants'; + +export default { + baseWeights: ['0', '1', '2', '3', '4', '5'], + components: { + GlDropdownDivider, + GlFilteredSearchSuggestion, + GlFilteredSearchToken, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + weights: this.$options.baseWeights, + defaultWeights: this.config.defaultWeights || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY], + }; + }, + methods: { + updateWeights({ data }) { + const weight = parseInt(data, 10); + this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)]; + }, + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="updateWeights" + > + <template #suggestions> + <gl-filtered-search-suggestion + v-for="weight in defaultWeights" + :key="weight.value" + :value="weight.value" + > + {{ weight.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultWeights.length" /> + <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight"> + {{ weight }} + </gl-filtered-search-suggestion> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/graphql/queries/burndown_chart/burnup.query.graphql b/app/graphql/queries/burndown_chart/burnup.query.graphql new file mode 100644 index 00000000000..7a389a6def5 --- /dev/null +++ b/app/graphql/queries/burndown_chart/burnup.query.graphql @@ -0,0 +1,70 @@ +query BurnupTimesSeriesData($id: ID!, $isIteration: Boolean = false, $weight: Boolean = false) { + milestone(id: $id) @skip(if: $isIteration) { + __typename + id + title + report { + __typename + burnupTimeSeries { + __typename + date + completedCount @skip(if: $weight) + scopeCount @skip(if: $weight) + completedWeight @include(if: $weight) + scopeWeight @include(if: $weight) + } + stats { + __typename + total { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + complete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + incomplete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + } + } + } + iteration(id: $id) @include(if: $isIteration) { + __typename + id + title + report { + __typename + burnupTimeSeries { + __typename + date + completedCount @skip(if: $weight) + scopeCount @skip(if: $weight) + completedWeight @include(if: $weight) + scopeWeight @include(if: $weight) + } + stats { + __typename + total { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + complete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + incomplete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + } + } + } +} diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 2cad6f4745c..6e4de4e90b0 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -8,7 +8,7 @@ module InviteMembersHelper end def can_invite_members_for_project?(project) - Feature.enabled?(:invite_members_group_modal, project.group) && can_import_members? + Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project) end def directly_invite_members? diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 7e03d709f24..719511bbb8a 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -8,6 +8,9 @@ module Ci include ::Checksummable include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::OptimisticLocking + include IgnorableColumns + + ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22' belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 16e65aa0fe6..8777d0081cc 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -198,7 +198,7 @@ class Namespace < ApplicationRecord end def any_project_has_container_registry_tags? - all_projects.any?(&:has_container_registry_tags?) + all_projects.includes(:container_repositories).any?(&:has_container_registry_tags?) end def first_project_with_container_registry_tags diff --git a/app/models/release.rb b/app/models/release.rb index 5037a1558ca..95de30523e1 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -24,6 +24,7 @@ class Release < ApplicationRecord before_create :set_released_at validates :project, :tag, presence: true + validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :should_validate_description_length? validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] } @@ -101,6 +102,11 @@ class Release < ApplicationRecord private + def should_validate_description_length? + description_changed? && + ::Feature.enabled?(:validate_release_description_length, project, default_enabled: :yaml) + end + def actual_sha sha || actual_tag&.dereferenced_target end diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b2380a3ba57..61cd8189551 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,6 +2,7 @@ - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 - emails_disabled = @project.emails_disabled? +- cache_enabled = Feature.enabled?(:cache_home_panel, type: :development, default_enabled: :yaml) .project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] } .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3 @@ -23,42 +24,45 @@ - if current_user %span.access-request-links.gl-ml-3 = render 'shared/members/access_request_links', source: @project - - if @project.tag_list.present? - %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center - = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2') - - @project.topics_to_show.each do |topic| - - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2" - - explore_project_topic_path = explore_projects_path(tag: topic) - - if topic.length > max_project_topic_length - %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' } - = topic.titleize - - else - %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' } - = topic.titleize + - if @project.tag_list.present? + = cache_if(cache_enabled, [@project, :tag_list], expires_in: 1.day) do + %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center + = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2') - - if @project.has_extra_topics? - .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil } - = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown } + - @project.topics_to_show.each do |topic| + - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2" + - explore_project_topic_path = explore_projects_path(tag: topic) + - if topic.length > max_project_topic_length + %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' } + = topic.titleize + - else + %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' } + = topic.titleize + - if @project.has_extra_topics? + .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil } + = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown } - .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5 - - if current_user - .gl-display-flex.gl-align-items-start.gl-mr-3 - - if @notification_setting - .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } } + = cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do + .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5 + - if current_user + .gl-display-flex.gl-align-items-start.gl-mr-3 + - if @notification_setting + .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } } - .count-buttons.gl-display-flex.gl-align-items-flex-start - = render 'projects/buttons/star' - = render 'projects/buttons/fork' + .count-buttons.gl-display-flex.gl-align-items-flex-start + = render 'projects/buttons/star' + = render 'projects/buttons/fork' - if can?(current_user, :download_code, @project) - %nav.project-stats - .nav-links.quick-links - - if @project.empty_repo? - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - - else - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = cache_if(cache_enabled, [@project, :download_code], expires_in: 1.minute) do + %nav.project-stats + .nav-links.quick-links + - if @project.empty_repo? + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + - else + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) .home-panel-home-desc.mt-1 - if @project.description.present? @@ -80,11 +84,12 @@ = render_if_exists "projects/home_mirror" - if @project.badges.present? - .project-badges.mb-2 - - @project.badges.each do |badge| - %a.gl-mr-3{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: 'Project badge' }> + = cache_if(cache_enabled, [@project, :badges], expires_in: 1.day) do + .project-badges.mb-2 + - @project.badges.each do |badge| + %a.gl-mr-3{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 2185df3a994..5a6c2c5faaf 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -4,6 +4,8 @@ - page_description @milestone.description_html - add_page_specific_style 'page_bundles/milestone' +- add_page_startup_api_call milestone_tab_path(@milestone, 'issues', show_project_name: false) + = render 'shared/milestones/header', milestone: @milestone = render 'shared/milestones/description', milestone: @milestone diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index ad84ce1d343..18912bf149f 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -43,13 +43,13 @@ = form.label :issues_events, class: 'list-label form-check-label gl-ml-1' do %strong= s_('Webhooks|Issues events') %p.text-muted.gl-ml-1 - = s_('Webhooks|URL is triggered when an issue is created, updated, or merged') + = s_('Webhooks|URL is triggered when an issue is created, updated, closed, or reopened') %li = form.check_box :confidential_issues_events, class: 'form-check-input' = form.label :confidential_issues_events, class: 'list-label form-check-label gl-ml-1' do %strong= s_('Webhooks|Confidential issues events') %p.text-muted.gl-ml-1 - = s_('Webhooks|URL is triggered when a confidential issue is created, updated, or merged') + = s_('Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened') - if @group = render_if_exists 'groups/hooks/member_events', form: form = render_if_exists 'groups/hooks/subgroup_events', form: form diff --git a/changelogs/unreleased/21042-update-username-100.yml b/changelogs/unreleased/21042-update-username-100.yml new file mode 100644 index 00000000000..691c53eceb6 --- /dev/null +++ b/changelogs/unreleased/21042-update-username-100.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 queries in namespace#any_project_has_container_registry_tags? +merge_request: 59916 +author: +type: performance diff --git a/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml b/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml new file mode 100644 index 00000000000..8ee3745d56b --- /dev/null +++ b/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml @@ -0,0 +1,5 @@ +--- +title: Initialize conversion of ci_build_trace_chunks.build_id to bigint +merge_request: 60346 +author: +type: other diff --git a/changelogs/unreleased/home-panel-tag-caching.yml b/changelogs/unreleased/home-panel-tag-caching.yml new file mode 100644 index 00000000000..29b756add9d --- /dev/null +++ b/changelogs/unreleased/home-panel-tag-caching.yml @@ -0,0 +1,5 @@ +--- +title: Cache project tag list +merge_request: 57031 +author: +type: performance diff --git a/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml b/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml new file mode 100644 index 00000000000..0622d642aab --- /dev/null +++ b/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml @@ -0,0 +1,6 @@ +--- +title: Fix copy on webhook admin pages for "Issues events" and "Confidential issues + events" +merge_request: 60453 +author: +type: changed diff --git a/config/feature_flags/development/cache_home_panel.yml b/config/feature_flags/development/cache_home_panel.yml new file mode 100644 index 00000000000..63798cd31d0 --- /dev/null +++ b/config/feature_flags/development/cache_home_panel.yml @@ -0,0 +1,8 @@ +--- +name: cache_home_panel +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57031 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328421 +milestone: '13.12' +type: development +group: group::source code +default_enabled: false diff --git a/config/feature_flags/development/validate_release_description_length.yml b/config/feature_flags/development/validate_release_description_length.yml new file mode 100644 index 00000000000..6a7b3a937ca --- /dev/null +++ b/config/feature_flags/development/validate_release_description_length.yml @@ -0,0 +1,8 @@ +--- +name: validate_release_description_length +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60380 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329192 +milestone: '13.12' +type: development +group: group::release +default_enabled: false diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 1dd58abd9f0..4c0ce5c44b5 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -97,7 +97,7 @@ elsif changelog.optional? message changelog.optional_text end -if changelog.required? || changelog.optional? +if helper.ci? && (changelog.required? || changelog.optional?) checked = 0 git.commits.each do |commit| diff --git a/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb b/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb new file mode 100644 index 00000000000..ec3bb0b7e45 --- /dev/null +++ b/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class InitializeConversionOfCiBuildTraceChunksToBigint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + TABLE = :ci_build_trace_chunks + COLUMNS = %i(build_id) + + def up + initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS) + end + + def down + revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS) + end +end diff --git a/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb b/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb new file mode 100644 index 00000000000..4c656f56a32 --- /dev/null +++ b/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class BackfillCiBuildTraceChunksForBigintConversion < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + TABLE = :ci_build_trace_chunks + COLUMNS = %i(build_id) + + def up + return unless should_run? + + backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS) + end + + def down + return unless should_run? + + revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS) + end + + private + + def should_run? + Gitlab.dev_or_test_env? || Gitlab.com? + end +end diff --git a/db/schema_migrations/20210427045604 b/db/schema_migrations/20210427045604 new file mode 100644 index 00000000000..6cb29994a87 --- /dev/null +++ b/db/schema_migrations/20210427045604 @@ -0,0 +1 @@ +bdeb78403607d45d5eb779623d0e2aa1acf026f6aced6f1134824a35dfec7e74
\ No newline at end of file diff --git a/db/schema_migrations/20210427045711 b/db/schema_migrations/20210427045711 new file mode 100644 index 00000000000..bb713fc08d0 --- /dev/null +++ b/db/schema_migrations/20210427045711 @@ -0,0 +1 @@ +3cd56794ac903d9598863215a34eda62c3dc96bed78bed5b8a99fc522e319b35
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a78b9656f28..f52f5457dd2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -162,6 +162,15 @@ BEGIN END; $$; +CREATE FUNCTION trigger_cf2f9e35f002() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + NEW."build_id_convert_to_bigint" := NEW."build_id"; + RETURN NEW; +END; +$$; + CREATE TABLE audit_events ( id bigint NOT NULL, author_id integer NOT NULL, @@ -10370,7 +10379,8 @@ CREATE TABLE ci_build_trace_chunks ( data_store integer NOT NULL, raw_data bytea, checksum bytea, - lock_version integer DEFAULT 0 NOT NULL + lock_version integer DEFAULT 0 NOT NULL, + build_id_convert_to_bigint bigint DEFAULT 0 NOT NULL ); CREATE SEQUENCE ci_build_trace_chunks_id_seq @@ -24819,6 +24829,8 @@ CREATE TRIGGER trigger_8485e97c00e3 BEFORE INSERT OR UPDATE ON ci_sources_pipeli CREATE TRIGGER trigger_be1804f21693 BEFORE INSERT OR UPDATE ON ci_job_artifacts FOR EACH ROW EXECUTE PROCEDURE trigger_be1804f21693(); +CREATE TRIGGER trigger_cf2f9e35f002 BEFORE INSERT OR UPDATE ON ci_build_trace_chunks FOR EACH ROW EXECUTE PROCEDURE trigger_cf2f9e35f002(); + CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON services FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_issue_tracker(); CREATE TRIGGER trigger_has_external_issue_tracker_on_insert AFTER INSERT ON services FOR EACH ROW WHEN ((((new.category)::text = 'issue_tracker'::text) AND (new.active = true) AND (new.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_issue_tracker(); diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md index 56079fba54b..bd34a82f688 100644 --- a/doc/administration/database_load_balancing.md +++ b/doc/administration/database_load_balancing.md @@ -208,7 +208,7 @@ response timings. ## Primary sticking After a write has been performed, GitLab sticks to using the primary for a -certain period of time, scoped to the user that performed the write. GitLab +certain period of time, scoped to the user that performed the write. GitLab reverts back to using secondaries when they have either caught up, or after 30 seconds. diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index e15be127211..14e5710e1ee 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -1453,7 +1453,7 @@ To determine the current primary Gitaly node for a specific Praefect node: - Use the `Shard Primary Election` [Grafana chart](#grafana) on the [`Gitlab Omnibus - Praefect` dashboard](https://gitlab.com/gitlab-org/grafana-dashboards/-/blob/master/omnibus/praefect.json). This is recommended. - If you do not have Grafana set up, use the following command on each host of each - Praefect node: + Praefect node: ```shell curl localhost:9652/metrics | grep gitaly_praefect_primaries` diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md index 602b7ec220e..c49a2c20ed2 100644 --- a/doc/administration/nfs.md +++ b/doc/administration/nfs.md @@ -22,7 +22,7 @@ file system performance, see ## Gitaly and NFS deprecation WARNING: -From GitLab 14.0, enhancements and bug fixes for NFS for Git repositories are no longer +From GitLab 14.0, enhancements and bug fixes for NFS for Git repositories are no longer considered and customer technical support is considered out of scope. [Read more about Gitaly and NFS](gitaly/index.md#nfs-deprecation-notice) and [the correct mount options to use](#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). diff --git a/doc/administration/operations/rails_console.md b/doc/administration/operations/rails_console.md index c9e5253fbd1..8c366e311b8 100644 --- a/doc/administration/operations/rails_console.md +++ b/doc/administration/operations/rails_console.md @@ -152,7 +152,7 @@ Traceback (most recent call last): In case you encounter a similar error to this: ```plaintext -[root ~]# sudo gitlab-rails runner helloworld.rb +[root ~]# sudo gitlab-rails runner helloworld.rb Please specify a valid ruby command or the path of a script to run. Run 'rails runner -h' for help. diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index e84811db30f..d8c018c83f8 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -310,7 +310,7 @@ and GCS, this transfer is achieved with a copy followed by a delete. With object these deleted temporary upload artifacts are kept as non-current versions, therefore increasing the storage bucket size. To ensure that non-current versions are deleted after a given amount of time, you should configure an object lifecycle policy with your storage provider. - + You can configure the Container Registry to use various storage backends by configuring a storage driver. By default the GitLab Container Registry is configured to use the [file system driver](#use-file-system) @@ -1075,15 +1075,15 @@ If the registry fails to authenticate valid login attempts, you get the followin ```shell # docker login gitlab.company.com:4567 Username: user -Password: +Password: Error response from daemon: login attempt to https://gitlab.company.com:4567/v2/ failed with status: 401 Unauthorized ``` And more specifically, this appears in the `/var/log/gitlab/registry/current` log file: ```plaintext -level=info msg="token signed by untrusted key with ID: "TOKE:NL6Q:7PW6:EXAM:PLET:OKEN:BG27:RCIB:D2S3:EXAM:PLET:OKEN"" -level=warning msg="error authorizing context: invalid token" go.version=go1.12.7 http.request.host="gitlab.company.com:4567" http.request.id=74613829-2655-4f96-8991-1c9fe33869b8 http.request.method=GET http.request.remoteaddr=10.72.11.20 http.request.uri="/v2/" http.request.useragent="docker/19.03.2 go/go1.12.8 git-commit/6a30dfc kernel/3.10.0-693.2.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.2 \(linux\))" +level=info msg="token signed by untrusted key with ID: "TOKE:NL6Q:7PW6:EXAM:PLET:OKEN:BG27:RCIB:D2S3:EXAM:PLET:OKEN"" +level=warning msg="error authorizing context: invalid token" go.version=go1.12.7 http.request.host="gitlab.company.com:4567" http.request.id=74613829-2655-4f96-8991-1c9fe33869b8 http.request.method=GET http.request.remoteaddr=10.72.11.20 http.request.uri="/v2/" http.request.useragent="docker/19.03.2 go/go1.12.8 git-commit/6a30dfc kernel/3.10.0-693.2.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.2 \(linux\))" ``` GitLab uses the contents of the certificate key pair's two sides to encrypt the authentication token diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index fdc23d3e9b3..129386d6ce5 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -34,7 +34,7 @@ full list of reference architectures, see | 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` | - + NOTE: Components marked with * can be optionally run on reputable third party external PaaS PostgreSQL solutions. Google Cloud SQL and AWS RDS are known to work. diff --git a/doc/api/commits.md b/doc/api/commits.md index 117f949aba0..22d98b2b0a6 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -316,7 +316,6 @@ Example response: { "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", "short_id": "8b090c1b", - "title": "Feature added", "author_name": "Example User", "author_email": "user@example.com", "authored_date": "2016-12-12T20:10:39.000+01:00", diff --git a/doc/api/deployments.md b/doc/api/deployments.md index 32d3ab55f9f..cf224ad60ab 100644 --- a/doc/api/deployments.md +++ b/doc/api/deployments.md @@ -266,7 +266,7 @@ Example of response "status": "success", "updated_at": "2016-08-11T07:43:52.143Z", "web_url": "http://gitlab.dev/root/project/pipelines/5" - } + }, "runner": null } } diff --git a/doc/api/discussions.md b/doc/api/discussions.md index 828370c3386..ef3c7e82b19 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -270,7 +270,7 @@ GET /projects/:id/snippets/:snippet_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Snippet", - "noteable_id": null + "noteable_iid": null }, { "id": 1129, @@ -290,7 +290,7 @@ GET /projects/:id/snippets/:snippet_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Snippet", - "noteable_id": null, + "noteable_iid": null, "resolvable": false } ] @@ -317,7 +317,7 @@ GET /projects/:id/snippets/:snippet_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Snippet", - "noteable_id": null, + "noteable_iid": null, "resolvable": false } ] @@ -476,7 +476,7 @@ GET /groups/:id/epics/:epic_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Epic", - "noteable_id": null, + "noteable_iid": null, "resolvable": false }, { @@ -497,7 +497,7 @@ GET /groups/:id/epics/:epic_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Epic", - "noteable_id": null, + "noteable_iid": null, "resolvable": false } ] @@ -524,7 +524,7 @@ GET /groups/:id/epics/:epic_id/discussions "system": false, "noteable_id": 3, "noteable_type": "Epic", - "noteable_id": null, + "noteable_iid": null, "resolvable": false } ] @@ -757,7 +757,7 @@ Diff comments also contain position: "notes": [ { "id": 1128, - "type": DiffNote, + "type": "DiffNote", "body": "diff comment", "attachment": null, "author": { @@ -787,12 +787,12 @@ Diff comments also contain position: "line_range": { "start": { "line_code": "588440f66559714280628a4f9799f0c4eb880a4a_10_10", - "type": "new", + "type": "new" }, "end": { "line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11", "type": "old" - }, + } } }, "resolved": false, @@ -1089,7 +1089,7 @@ Diff comments contain also position: "notes": [ { "id": 1128, - "type": DiffNote, + "type": "DiffNote", "body": "diff comment", "attachment": null, "author": { diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md index 228f68531fd..8198130c61e 100644 --- a/doc/api/epic_links.md +++ b/doc/api/epic_links.md @@ -97,7 +97,7 @@ Example response: "id": 6, "iid": 38, "group_id": 1, - "parent_id": 5 + "parent_id": 5, "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", "author": { diff --git a/doc/api/events.md b/doc/api/events.md index befc1164432..5664d97d7db 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -301,7 +301,7 @@ Example response: ```json [ { - "id": 8 + "id": 8, "title":null, "project_id":1, "action_name":"opened", diff --git a/doc/api/feature_flag_user_lists.md b/doc/api/feature_flag_user_lists.md index e0a5fe99663..af8b3fcc71e 100644 --- a/doc/api/feature_flag_user_lists.md +++ b/doc/api/feature_flag_user_lists.md @@ -126,7 +126,7 @@ Example response: "iid": 1, "project_id": 1, "created_at": "2020-02-04T08:13:10.507Z", - "updated_at": "2020-02-04T08:13:10.507Z", + "updated_at": "2020-02-04T08:13:10.507Z" } ``` diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md index fa5481b12a7..50cb9b1141e 100644 --- a/doc/api/feature_flags.md +++ b/doc/api/feature_flags.md @@ -70,7 +70,7 @@ Example response: "version": "new_version_flag", "created_at":"2019-11-04T08:13:10.507Z", "updated_at":"2019-11-04T08:13:10.507Z", - "scopes":[] + "scopes":[], "strategies": [ { "id": 2, diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index 38f11d8dfe2..7926aeeeed7 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -362,9 +362,6 @@ Example response: "wikis_checksum_mismatch_count": 1, "repositories_retrying_verification_count": 1, "wikis_retrying_verification_count": 3, - "repositories_checked_count": 7, - "repositories_checked_failed_count": 2, - "repositories_checked_in_percentage": "17.07%", "last_event_id": 23, "last_event_timestamp": 1509681166, "cursor_last_event_id": null, diff --git a/doc/api/graphql/users_example.md b/doc/api/graphql/users_example.md index 78703c3ce89..1222cd8ee8e 100644 --- a/doc/api/graphql/users_example.md +++ b/doc/api/graphql/users_example.md @@ -68,7 +68,7 @@ explorer. GraphiQL explorer is available for: } } } - } + } ``` 1. Open the [GraphiQL explorer tool](https://gitlab.com/-/graphql-explorer). diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md index 881054ef44b..848d5735096 100644 --- a/doc/api/group_badges.md +++ b/doc/api/group_badges.md @@ -192,6 +192,6 @@ Example response: "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", - "rendered_image_url": "https://shields.io/my/badge", + "rendered_image_url": "https://shields.io/my/badge" } ``` diff --git a/doc/api/groups.md b/doc/api/groups.md index 728d5149e73..b1e1e2bb6c5 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -101,7 +101,7 @@ GET /groups?statistics=true "lfs_objects_size" : 123, "job_artifacts_size" : 57, "packages_size": 0, - "snippets_size" : 50, + "snippets_size" : 50 } } ] diff --git a/doc/api/instance_clusters.md b/doc/api/instance_clusters.md index 99717ba939d..1c4996975f7 100644 --- a/doc/api/instance_clusters.md +++ b/doc/api/instance_clusters.md @@ -84,7 +84,7 @@ Example response: }, "provider_gcp": null, "management_project": null - } + }, { "id": 11, "name": "cluster-3", diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md index 25d85577c13..e6e5a0b3d25 100644 --- a/doc/api/issue_links.md +++ b/doc/api/issue_links.md @@ -117,7 +117,7 @@ Example response: "due_date": null, "web_url": "http://example.com/example/example/issues/11", "confidential": false, - "weight": null, + "weight": null }, "target_issue" : { "id" : 84, @@ -147,7 +147,7 @@ Example response: "due_date": null, "web_url": "http://example.com/example/example/issues/14", "confidential": false, - "weight": null, + "weight": null }, "link_type": "relates_to" } @@ -198,7 +198,7 @@ DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id "due_date": null, "web_url": "http://example.com/example/example/issues/11", "confidential": false, - "weight": null, + "weight": null }, "target_issue" : { "id" : 84, @@ -228,7 +228,7 @@ DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id "due_date": null, "web_url": "http://example.com/example/example/issues/14", "confidential": false, - "weight": null, + "weight": null }, "link_type": "relates_to" } diff --git a/doc/api/issues.md b/doc/api/issues.md index 79b1fc78108..a9a3f04a064 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -591,83 +591,79 @@ Example response: ```json { - "id" : 1, - "milestone" : { - "due_date" : null, - "project_id" : 4, - "state" : "closed", - "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.", - "iid" : 3, - "id" : 11, - "title" : "v3.0", - "created_at" : "2016-01-04T15:31:39.788Z", - "updated_at" : "2016-01-04T15:31:39.788Z", - "closed_at" : "2016-01-05T15:31:46.176Z" - }, - "author" : { - "state" : "active", - "web_url" : "https://gitlab.example.com/root", - "avatar_url" : null, - "username" : "root", - "id" : 1, - "name" : "Administrator" - }, - "description" : "Omnis vero earum sunt corporis dolor et placeat.", - "state" : "closed", - "iid" : 1, - "assignees" : [{ - "avatar_url" : null, - "web_url" : "https://gitlab.example.com/lennie", - "state" : "active", - "username" : "lennie", - "id" : 9, - "name" : "Dr. Luella Kovacek" - }], - "assignee" : { - "avatar_url" : null, - "web_url" : "https://gitlab.example.com/lennie", - "state" : "active", - "username" : "lennie", - "id" : 9, - "name" : "Dr. Luella Kovacek" - }, - "labels" : [], - "upvotes": 4, - "downvotes": 0, - "merge_requests_count": 0, - "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", - "updated_at" : "2016-01-04T15:31:46.176Z", - "created_at" : "2016-01-04T15:31:46.176Z", - "closed_at" : null, - "closed_by" : null, - "subscribed": false, - "user_notes_count": 1, - "due_date": null, - "web_url": "http://example.com/my-group/my-project/issues/1", - "references": { - "short": "#1", - "relative": "#1", - "full": "my-group/my-project#1" - }, - "time_stats": { - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": null, - "human_total_time_spent": null - }, - "confidential": false, - "discussion_locked": false, - "_links": { - "self": "http://example.com/api/v4/projects/1/issues/2", - "notes": "http://example.com/api/v4/projects/1/issues/2/notes", - "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", - "project": "http://example.com/api/v4/projects/1" - }, - "task_completion_status":{ - "count":0, - "completed_count":0 - }, - "weight": null, + "id": 1, + "milestone": { + "due_date": null, + "project_id": 4, + "state": "closed", + "description": "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.", + "iid": 3, + "id": 11, + "title": "v3.0", + "created_at": "2016-01-04T15:31:39.788Z", + "updated_at": "2016-01-04T15:31:39.788Z", + "closed_at": "2016-01-05T15:31:46.176Z" + }, + "author": { + "state": "active", + "web_url": "https://gitlab.example.com/root", + "avatar_url": null, + "username": "root", + "id": 1, + "name": "Administrator" + }, + "description": "Omnis vero earum sunt corporis dolor et placeat.", + "state": "closed", + "iid": 1, + "assignees": [ + { + "avatar_url": null, + "web_url": "https://gitlab.example.com/lennie", + "state": "active", + "username": "lennie", + "id": 9, + "name": "Dr. Luella Kovacek" + } + ], + "assignee": { + "avatar_url": null, + "web_url": "https://gitlab.example.com/lennie", + "state": "active", + "username": "lennie", + "id": 9, + "name": "Dr. Luella Kovacek" + }, + "labels": [], + "upvotes": 4, + "downvotes": 0, + "merge_requests_count": 0, + "title": "Ut commodi ullam eos dolores perferendis nihil sunt.", + "updated_at": "2016-01-04T15:31:46.176Z", + "created_at": "2016-01-04T15:31:46.176Z", + "closed_at": null, + "closed_by": null, + "subscribed": false, + "user_notes_count": 1, + "due_date": null, + "web_url": "http://example.com/my-group/my-project/issues/1", + "references": { + "short": "#1", + "relative": "#1", + "full": "my-group/my-project#1" + }, + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "confidential": false, + "discussion_locked": false, + "task_completion_status": { + "count": 0, + "completed_count": 0 + }, + "weight": null, "has_tasks": false, "_links": { "self": "http://gitlab.example:3000/api/v4/projects/1/issues/1", @@ -675,12 +671,6 @@ Example response: "award_emoji": "http://gitlab.example:3000/api/v4/projects/1/issues/1/award_emoji", "project": "http://gitlab.example:3000/api/v4/projects/1" }, - "references": { - "short": "#1", - "relative": "#1", - "full": "gitlab-org/gitlab-test#1" - }, - "subscribed": true, "moved_to_id": null, "service_desk_reply_to": "service.desk@gitlab.com", "epic_iid": null, diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 97a155bce06..375d52c3c58 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -68,7 +68,6 @@ Example of response "status": "pending" }, "ref": "master", - "artifacts": [], "runner": null, "stage": "test", "status": "failed", diff --git a/doc/api/keys.md b/doc/api/keys.md index 98159bcf027..99c9745d8f2 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -31,7 +31,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a "title": "Sample key 25", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "created_at": "2015-09-03T07:24:44.627Z", - "expires_at": "2020-05-05T00:00:00.000Z" + "expires_at": "2020-05-05T00:00:00.000Z", "user": { "name": "John Smith", "username": "john_smith", @@ -59,7 +59,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a "identities": [], "can_create_group": true, "can_create_project": true, - "two_factor_enabled": false + "two_factor_enabled": false, "external": false, "private_profile": null } @@ -100,7 +100,7 @@ Example response: "title": "Sample key 1", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "created_at": "2019-11-14T15:11:13.222Z", - "expires_at": "2020-05-05T00:00:00.000Z" + "expires_at": "2020-05-05T00:00:00.000Z", "user": { "id": 1, "name": "Administrator", diff --git a/doc/api/license.md b/doc/api/license.md index ceaa471c25f..b69a5e81ea6 100644 --- a/doc/api/license.md +++ b/doc/api/license.md @@ -80,7 +80,7 @@ GET /licenses "Name": "Doe John" }, "add_ons": { - "GitLab_FileLocks": 1, + "GitLab_FileLocks": 1 } } ] diff --git a/doc/api/members.md b/doc/api/members.md index adfe2df8f30..4c740247a70 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -136,7 +136,7 @@ Example response: "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", "web_url": "http://192.168.1.8:3000/root", "expires_at": "2012-10-22T14:13:35Z", - "access_level": 30 + "access_level": 30, "email": "john@example.com", "group_saml_identity": { "extern_uid":"ABC-1234567890", diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index 8f2a6bd3d00..6625742e312 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -664,7 +664,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/approvals "web_url": "http://localhost:3000/root" } } - ], + ] } ``` @@ -1109,7 +1109,7 @@ does not match, the response code is `409`. "web_url": "http://localhost:3000/ryley" } } - ], + ] } ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index d28c7d8e8a7..fcfcc58231b 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -2319,7 +2319,7 @@ Example response: "short": "!1", "relative": "!1", "full": "my-group/my-project!1" - }, + } }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7", "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.", diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index 037b9053c3d..298c0ead8c1 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -176,7 +176,9 @@ Example responses: { "level": "watch" } +``` +```json { "level": "custom", "events": { diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md index 67529adee93..6b3b6f4f36b 100644 --- a/doc/api/pipeline_schedules.md +++ b/doc/api/pipeline_schedules.md @@ -370,8 +370,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "value=u ```json { "key": "NEW_VARIABLE", - "value": "updated value" - "variable_type": "env_var", + "value": "updated value", + "variable_type": "env_var" } ``` diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 44129b44173..497c70b19ba 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -60,7 +60,7 @@ Example of response "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "web_url": "https://example.com/foo/bar/pipelines/47", "created_at": "2016-08-11T11:28:34.085Z", - "updated_at": "2016-08-11T11:32:35.169Z", + "updated_at": "2016-08-11T11:32:35.169Z" }, { "id": 48, @@ -70,7 +70,7 @@ Example of response "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", "web_url": "https://example.com/foo/bar/pipelines/48", "created_at": "2016-08-12T10:06:04.561Z", - "updated_at": "2016-08-12T10:09:56.223Z", + "updated_at": "2016-08-12T10:09:56.223Z" } ] ``` diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md index 58f5b000958..a17f7d15e76 100644 --- a/doc/api/project_badges.md +++ b/doc/api/project_badges.md @@ -59,7 +59,7 @@ Example response: "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", "rendered_image_url": "https://shields.io/my/badge", "kind": "group" - }, + } ] ``` @@ -202,6 +202,6 @@ Example response: "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", - "rendered_image_url": "https://shields.io/my/badge", + "rendered_image_url": "https://shields.io/my/badge" } ``` diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index c895a7c4155..b95f8c2984d 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -104,7 +104,7 @@ an email notifying the user to download the file, uploading the exported file to "export_status": "finished", "_links": { "api_url": "https://gitlab.example.com/api/v4/projects/1/export/download", - "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/download_export", + "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/download_export" } } ``` diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md index 94aeb665c7f..dd8954f2f0f 100644 --- a/doc/api/project_repository_storage_moves.md +++ b/doc/api/project_repository_storage_moves.md @@ -68,6 +68,7 @@ Example response: "path": "project1", "path_with_namespace": "namespace1/project1", "created_at": "2020-05-07T04:27:17.016Z" + } } ] ``` @@ -111,6 +112,7 @@ Example response: "path": "project1", "path_with_namespace": "namespace1/project1", "created_at": "2020-05-07T04:27:17.016Z" + } } ] ``` @@ -150,6 +152,7 @@ Example response: "path": "project1", "path_with_namespace": "namespace1/project1", "created_at": "2020-05-07T04:27:17.016Z" + } } ``` @@ -189,6 +192,7 @@ Example response: "path": "project1", "path_with_namespace": "namespace1/project1", "created_at": "2020-05-07T04:27:17.016Z" + } } ``` @@ -237,6 +241,7 @@ Example response: "path": "project1", "path_with_namespace": "namespace1/project1", "created_at": "2020-05-07T04:27:17.016Z" + } } ``` diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index fc8882be283..070429eafd5 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -114,7 +114,7 @@ curl --request POST "https://gitlab.com/api/v4/projects/:id/snippets" \ "files": [ { "file_path": "example.txt", - "content" : "source code \n with multiple lines\n", + "content" : "source code \n with multiple lines\n" } ] } diff --git a/doc/api/projects.md b/doc/api/projects.md index 2961cb725b3..db8694d80a1 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -94,7 +94,7 @@ When `simple=true` or the user is unauthenticated this returns something like: "last_activity_at": "2013-09-30T13:46:02Z", "forks_count": 0, "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", - "star_count": 0, + "star_count": 0 }, { "id": 6, @@ -189,7 +189,7 @@ When the user is authenticated and `simple` is not set this returns something li "labels": "http://example.com/api/v4/projects/1/labels", "events": "http://example.com/api/v4/projects/1/events", "members": "http://example.com/api/v4/projects/1/members" - }, + } }, { "id": 6, @@ -902,7 +902,6 @@ GET /projects/:id "merge_method": "merge", "auto_devops_enabled": true, "auto_devops_deploy_strategy": "continuous", - "repository_storage": "default", "approvals_before_merge": 0, "mirror": false, "mirror_user_id": 45, @@ -986,7 +985,7 @@ If the project is a fork, and you provide a valid token to authenticate, the "name": "MIT License", "nickname": null, "html_url": "http://choosealicense.com/licenses/mit/", - "source_url": "https://opensource.org/licenses/MIT", + "source_url": "https://opensource.org/licenses/MIT" }, "star_count":3812, "forks_count":3561, @@ -1661,26 +1660,26 @@ Example responses: [ { "starred_since": "2019-01-28T14:47:30.642Z", - "user": - { + "user": { "id": 1, "username": "jane_smith", "name": "Jane Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "web_url": "http://localhost:3000/jane_smith" - } + } }, + { "starred_since": "2018-01-02T11:40:26.570Z", - "user": - { - "id": 2, - "username": "janine_smith", - "name": "Janine Smith", - "state": "blocked", - "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", - "web_url": "http://localhost:3000/janine_smith" - } + "user": { + "id": 2, + "username": "janine_smith", + "name": "Janine Smith", + "state": "blocked", + "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", + "web_url": "http://localhost:3000/janine_smith" + } + } ] ``` diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index ff66f41b4e8..e4942b34a49 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -144,9 +144,9 @@ Example response: }, "evidences":[ { - sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", - filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json", - collected_at: "2019-01-03T01:56:19.539Z" + "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", + "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json", + "collected_at": "2019-01-03T01:56:19.539Z" } ] }, @@ -208,9 +208,9 @@ Example response: }, "evidences":[ { - sha: "c3ffedec13af470e760d6cdfb08790f71cf52c6cde4d", - filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json", - collected_at: "2019-01-03T01:55:18.203Z" + "sha": "c3ffedec13af470e760d6cdfb08790f71cf52c6cde4d", + "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json", + "collected_at": "2019-01-03T01:55:18.203Z" } ] } @@ -340,9 +340,9 @@ Example response: }, "evidences":[ { - sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", - filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json", - collected_at: "2019-07-16T14:00:12.256Z" + "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", + "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json", + "collected_at": "2019-07-16T14:00:12.256Z" } ] } @@ -482,7 +482,7 @@ Example response: } ], "evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.3/evidence.json" - }, + } } ``` @@ -625,7 +625,7 @@ Example response: ], "evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json" - }, + } } ``` @@ -709,7 +709,7 @@ Example response: ], "evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json" - }, + } } ``` diff --git a/doc/api/runners.md b/doc/api/runners.md index f9febaef305..1f0209c3cae 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -126,7 +126,7 @@ Example response: "ip_address": "127.0.0.1", "is_shared": true, "name": null, - "online": false + "online": false, "status": "offline" }, { @@ -136,7 +136,7 @@ Example response: "ip_address": "127.0.0.1", "is_shared": false, "name": null, - "online": true + "online": true, "status": "paused" }, { @@ -428,7 +428,7 @@ Example response: "ip_address": "127.0.0.1", "is_shared": true, "name": null, - "online": true + "online": true, "status": "paused" } ] diff --git a/doc/api/services.md b/doc/api/services.md index d42752848c5..27e057081bc 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -42,7 +42,7 @@ Example response: "wiki_page_events": true, "job_events": true, "comment_on_event_enabled": true - } + }, { "id": 76, "title": "Alerts endpoint", diff --git a/doc/api/users.md b/doc/api/users.md index 0e4012935f9..228194f10fa 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1762,6 +1762,6 @@ Example response: "source_name": "Group three", "source_type": "Namespace", "access_level": "20" - }, + } ] ``` diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md index e38d9031ffd..6fda6bb0d8b 100644 --- a/doc/ci/environments/deployment_safety.md +++ b/doc/ci/environments/deployment_safety.md @@ -154,7 +154,7 @@ If you have multiple jobs for the same environment (including non-deployment job build:service-a: environment: name: production - + build:service-b: environment: name: production diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index d4b9282ac38..874a83f94d9 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -269,7 +269,7 @@ which pipelines can run. resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name') busy_resources = resource_group.resources.where('build_id IS NOT NULL') -# identify which builds are occupying the resource +# identify which builds are occupying the resource # (I think it should be 1 as of today) busy_resources.pluck(:build_id) diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md index 0233e9f9c3c..2d2ed6e8b1b 100644 --- a/doc/development/cascading_settings.md +++ b/doc/development/cascading_settings.md @@ -26,7 +26,7 @@ Settings are not cascading by default. To define a cascading setting, take the f ```ruby class NamespaceSetting include CascadingNamespaceSettingAttribute - + cascading_attr :delayed_project_removal end ``` @@ -40,11 +40,11 @@ Settings are not cascading by default. To define a cascading setting, take the f ```ruby class AddDelayedProjectRemovalCascadingSetting < ActiveRecord::Migration[6.0] include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings - + def up add_cascading_namespace_setting :delayed_project_removal, :boolean, default: false, null: false end - + def down remove_cascading_namespace_setting :delayed_project_removal end @@ -100,7 +100,7 @@ cascaded value using the following criteria: ### `_locked?` method By default, the `_locked?` method (`delayed_project_removal_locked?`) returns -`true` if an ancestor of the group or application setting locks the attribute. +`true` if an ancestor of the group or application setting locks the attribute. It returns `false` when called from the group that locked the attribute. When `include_self: true` is specified, it returns `true` when called from the group that locked the attribute. diff --git a/doc/development/gemfile.md b/doc/development/gemfile.md index f50e19bc383..fcb317e1e88 100644 --- a/doc/development/gemfile.md +++ b/doc/development/gemfile.md @@ -19,6 +19,63 @@ dependencies and build times. Refer to [licensing guidelines](licensing.md) for ensuring license compliance. +## GitLab-created gems + +Sometimes we create libraries within our codebase that we want to +extract, either because we want to use them in other applications +ourselves, or because we think it would benefit the wider community. +Extracting code to a gem also means that we can be sure that the gem +does not contain any hidden dependencies on our application code. + +In general, we want to think carefully before doing this as there are +also disadvantages: + +1. Gems - even those maintained by GitLab - do not necessarily go + through the same [code review process](code_review.md) as the main + Rails application. +1. Extracting the code into a separate project means that we need a + minimum of two merge requests to change functionality: one in the gem + to make the functional change, and one in the Rails app to bump the + version. +1. Our needs for our own usage of the gem may not align with the wider + community's needs. In general, if we are not using the latest version + of our own gem, that might be a warning sign. + +In the case where we do want to extract some library code we've written +to a gem, go through these steps: + +1. Start with the code in the Rails application. Here it's fine to have + the code in `lib/` and loaded automatically. We can skip this step if + the step below makes more sense initially. +1. Before extracting to its own project, move the gem to `vendor/gems` and + load it in the `Gemfile` using the `path` option. This gives us a gem + that can be published to RubyGems.org, with its own test suite and + isolated set of dependencies, that is still in our main code tree and + goes through the standard code review process. + - For an example, see the [merge request !57805](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57805). +1. Once the gem is stable - we have been using it in production for a + while with few, if any, changes - extract to its own project under + the `gitlab-org` namespace. + 1. When creating the project, follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/#creating-a-new-project). + 1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/#cicd-configuration). + 1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/#publishing-a-project). + - See [issue + #325463](https://gitlab.com/gitlab-org/gitlab/-/issues/325463) + for an example. + - In some cases we may want to move a gem to its own namespace. Some + examples might be that it will naturally have more than one project + (say, something that has plugins as separate libraries), or that we + expect non-GitLab-team-members to be maintainers on this project as + well as GitLab team members. + + The latter situation (maintainers from outside GitLab) could also + apply if someone who currently works at GitLab wants to maintain + the gem beyond their time working at GitLab. + +When publishing a gem to RubyGems.org, also note the section on [gem +owners](https://about.gitlab.com/handbook/developer-onboarding/#ruby-gems) +in the handbook. + ## Upgrade Rails When upgrading the Rails gem and its dependencies, you also should update the following: diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md index 0f696d39092..6ce372ebc0d 100644 --- a/doc/development/merge_request_performance_guidelines.md +++ b/doc/development/merge_request_performance_guidelines.md @@ -162,7 +162,22 @@ query. This in turn makes it much harder for this code to overload a database. In a DB cluster we have many read replicas and one primary. A classic use of scaling the DB is to have read-only actions be performed by the replicas. We use [load balancing](../administration/database_load_balancing.md) to distribute this load. This allows for the replicas to grow as the pressure on the DB grows. -By default, queries use read-only replicas, but due to [primary sticking](../administration/database_load_balancing.md#primary-sticking), GitLab sticks to using the primary for a certain period of time and reverts back to secondaries after they have either caught up or after 30 seconds, which can lead to a considerable amount of unnecessary load on the primary. In this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56849) we introduced the `without_sticky_writes` block to prevent switching to the primary. This [merge request example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57328) provides a good use case for when queries can stick to the primary and how to prevent this by using `without_sticky_writes`. +By default, queries use read-only replicas, but due to +[primary sticking](../administration/database_load_balancing.md#primary-sticking), GitLab uses the +primary for some time and reverts to secondaries after they have either caught up or after 30 seconds. +Doing this can lead to a considerable amount of unnecessary load on the primary. +To prevent switching to the primary [merge request 56849](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56849) introduced the +`without_sticky_writes` block. Typically, this method can be applied to prevent primary stickiness +after a trivial or insignificant write which doesn't affect the following queries in the same session. + +To learn when a usage timestamp update can lead the session to stick to the primary and how to +prevent it by using `without_sticky_writes`, see [merge request 57328](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57328) + +As a counterpart of the `without_sticky_writes` utility, +[merge request 59167](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59167) introduced +`use_replicas_for_read_queries`. This method forces all read-only queries inside its block to read +replicas regardless of the current primary stickiness. +This utility is reserved for cases where queries can tolerate replication lag. Internally, our database load balancer classifies the queries based on their main statement (`select`, `update`, `delete`, etc.). When in doubt, it redirects the queries to the primary database. Hence, there are some common cases the load balancer sends the queries to the primary unnecessarily: @@ -171,7 +186,12 @@ Internally, our database load balancer classifies the queries based on their mai - In-flight connection configuration set - Sidekiq background jobs -Worse, after the above queries are executed, GitLab [sticks to the primary](../administration/database_load_balancing.md#primary-sticking). In [this merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56476), we introduced `use_replica_if_possible` to make the inside queries prefer to use the replicas. That MR is also an example how we redirected a costly, time-consuming query to the replicas. +After the above queries are executed, GitLab +[sticks to the primary](../administration/database_load_balancing.md#primary-sticking). +To make the inside queries prefer using the replicas, +[merge request 59086](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59086) introduced +`fallback_to_replicas_for_ambiguous_queries`. This MR is also an example of how we redirected a +costly, time-consuming query to the replicas. ## Use CTEs wisely diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 41b5d3179ed..51724d32f3a 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -451,7 +451,7 @@ expect(page).to have_current_path 'gitlab/gitlab-test/-/issues' expect(page).to have_title 'Not Found' -# acceptable when a more specific matcher above is not possible +# acceptable when a more specific matcher above is not possible expect(page).to have_css 'h2', text: 'Issue title' expect(page).to have_css 'p', text: 'Issue description', exact: true expect(page).to have_css '[data-testid="weight"]', text: 2 diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md index 09c388eb115..0e12c620a8a 100644 --- a/doc/development/usage_ping/index.md +++ b/doc/development/usage_ping/index.md @@ -248,12 +248,12 @@ To deprecate a metric: end end ``` - + 1. Update the Metrics Dictionary following [guidelines instructions](dictionary.md). ### 4. Remove a metric -Only deprecated metrics can be removed from Usage Ping. +Only deprecated metrics can be removed from Usage Ping. For an example of the metric removal process take a look at this [example issue](https://gitlab.com/gitlab-org/gitlab/-/issues/297029) @@ -262,9 +262,9 @@ To remove a deprecated metric: 1. Verify that removing the metric from the Usage Ping payload does not cause errors in [Version App](https://gitlab.com/gitlab-services/version-gitlab-com) when the updated payload is collected and processed. Version App collects - and persists all Usage Ping reports. To do that you can modify + and persists all Usage Ping reports. To do that you can modify [fixtures](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/spec/support/usage_data_helpers.rb#L540) - used to test + used to test [`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75) endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload. @@ -273,7 +273,7 @@ To remove a deprecated metric: Ask for confirmation that the metric is not referred to in any SiSense dashboards and can be safely removed from Usage Ping. Use this [example issue](https://gitlab.com/gitlab-data/analytics/-/issues/7539) for guidance. - This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric) + This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric) reported that metric is not required by any data transformation in Snowflake data warehouse nor it is used by any of SiSense dashboards. @@ -288,15 +288,15 @@ To remove a deprecated metric: instances might not immediately update to the latest version of GitLab, and therefore continue to report the removed metric. The Product Intelligence team requires a record of all removed metrics in order to identify and filter them. - + For example please take a look at this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60149/diffs#b01f429a54843feb22265100c0e4fec1b7da1240_10_10). - + 1. After you verify the metric can be safely removed, remove the metric's instrumentation from [`lib/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb) or [`ee/lib/ee/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/ee/gitlab/usage_data.rb). - + For example please take a look at this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60149/diffs#6335dc533bd21df26db9de90a02dd66278c2390d_167_167). 1. Remove any other records related to the metric: diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md index 100bb71cc8c..2fca70fd07a 100644 --- a/doc/install/azure/index.md +++ b/doc/install/azure/index.md @@ -190,7 +190,7 @@ To set up the GitLab external URL: 1. Open `/etc/gitlab/gitlab.rb` with your editor. 1. Find `external_url` and replace it with your own domain name. For the sake of this example, use the default domain name Azure sets up. - Using `https` in the URL + Using `https` in the URL [automatically enables](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), Let's Encrypt, and sets HTTPS by default: diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 468d068725d..da1278b9edd 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -442,7 +442,7 @@ in the OmniAuth [`info` hash](https://github.com/omniauth/omniauth/wiki/Auth-Has For example, if your SAMLResponse contains an Attribute called `EmailAddress`, specify `{ email: ['EmailAddress'] }` to map the Attribute to the -corresponding key in the `info` hash. URI-named Attributes are also supported, for example, +corresponding key in the `info` hash. URI-named Attributes are also supported, for example, `{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`. This setting allows you tell GitLab where to look for certain attributes required @@ -859,7 +859,7 @@ For this you need take the following into account: the request to contain one. In this case the fingerprint or fingerprint validators are optional -If none of the above described scenarios is valid, the request +If none of the above described scenarios is valid, the request fails with one of the mentioned errors. ### User is blocked when signing in through SAML diff --git a/doc/ssh/README.md b/doc/ssh/README.md index b649ae3430f..358323e4ef5 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -325,7 +325,7 @@ If you are using [EGit](https://www.eclipse.org/egit/), you can [add your SSH ke If you're running Windows 10, you can either use the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) with [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install-win10#update-to-wsl-2) which -has both `git` and `ssh` preinstalled, or install [Git for Windows](https://gitforwindows.org) to +has both `git` and `ssh` preinstalled, or install [Git for Windows](https://gitforwindows.org) to use SSH through Powershell. The SSH key generated in WSL is not directly available for Git for Windows, and vice versa, diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index d4551edddcd..4978e46cccf 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -100,7 +100,7 @@ To use the KAS: ### Define a configuration repository -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository. To configure an Agent, you need: diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index 8e3718f3255..c5f9d3dd47b 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -318,7 +318,7 @@ npmScopes: foo: npmRegistryServer: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/" npmPublishRegistry: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/" - + npmRegistries: //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/: npmAlwaysAuth: true diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md index 712bb3cc954..8be69cdeb79 100644 --- a/doc/user/project/import/svn.md +++ b/doc/user/project/import/svn.md @@ -44,9 +44,9 @@ directly in a file system level. The first step to mirror you SVN repository in GitLab is to create a new empty project that is used as a mirror. For Omnibus installations the path to -the repository is +the repository is `/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For -installations from source, the default repository directory is +installations from source, the default repository directory is `/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a variable: diff --git a/lib/gitlab/database/background_migration/batch_optimizer.rb b/lib/gitlab/database/background_migration/batch_optimizer.rb index 033dd5d54e1..7523c828399 100644 --- a/lib/gitlab/database/background_migration/batch_optimizer.rb +++ b/lib/gitlab/database/background_migration/batch_optimizer.rb @@ -17,22 +17,26 @@ module Gitlab class BatchOptimizer # Target time efficiency for a job # Time efficiency is defined as: job duration / interval - TARGET_EFFICIENCY = (0.8..0.98).freeze + TARGET_EFFICIENCY = (0.9..0.95).freeze # Lower and upper bound for the batch size - ALLOWED_BATCH_SIZE = (1_000..1_000_000).freeze + ALLOWED_BATCH_SIZE = (1_000..2_000_000).freeze - # Use this batch_size multiplier to increase batch size - INCREASE_MULTIPLIER = 1.1 + # Limit for the multiplier of the batch size + MAX_MULTIPLIER = 1.2 - # Use this batch_size multiplier to decrease batch size - DECREASE_MULTIPLIER = 0.8 + # When smoothing time efficiency, use this many jobs + NUMBER_OF_JOBS = 20 - attr_reader :migration, :number_of_jobs + # Smoothing factor for exponential moving average + EMA_ALPHA = 0.4 - def initialize(migration, number_of_jobs: 10) + attr_reader :migration, :number_of_jobs, :ema_alpha + + def initialize(migration, number_of_jobs: NUMBER_OF_JOBS, ema_alpha: EMA_ALPHA) @migration = migration @number_of_jobs = number_of_jobs + @ema_alpha = ema_alpha end def optimize! @@ -47,20 +51,15 @@ module Gitlab private def batch_size_multiplier - efficiency = migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) + efficiency = migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs, alpha: ema_alpha) - return unless efficiency + return if efficiency.nil? || efficiency == 0 - if TARGET_EFFICIENCY.include?(efficiency) - # We hit the range - no change - nil - elsif efficiency > TARGET_EFFICIENCY.max - # We're above the range - decrease by 20% - DECREASE_MULTIPLIER - else - # We're below the range - increase by 10% - INCREASE_MULTIPLIER - end + # We hit the range - no change + return if TARGET_EFFICIENCY.include?(efficiency) + + # Assumption: time efficiency is linear in the batch size + [TARGET_EFFICIENCY.max / efficiency, MAX_MULTIPLIER].min end end end diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb index bef3fc7b504..bc0126cd893 100644 --- a/lib/gitlab/usage_data_counters/editor_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb @@ -50,7 +50,6 @@ module Gitlab private def track_unique_action(action, author, time) - return unless Feature.enabled?(:track_editor_edit_actions, default_enabled: true) return unless author Gitlab::UsageDataCounters::HLLRedisCounter.track_event(action, values: author.id, time: time) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 00c749e85d5..6c71d4b9807 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8287,6 +8287,9 @@ msgstr "" msgid "ComplianceFramework|This project is regulated by %{framework}." msgstr "" +msgid "Component" +msgstr "" + msgid "Confidence" msgstr "" @@ -10354,6 +10357,9 @@ msgstr "" msgid "Data is still calculating..." msgstr "" +msgid "Data type" +msgstr "" + msgid "Database update failed" msgstr "" @@ -14436,6 +14442,9 @@ msgstr "" msgid "GeoNodes|secondary nodes" msgstr "" +msgid "Geo|%{component} synced" +msgstr "" + msgid "Geo|%{itemTitle} checksum progress" msgstr "" @@ -14655,16 +14664,13 @@ msgstr "" msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?" msgstr "" -msgid "Geo|Replicated data is verified with the secondary node(s) using checksums." -msgstr "" - -msgid "Geo|Replication Details" +msgid "Geo|Replicated data is verified with the secondary node(s) using checksums" msgstr "" -msgid "Geo|Replication Details Desktop" +msgid "Geo|Replicated data is verified with the secondary node(s) using checksums." msgstr "" -msgid "Geo|Replication Details Mobile" +msgid "Geo|Replication Details" msgstr "" msgid "Geo|Replication details" @@ -14733,6 +14739,9 @@ msgstr "" msgid "Geo|Synchronization settings" msgstr "" +msgid "Geo|Synchronization status" +msgstr "" + msgid "Geo|The database is currently %{db_lag} behind the primary node." msgstr "" @@ -14778,6 +14787,9 @@ msgstr "" msgid "Geo|Verification failed - %{error}" msgstr "" +msgid "Geo|Verification status" +msgstr "" + msgid "Geo|Verificaton information" msgstr "" @@ -32177,6 +32189,9 @@ msgstr "" msgid "There was a problem fetching groups." msgstr "" +msgid "There was a problem fetching iterations." +msgstr "" + msgid "There was a problem fetching labels." msgstr "" @@ -35841,7 +35856,7 @@ msgstr "" msgid "Webhooks|URL is triggered by a push to the repository" msgstr "" -msgid "Webhooks|URL is triggered when a confidential issue is created, updated, or merged" +msgid "Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened" msgstr "" msgid "Webhooks|URL is triggered when a deployment starts, finishes, fails, or is canceled" @@ -35868,7 +35883,7 @@ msgstr "" msgid "Webhooks|URL is triggered when a wiki page is created or updated" msgstr "" -msgid "Webhooks|URL is triggered when an issue is created, updated, or merged" +msgid "Webhooks|URL is triggered when an issue is created, updated, closed, or reopened" msgstr "" msgid "Webhooks|URL is triggered when someone adds a comment" diff --git a/package.json b/package.json index 7f2d7fb342e..c669c6865c8 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "deckar01-task_list": "^2.3.1", "diff": "^3.4.0", "document-register-element": "1.14.3", - "dompurify": "^2.2.7", + "dompurify": "^2.2.8", "dropzone": "^4.2.0", "editorconfig": "^0.15.3", "emoji-regex": "^7.0.3", diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 5980246944e..be4b6d6b82d 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -52,9 +52,11 @@ RSpec.describe 'Projects > Settings > User manages project members' do end describe 'when the :invite_members_group_modal is disabled' do - it 'imports a team from another project', :js do + before do stub_feature_flags(invite_members_group_modal: false) + end + it 'imports a team from another project', :js do project2.add_maintainer(user) project2.add_reporter(user_mike) diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js index faeab0c244d..f75c3d8bcb9 100644 --- a/spec/frontend/issues_list/mock_data.js +++ b/spec/frontend/issues_list/mock_data.js @@ -14,6 +14,10 @@ export const locationSearch = [ 'not[label_name][]=drama', 'my_reaction_emoji=thumbsup', 'confidential=no', + 'iteration_title=season:+%234', + 'not[iteration_title]=season:+%2320', + 'weight=1', + 'not[weight]=3', ].join('&'); export const filteredTokens = [ @@ -29,6 +33,10 @@ export const filteredTokens = [ { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } }, { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } }, { type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } }, + { type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } }, + { type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } }, + { type: 'weight', value: { data: '1', operator: OPERATOR_IS } }, + { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } }, { type: 'filtered-search-term', value: { data: 'find' } }, { type: 'filtered-search-term', value: { data: 'issues' } }, ]; @@ -44,6 +52,10 @@ export const apiParams = { 'not[labels]': 'live action,drama', my_reaction_emoji: 'thumbsup', confidential: 'no', + iteration_title: 'season: #4', + 'not[iteration_title]': 'season: #20', + weight: '1', + 'not[weight]': '3', }; export const urlParams = { @@ -57,4 +69,8 @@ export const urlParams = { 'not[label_name][]': ['live action', 'drama'], my_reaction_emoji: ['thumbsup'], confidential: ['no'], + iteration_title: ['season: #4'], + 'not[iteration_title]': ['season: #20'], + weight: ['1'], + 'not[weight]': ['3'], }; diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index 46b7e49979e..c49a1ab68b1 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -5,8 +5,10 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; +import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; +import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; export const mockAuthor1 = { id: 1, @@ -98,6 +100,15 @@ export const mockAuthorToken = { fetchAuthors: Api.projectUsers.bind(Api), }; +export const mockIterationToken = { + type: 'iteration', + icon: 'iteration', + title: 'Iteration', + unique: true, + token: IterationToken, + fetchIterations: () => Promise.resolve(), +}; + export const mockLabelToken = { type: 'label_name', icon: 'labels', @@ -155,6 +166,14 @@ export const mockMembershipToken = { ], }; +export const mockWeightToken = { + type: 'weight', + icon: 'weight', + title: 'Weight', + unique: true, + token: WeightToken, +}; + export const mockMembershipTokenOptionsWithoutTitles = { ...mockMembershipToken, options: [{ value: 'exclude' }, { value: 'only' }], diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js new file mode 100644 index 00000000000..ca5dc984ae0 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js @@ -0,0 +1,78 @@ +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import createFlash from '~/flash'; +import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; +import { mockIterationToken } from '../mock_data'; + +jest.mock('~/flash'); + +describe('IterationToken', () => { + const title = 'gitlab-org: #1'; + let wrapper; + + const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) => + mount(IterationToken, { + propsData: { + config, + value, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: 'custom-class', + }, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders iteration value', async () => { + wrapper = createComponent({ value: { data: title } }); + + await wrapper.vm.$nextTick(); + + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // `Iteration` `=` `gitlab-org: #1` + expect(tokenSegments.at(2).text()).toBe(title); + }); + + it('fetches initial values', () => { + const fetchIterationsSpy = jest.fn().mockResolvedValue(); + + wrapper = createComponent({ + config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, + value: { data: title }, + }); + + expect(fetchIterationsSpy).toHaveBeenCalledWith(title); + }); + + it('fetches iterations on user input', () => { + const search = 'hello'; + const fetchIterationsSpy = jest.fn().mockResolvedValue(); + + wrapper = createComponent({ + config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, + }); + + wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search }); + + expect(fetchIterationsSpy).toHaveBeenCalledWith(search); + }); + + it('renders error message when request fails', async () => { + const fetchIterationsSpy = jest.fn().mockRejectedValue(); + + wrapper = createComponent({ + config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, + }); + + await wrapper.vm.$nextTick(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching iterations.', + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js new file mode 100644 index 00000000000..9a72be636cd --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js @@ -0,0 +1,37 @@ +import { GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; +import { mockWeightToken } from '../mock_data'; + +jest.mock('~/flash'); + +describe('WeightToken', () => { + const weight = '3'; + let wrapper; + + const createComponent = ({ config = mockWeightToken, value = { data: '' } } = {}) => + mount(WeightToken, { + propsData: { + config, + value, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: 'custom-class', + }, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders weight value', () => { + wrapper = createComponent({ value: { data: weight } }); + + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // `Weight` `=` `3` + expect(tokenSegments.at(2).text()).toBe(weight); + }); +}); diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index dbe4f970a99..3edab40ac3f 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" RSpec.describe InviteMembersHelper do + include Devise::Test::ControllerHelpers + let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user, developer_projects: [project]) } @@ -14,18 +16,19 @@ RSpec.describe InviteMembersHelper do context 'with project' do before do + allow(helper).to receive(:current_user) { owner } assign(:project, project) end describe "#can_invite_members_for_project?" do - context 'when the user can_import_members' do + context 'when the user can_manage_project_members' do before do - allow(helper).to receive(:can_import_members?).and_return(true) + allow(helper).to receive(:can_manage_project_members?).and_return(true) end it 'returns true' do expect(helper.can_invite_members_for_project?(project)).to eq true - expect(helper).to have_received(:can_import_members?) + expect(helper).to have_received(:can_manage_project_members?) end context 'when feature flag is disabled' do @@ -35,14 +38,14 @@ RSpec.describe InviteMembersHelper do it 'returns false' do expect(helper.can_invite_members_for_project?(project)).to eq false - expect(helper).not_to have_received(:can_import_members?) + expect(helper).not_to have_received(:can_manage_project_members?) end end end - context 'when the user can not invite members' do + context 'when the user can not manage project members' do before do - expect(helper).to receive(:can_import_members?).and_return(false) + expect(helper).to receive(:can_manage_project_members?).and_return(false) end it 'returns false' do @@ -87,7 +90,7 @@ RSpec.describe InviteMembersHelper do allow(helper).to receive(:current_user) { user } end - context 'when the user can_import_members' do + context 'when the user can admin_group_member' do before do allow(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(true) end @@ -109,7 +112,7 @@ RSpec.describe InviteMembersHelper do end end - context 'when the user can not invite members' do + context 'when the user can not admin_group_member' do before do expect(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(false) end diff --git a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb index 5386e5b0b1d..95863ce3765 100644 --- a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb @@ -4,16 +4,19 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do describe '#optimize' do - subject { described_class.new(migration, number_of_jobs: number_of_jobs).optimize! } + subject { described_class.new(migration, number_of_jobs: number_of_jobs, ema_alpha: ema_alpha).optimize! } let(:migration) { create(:batched_background_migration, batch_size: batch_size, sub_batch_size: 100, interval: 120) } let(:batch_size) { 10_000 } let_it_be(:number_of_jobs) { 5 } + let_it_be(:ema_alpha) { 0.4 } + + let_it_be(:target_efficiency) { described_class::TARGET_EFFICIENCY.max } def mock_efficiency(eff) - expect(migration).to receive(:smoothed_time_efficiency).with(number_of_jobs: number_of_jobs).and_return(eff) + expect(migration).to receive(:smoothed_time_efficiency).with(number_of_jobs: number_of_jobs, alpha: ema_alpha).and_return(eff) end it 'with unknown time efficiency, it keeps the batch size' do @@ -34,25 +37,55 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do expect { subject }.not_to change { migration.reload.batch_size } end - it 'with a time efficiency of 70%, it increases the batch size by 10%' do - mock_efficiency(0.7) + it 'with a time efficiency of 85%, it increases the batch size' do + time_efficiency = 0.85 + + mock_efficiency(time_efficiency) + + new_batch_size = ((target_efficiency / time_efficiency) * batch_size).to_i + + expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size) + end + + it 'with a time efficiency of 110%, it decreases the batch size' do + time_efficiency = 1.1 + + mock_efficiency(time_efficiency) + + new_batch_size = ((target_efficiency / time_efficiency) * batch_size).to_i - expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(11_000) + expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size) end - it 'with a time efficiency of 110%, it decreases the batch size by 20%' do - mock_efficiency(1.1) + context 'reaching the upper limit for an increase' do + it 'caps the batch size multiplier at 20% when increasing' do + time_efficiency = 0.1 # this would result in a factor of 10 if not limited - expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(8_000) + mock_efficiency(time_efficiency) + + new_batch_size = (1.2 * batch_size).to_i + + expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size) + end + + it 'does not limit the decrease multiplier' do + time_efficiency = 10 + + mock_efficiency(time_efficiency) + + new_batch_size = (0.1 * batch_size).to_i + + expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size) + end end context 'reaching the upper limit for the batch size' do - let(:batch_size) { 950_000 } + let(:batch_size) { 1_950_000 } it 'caps the batch size at 10M' do mock_efficiency(0.7) - expect { subject }.to change { migration.reload.batch_size }.to(1_000_000) + expect { subject }.to change { migration.reload.batch_size }.to(2_000_000) end end diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb index 82db3d94493..5f66387c82b 100644 --- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb @@ -28,14 +28,6 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red it 'does not track edit actions if author is not present' do expect(track_action(author: nil)).to be_nil end - - context 'when feature flag track_editor_edit_actions is disabled' do - it 'does not track edit actions' do - stub_feature_flags(track_editor_edit_actions: false) - - expect(track_action(author: user1)).to be_nil - end - end end context 'for web IDE edit actions' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 9969c0775d7..7c9e7986aab 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -224,6 +224,41 @@ RSpec.describe Namespace do it { expect(namespace.human_name).to eq(namespace.owner_name) } end + describe '#any_project_has_container_registry_tags?' do + subject { namespace.any_project_has_container_registry_tags? } + + let!(:project_without_registry) { create(:project, namespace: namespace) } + + context 'without tags' do + it { is_expected.to be_falsey } + end + + context 'with tags' do + before do + repositories = create_list(:container_repository, 3) + create(:project, namespace: namespace, container_repositories: repositories) + + stub_container_registry_config(enabled: true) + end + + it 'finds tags' do + stub_container_registry_tags(repository: :any, tags: ['tag']) + + is_expected.to be_truthy + end + + it 'does not cause N+1 query in fetching registries' do + stub_container_registry_tags(repository: :any, tags: []) + control_count = ActiveRecord::QueryRecorder.new { namespace.any_project_has_container_registry_tags? }.count + + other_repositories = create_list(:container_repository, 2) + create(:project, namespace: namespace, container_repositories: other_repositories) + + expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1) + end + end + end + describe '#first_project_with_container_registry_tags' do let(:container_repository) { create(:container_repository) } let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) } diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 0777ca04deb..836ffadd7f7 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -38,6 +38,30 @@ RSpec.describe Release do end end + context 'when description of a release is longer than the limit' do + let(:description) { 'a' * (Gitlab::Database::MAX_TEXT_SIZE_LIMIT + 1) } + let(:release) { build(:release, project: project, description: description) } + + it 'creates a validation error' do + release.validate + + expect(release.errors.full_messages) + .to include("Description is too long (maximum is #{Gitlab::Database::MAX_TEXT_SIZE_LIMIT} characters)") + end + + context 'when validate_release_description_length feature flag is disabled' do + before do + stub_feature_flags(validate_release_description_length: false) + end + + it 'does not create a validation error' do + release.validate + + expect(release.errors.full_messages).to be_empty + end + end + end + context 'when a release is tied to a milestone for another project' do it 'creates a validation error' do milestone = build(:milestone, project: create(:project)) diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index 9bd71ea6f64..84c64508a8d 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -5,38 +5,27 @@ require 'spec_helper' RSpec.describe AlertManagement::ProcessPrometheusAlertService do let_it_be(:project, reload: true) { create(:project, :repository) } - before do - allow(ProjectServiceWorker).to receive(:perform_async) - end + let(:service) { described_class.new(project, payload) } describe '#execute' do - let(:service) { described_class.new(project, payload) } - let(:source) { 'Prometheus' } - let(:auto_close_incident) { true } - let(:create_issue) { true } - let(:send_email) { true } - let(:incident_management_setting) do - double( - auto_close_incident?: auto_close_incident, - create_issue?: create_issue, - send_email?: send_email - ) - end + include_context 'incident management settings enabled' + + subject(:execute) { service.execute } before do - allow(service) - .to receive(:incident_management_setting) - .and_return(incident_management_setting) + stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false) end - subject(:execute) { service.execute } - context 'when alert payload is valid' do - let(:parsed_payload) { Gitlab::AlertManagement::Payload.parse(project, payload, monitoring_tool: source) } - let(:fingerprint) { parsed_payload.gitlab_fingerprint } + let_it_be(:starts_at) { '2020-04-27T10:10:22.265949279Z' } + let_it_be(:title) { 'Alert title' } + let_it_be(:fingerprint) { [starts_at, title, 'vector(1)'].join('/') } + let_it_be(:source) { 'Prometheus' } + + let(:prometheus_status) { 'firing' } let(:payload) do { - 'status' => status, + 'status' => prometheus_status, 'labels' => { 'alertname' => 'GitalyFileServerDown', 'channel' => 'gitaly', @@ -46,196 +35,32 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do 'annotations' => { 'description' => 'Alert description', 'runbook' => 'troubleshooting/gitaly-down.md', - 'title' => 'Alert title' + 'title' => title }, - 'startsAt' => '2020-04-27T10:10:22.265949279Z', + 'startsAt' => starts_at, 'endsAt' => '2020-04-27T10:20:22.265949279Z', - 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1', - 'fingerprint' => 'b6ac4d42057c43c1' + 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1' } end - let(:status) { 'firing' } - - context 'when Prometheus alert status is firing' do - context 'when alert with the same fingerprint already exists' do - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'processes incident issues' - it_behaves_like 'Alert Notification Service sends notification email' - - context 'existing alert is resolved' do - let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'Alert Notification Service sends notification email' - end - - context 'existing alert is ignored' do - let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends no notifications' - end - - context 'existing alert is acknowledged' do - let!(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends no notifications' - end - - context 'two existing alerts, one resolved one open' do - let!(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends notification email' - end - - context 'when auto-creation of issues is disabled' do - let(:create_issue) { false } - - it_behaves_like 'does not process incident issues' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end - end - - context 'when alert does not exist' do - context 'when alert can be created' do - it_behaves_like 'creates an alert management alert' - it_behaves_like 'Alert Notification Service sends notification email' - it_behaves_like 'processes incident issues' - - it_behaves_like 'creates single system note based on the source of the alert' - - context 'when auto-alert creation is disabled' do - let(:create_issue) { false } - - it_behaves_like 'does not process incident issues' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end - end - - context 'when alert cannot be created' do - let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} - - before do - allow(service).to receive(:alert).and_call_original - allow(service).to receive_message_chain(:alert, :save).and_return(false) - allow(service).to receive_message_chain(:alert, :errors).and_return(errors) - end - - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request - it_behaves_like 'does not process incident issues due to error', http_status: :bad_request - - it 'writes a warning to the log' do - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Unable to create AlertManagement::Alert from Prometheus', - project_id: project.id, - alert_errors: { hosts: ['hosts array is over 255 chars'] } - ) - - execute - end - end - - it { is_expected.to be_success } - end - end - - context 'when Prometheus alert status is resolved' do - let(:status) { 'resolved' } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint, monitoring_tool: source) } - - context 'when auto_resolve_incident set to true' do - context 'when status can be changed' do - it_behaves_like 'Alert Notification Service sends notification email' - it_behaves_like 'does not process incident issues' - - it 'resolves an existing alert without error' do - expect(Gitlab::AppLogger).not_to receive(:warn) - expect { execute }.to change { alert.reload.resolved? }.to(true) - end - - it_behaves_like 'creates status-change system note for an auto-resolved alert' - - context 'existing issue' do - let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint) } - - it 'closes the issue' do - issue = alert.issue - - expect { execute } - .to change { issue.reload.state } - .from('opened') - .to('closed') - end - - it 'creates a resource state event' do - expect { execute }.to change(ResourceStateEvent, :count).by(1) - end - end - end - - context 'when status change did not succeed' do - before do - allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert]) - allow(alert).to receive(:resolve).and_return(false) - end - - it 'writes a warning to the log' do - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Unable to update AlertManagement::Alert status to resolved', - project_id: project.id, - alert_id: alert.id - ) - - execute - end - - it_behaves_like 'Alert Notification Service sends notification email' - end - - it { is_expected.to be_success } - end + it_behaves_like 'processes new firing alert' - context 'when auto_resolve_incident set to false' do - let(:auto_close_incident) { false } + context 'with resolving payload' do + let(:prometheus_status) { 'resolved' } - it 'does not resolve an existing alert' do - expect { execute }.not_to change { alert.reload.resolved? } - end - - it_behaves_like 'creates single system note based on the source of the alert' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end + it_behaves_like 'processes prometheus recovery alert' end context 'environment given' do let(:environment) { create(:environment, project: project) } + let(:alert) { project.alert_management_alerts.last } - it 'sets the environment' do + before do payload['labels']['gitlab_environment_name'] = environment.name - execute + end - alert = project.alert_management_alerts.last + it 'sets the environment' do + execute expect(alert.environment).to eq(environment) end @@ -243,12 +68,14 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do context 'prometheus alert given' do let(:prometheus_alert) { create(:prometheus_alert, project: project) } + let(:alert) { project.alert_management_alerts.last } - it 'sets the prometheus alert and environment' do + before do payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id - execute + end - alert = project.alert_management_alerts.last + it 'sets the prometheus alert and environment' do + execute expect(alert.prometheus_alert).to eq(prometheus_alert) expect(alert.environment).to eq(prometheus_alert.environment) @@ -259,10 +86,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do context 'when alert payload is invalid' do let(:payload) { {} } - it 'responds with bad_request' do - expect(execute).to be_error - expect(execute.http_status).to eq(:bad_request) - end + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end end end diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb index c272ce13132..feae8f3967c 100644 --- a/spec/services/projects/alerting/notify_service_spec.rb +++ b/spec/services/projects/alerting/notify_service_spec.rb @@ -3,77 +3,49 @@ require 'spec_helper' RSpec.describe Projects::Alerting::NotifyService do - let_it_be_with_reload(:project) { create(:project, :repository) } + let_it_be_with_reload(:project) { create(:project) } + + let(:payload) { ActionController::Parameters.new(payload_raw).permit! } + let(:payload_raw) { {} } + + let(:service) { described_class.new(project, payload) } before do - allow(ProjectServiceWorker).to receive(:perform_async) + stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false) end describe '#execute' do - let(:token) { 'invalid-token' } - let(:starts_at) { Time.current.change(usec: 0) } - let(:fingerprint) { 'testing' } - let(:service) { described_class.new(project, payload) } - let_it_be(:environment) { create(:environment, project: project) } - let(:environment) { create(:environment, project: project) } - let(:ended_at) { nil } - let(:payload_raw) do - { - title: 'alert title', - start_time: starts_at.rfc3339, - end_time: ended_at&.rfc3339, - severity: 'low', - monitoring_tool: 'GitLab RSpec', - service: 'GitLab Test Suite', - description: 'Very detailed description', - hosts: ['1.1.1.1', '2.2.2.2'], - fingerprint: fingerprint, - gitlab_environment_name: environment.name - }.with_indifferent_access - end + include_context 'incident management settings enabled' - let(:payload) { ActionController::Parameters.new(payload_raw).permit! } + subject { service.execute(token, integration) } - subject { service.execute(token, nil) } + context 'with HTTP integration' do + let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) } - shared_examples 'notifications are handled correctly' do context 'with valid token' do let(:token) { integration.token } - let(:incident_management_setting) { double(send_email?: email_enabled, create_issue?: issue_enabled, auto_close_incident?: auto_close_enabled) } - let(:email_enabled) { false } - let(:issue_enabled) { false } - let(:auto_close_enabled) { false } - - before do - allow(service) - .to receive(:incident_management_setting) - .and_return(incident_management_setting) - end context 'with valid payload' do - shared_examples 'assigns the alert properties' do - it 'ensure that created alert has all data properly assigned' do - subject - expect(last_alert_attributes).to match( - project_id: project.id, - title: payload_raw.fetch(:title), - started_at: Time.zone.parse(payload_raw.fetch(:start_time)), - severity: payload_raw.fetch(:severity), - status: AlertManagement::Alert.status_value(:triggered), - events: 1, - domain: 'operations', - hosts: payload_raw.fetch(:hosts), - payload: payload_raw.with_indifferent_access, - issue_id: nil, - description: payload_raw.fetch(:description), - monitoring_tool: payload_raw.fetch(:monitoring_tool), - service: payload_raw.fetch(:service), - fingerprint: Digest::SHA1.hexdigest(fingerprint), - environment_id: environment.id, - ended_at: nil, - prometheus_alert_id: nil - ) - end + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:fingerprint) { 'testing' } + let_it_be(:source) { 'GitLab RSpec' } + let_it_be(:starts_at) { Time.current.change(usec: 0) } + + let(:ended_at) { nil } + let(:domain) { 'operations' } + let(:payload_raw) do + { + title: 'alert title', + start_time: starts_at.rfc3339, + end_time: ended_at&.rfc3339, + severity: 'low', + monitoring_tool: source, + service: 'GitLab Test Suite', + description: 'Very detailed description', + hosts: ['1.1.1.1', '2.2.2.2'], + fingerprint: fingerprint, + gitlab_environment_name: environment.name + }.with_indifferent_access end let(:last_alert_attributes) do @@ -82,8 +54,8 @@ RSpec.describe Projects::Alerting::NotifyService do .with_indifferent_access end - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' + it_behaves_like 'processes new firing alert' + it_behaves_like 'properly assigns the alert properties' it 'passes the integration to alert processing' do expect(Gitlab::AlertManagement::Payload) @@ -94,101 +66,18 @@ RSpec.describe Projects::Alerting::NotifyService do subject end - it 'creates a system note corresponding to alert creation' do - expect { subject }.to change(Note, :count).by(1) - expect(Note.last.note).to include(payload_raw.fetch(:monitoring_tool)) - end - - context 'existing alert with same fingerprint' do - let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - - context 'end time given' do - let(:ended_at) { Time.current.change(nsec: 0) } - - it 'does not resolve the alert' do - expect { subject }.not_to change { alert.reload.status } - end - - it 'does not set the ended at' do - subject - - expect(alert.reload.ended_at).to be_nil - end - - it_behaves_like 'does not an create alert management alert' - it_behaves_like 'creates single system note based on the source of the alert' - - context 'auto_close_enabled setting enabled' do - let(:auto_close_enabled) { true } - - it 'resolves the alert and sets the end time', :aggregate_failures do - subject - alert.reload - - expect(alert.resolved?).to eq(true) - expect(alert.ended_at).to eql(ended_at) - end - - it_behaves_like 'creates status-change system note for an auto-resolved alert' - - context 'related issue exists' do - let(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) } - let(:issue) { alert.issue } - - it { expect { subject }.to change { issue.reload.state }.from('opened').to('closed') } - it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } - end - - context 'with issue enabled' do - let(:issue_enabled) { true } - - it_behaves_like 'does not process incident issues' - end - end - end - - context 'existing alert is resolved' do - let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' - end - - context 'existing alert is ignored' do - let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - end - - context 'two existing alerts, one resolved one open' do - let!(:resolved_existing_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - end - end - - context 'end time given' do - let(:ended_at) { Time.current } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' - end - - context 'with a minimal payload' do - let(:payload_raw) do + context 'with partial payload' do + let_it_be(:source) { integration.name } + let_it_be(:payload_raw) do { title: 'alert title', start_time: starts_at.rfc3339 } end - it_behaves_like 'creates an alert management alert' + include_examples 'processes never-before-seen alert' - it 'created alert has all data properly assigned' do + it 'assigns the alert properties' do subject expect(last_alert_attributes).to match( @@ -212,7 +101,19 @@ RSpec.describe Projects::Alerting::NotifyService do ) end - it_behaves_like 'creates single system note based on the source of the alert' + context 'with existing alert with matching payload' do + let_it_be(:fingerprint) { payload_raw.except(:start_time).stringify_keys } + let_it_be(:gitlab_fingerprint) { Gitlab::AlertManagement::Fingerprint.generate(fingerprint) } + let_it_be(:alert) { create(:alert_management_alert, project: project, fingerprint: gitlab_fingerprint) } + + include_examples 'processes never-before-seen alert' + end + end + + context 'with resolving payload' do + let(:ended_at) { Time.current.change(usec: 0) } + + it_behaves_like 'processes recovery alert' end end @@ -223,63 +124,30 @@ RSpec.describe Projects::Alerting::NotifyService do allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) end - it_behaves_like 'does not process incident issues due to error', http_status: :bad_request - it_behaves_like 'does not an create alert management alert' + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end - it_behaves_like 'does not process incident issues' - - context 'issue enabled' do - let(:issue_enabled) { true } - - it_behaves_like 'processes incident issues' - - context 'when alert already exists' do - let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - context 'when existing alert does not have an associated issue' do - it_behaves_like 'processes incident issues' - end - - context 'when existing alert has an associated issue' do - let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'does not process incident issues' - end + context 'with inactive integration' do + before do + integration.update!(active: false) end - end - context 'with emails turned on' do - let(:email_enabled) { true } - - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden end end context 'with invalid token' do - it_behaves_like 'does not process incident issues due to error', http_status: :unauthorized - it_behaves_like 'does not an create alert management alert' - end - end - - context 'with an HTTP Integration' do - let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) } + let(:token) { 'invalid-token' } - subject { service.execute(token, integration) } - - it_behaves_like 'notifications are handled correctly' do - let(:source) { integration.name } + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end + end - context 'with deactivated HTTP Integration' do - before do - integration.update!(active: false) - end + context 'without HTTP integration' do + let(:integration) { nil } + let(:token) { nil } - it_behaves_like 'does not process incident issues due to error', http_status: :forbidden - it_behaves_like 'does not an create alert management alert' - end + it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden end end end diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index e196220eabe..e70a23de2a0 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -6,25 +6,26 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do include PrometheusHelpers using RSpec::Parameterized::TableSyntax - let_it_be(:project, reload: true) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } + let_it_be_with_reload(:setting) do + create(:project_incident_management_setting, project: project, send_email: true, create_issue: true) + end let(:service) { described_class.new(project, payload) } let(:token_input) { 'token' } - let!(:setting) do - create(:project_incident_management_setting, project: project, send_email: true, create_issue: true) - end - - let(:subject) { service.execute(token_input) } + subject { service.execute(token_input) } context 'with valid payload' do let_it_be(:alert_firing) { create(:prometheus_alert, project: project) } let_it_be(:alert_resolved) { create(:prometheus_alert, project: project) } - let_it_be(:cluster) { create(:cluster, :provided_by_user, projects: [project]) } + let_it_be(:cluster, reload: true) { create(:cluster, :provided_by_user, projects: [project]) } + let(:payload_raw) { prometheus_alert_payload(firing: [alert_firing], resolved: [alert_resolved]) } let(:payload) { ActionController::Parameters.new(payload_raw).permit! } let(:payload_alert_firing) { payload_raw['alerts'].first } let(:token) { 'token' } + let(:source) { 'Prometheus' } context 'with environment specific clusters' do let(:prd_cluster) do @@ -53,11 +54,11 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do context 'without token' do let(:token_input) { nil } - it_behaves_like 'Alert Notification Service sends notification email' + include_examples 'processes one firing and one resolved prometheus alerts' end context 'with token' do - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end end @@ -87,9 +88,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + include_examples 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end @@ -97,9 +98,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'without project specific cluster' do - let!(:cluster) { create(:cluster, enabled: true) } + let_it_be(:cluster) { create(:cluster, enabled: true) } - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end context 'with manual prometheus installation' do @@ -126,9 +127,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end @@ -150,50 +151,53 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do let(:token_input) { public_send(token) if token } let(:integration) { create(:alert_management_http_integration, active, project: project) if active } - let(:subject) { service.execute(token_input, integration) } + subject { service.execute(token_input, integration) } case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end end end - context 'alert emails' do + context 'incident settings' do before do create(:prometheus_service, project: project) create(:project_alerting_setting, project: project, token: token) end - context 'when incident_management_setting does not exist' do - let!(:setting) { nil } - - it 'does not send notification email', :sidekiq_might_not_need_inline do - expect_any_instance_of(NotificationService) - .not_to receive(:async) + it_behaves_like 'processes one firing and one resolved prometheus alerts' - expect(subject).to be_success + context 'when incident_management_setting does not exist' do + before do + setting.destroy! end - end - context 'when incident_management_setting.send_email is true' do - it_behaves_like 'Alert Notification Service sends notification email' + it { is_expected.to be_success } + include_examples 'does not send alert notification emails' + include_examples 'does not process incident issues' end context 'incident_management_setting.send_email is false' do - let!(:setting) do - create(:project_incident_management_setting, send_email: false, project: project) + before do + setting.update!(send_email: false) end - it 'does not send notification' do - expect(NotificationService).not_to receive(:new) + it { is_expected.to be_success } + include_examples 'does not send alert notification emails' + end - expect(subject).to be_success + context 'incident_management_setting.create_issue is false' do + before do + setting.update!(create_issue: false) end + + it { is_expected.to be_success } + include_examples 'does not process incident issues' end end @@ -233,7 +237,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do .and_return(false) end - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unprocessable_entity + it_behaves_like 'alerts service responds with an error and takes no actions', :unprocessable_entity end context 'when the payload is too big' do @@ -244,14 +248,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) end - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request - - it 'does not process Prometheus alerts' do - expect(AlertManagement::ProcessPrometheusAlertService) - .not_to receive(:new) - - subject - end + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end end diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb deleted file mode 100644 index fc935effe0e..00000000000 --- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'Alert Notification Service sends notification email' do - let(:notification_service) { spy } - - it 'sends a notification' do - expect(NotificationService) - .to receive(:new) - .and_return(notification_service) - - expect(notification_service) - .to receive_message_chain(:async, :prometheus_alerts_fired) - - expect(subject).to be_success - end -end - -RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil| - it 'does not notify' do - expect(NotificationService).not_to receive(:new) - - if http_status.present? - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - else - expect(subject).to be_success - end - end -end - -RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do - it 'has 2 new system notes' do - expect { subject }.to change(Note, :count).by(2) - expect(Note.last.note).to include('Resolved') - end -end - -# Requires `source` to be defined -RSpec.shared_examples 'creates single system note based on the source of the alert' do - it 'has one new system note' do - expect { subject }.to change(Note, :count).by(1) - expect(Note.last.note).to include(source) - end -end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb new file mode 100644 index 00000000000..218a3462c35 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `service`, the service which includes AlertManagement::AlertProcessing +RSpec.shared_examples 'creates an alert management alert or errors' do + it { is_expected.to be_success } + + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject }.to change(AlertManagement::Alert, :count).by(1) + end + + it 'executes the alert service hooks' do + expect_next_instance_of(AlertManagement::Alert) do |alert| + expect(alert).to receive(:execute_services) + end + + subject + end + + context 'and fails to save' do + let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} + + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :save).and_return(false) + allow(service).to receive_message_chain(:alert, :errors).and_return(errors) + end + + it_behaves_like 'alerts service responds with an error', :bad_request + + it 'writes a warning to the log' do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: "Unable to create AlertManagement::Alert from #{source}", + project_id: project.id, + alert_errors: { hosts: ['hosts array is over 255 chars'] } + ) + + subject + end + end +end + +# This shared_example requires the following variables: +# - last_alert_attributes, last created alert +# - project, project that alert created +# - payload_raw, hash representation of payload +# - environment, project's environment +# - fingerprint, fingerprint hash +RSpec.shared_examples 'properly assigns the alert properties' do + specify do + subject + + expect(last_alert_attributes).to match({ + project_id: project.id, + title: payload_raw.fetch(:title), + started_at: Time.zone.parse(payload_raw.fetch(:start_time)), + severity: payload_raw.fetch(:severity, nil), + status: AlertManagement::Alert.status_value(:triggered), + events: 1, + domain: domain, + hosts: payload_raw.fetch(:hosts, nil), + payload: payload_raw.with_indifferent_access, + issue_id: nil, + description: payload_raw.fetch(:description, nil), + monitoring_tool: payload_raw.fetch(:monitoring_tool, nil), + service: payload_raw.fetch(:service, nil), + fingerprint: Digest::SHA1.hexdigest(fingerprint), + environment_id: environment.id, + ended_at: nil, + prometheus_alert_id: nil + }.with_indifferent_access) + end +end + +RSpec.shared_examples 'does not create an alert management alert' do + specify do + expect { subject }.not_to change(AlertManagement::Alert, :count) + end +end + +# This shared_example requires the following variables: +# - `alert`, the alert for which events should be incremented +RSpec.shared_examples 'adds an alert management alert event' do + specify do + expect(alert).not_to receive(:execute_services) + + expect { subject }.to change { alert.reload.events }.by(1) + + expect(subject).to be_success + end + + it_behaves_like 'does not create an alert management alert' +end + +# This shared_example requires the following variables: +# - `alert`, the alert for which events should not be incremented +RSpec.shared_examples 'does not add an alert management alert event' do + specify do + expect { subject }.not_to change { alert.reload.events } + end +end + +RSpec.shared_examples 'processes new firing alert' do + include_examples 'processes never-before-seen alert' + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + + context 'with an existing resolved alert as well' do + let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + end + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, fingerprint: gitlab_fingerprint, project: project) } + + include_examples 'processes never-before-seen alert' + end + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb new file mode 100644 index 00000000000..e299a83cf97 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `alert`, the alert to be resolved +RSpec.shared_examples 'resolves an existing alert management alert' do + it 'sets the end time and status' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject } + .to change { alert.reload.resolved? }.to(true) + .and change { alert.ended_at.present? }.to(true) + + expect(subject).to be_success + end +end + +# This shared_example requires the following variables: +# - `alert`, the alert not to be updated +RSpec.shared_examples 'does not change the alert end time' do + specify do + expect { subject }.not_to change { alert.reload.ended_at } + end +end + +# This shared_example requires the following variables: +# - `project`, expected project for an incoming alert +# - `service`, a service which includes AlertManagement::AlertProcessing +# - `alert` (optional), the alert which should fail to resolve. If not +# included, the log is expected to correspond to a new alert +RSpec.shared_examples 'writes a warning to the log for a failed alert status update' do + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :resolve).and_return(false) + end + + specify do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: 'Unable to update AlertManagement::Alert status to resolved', + project_id: project.id, + alert_id: alert ? alert.id : (last_alert_id + 1) + ) + + # Failure to resolve a recovery alert is not a critical failure + expect(subject).to be_success + end + + private + + def last_alert_id + AlertManagement::Alert.connection + .select_value("SELECT nextval('#{AlertManagement::Alert.sequence_name}')") + end +end + +RSpec.shared_examples 'processes recovery alert' do + context 'seen for the first time' do + let(:alert) { AlertManagement::Alert.last } + + include_examples 'processes never-before-seen recovery alert' + end + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + include_examples 'processes never-before-seen recovery alert' + end + end +end + +RSpec.shared_examples 'processes prometheus recovery alert' do + context 'seen for the first time' do + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not send alert notification emails' + it_behaves_like 'does not process incident issues' + end + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not send alert notification emails' + it_behaves_like 'does not change the alert end time' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb new file mode 100644 index 00000000000..186af49afde --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - with_issue: includes a test for when the defined `alert` has an associated issue +# +# This shared_example requires the following variables: +# - `alert`, required if :with_issue is true +RSpec.shared_examples 'processes incident issues if enabled' do |with_issue: false| + include_examples 'processes incident issues', with_issue + + context 'with incident setting disabled' do + let(:create_issue) { false } + + it_behaves_like 'does not process incident issues' + end +end + +RSpec.shared_examples 'processes incident issues' do |with_issue: false| + before do + allow_next_instance_of(AlertManagement::Alert) do |alert| + allow(alert).to receive(:execute_services) + end + end + + specify do + expect(IncidentManagement::ProcessAlertWorker) + .to receive(:perform_async) + .with(nil, nil, kind_of(Integer)) + + Sidekiq::Testing.inline! do + expect(subject).to be_success + end + end + + context 'with issue', if: with_issue do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it_behaves_like 'does not process incident issues' + end +end + +RSpec.shared_examples 'does not process incident issues' do + specify do + expect(IncidentManagement::ProcessAlertWorker).not_to receive(:perform_async) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb new file mode 100644 index 00000000000..132f1e0422e --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example requires the following variables: +# - `alert`, alert for which related incidents should be closed +# - `project`, project of the alert +RSpec.shared_examples 'closes related incident if enabled' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.to change { alert.issue.reload.closed? }.from(false).to(true) } + it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'with incident setting disabled' do + let(:auto_close_incident) { false } + + it_behaves_like 'does not close related incident' + end +end + +RSpec.shared_examples 'does not close related incident' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.not_to change { alert.issue.reload.state } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb new file mode 100644 index 00000000000..5f30b58176b --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - count: number of notifications expected to be sent +RSpec.shared_examples 'sends alert notification emails if enabled' do |count: 1| + include_examples 'sends alert notification emails', count + + context 'with email setting disabled' do + let(:send_email) { false } + + it_behaves_like 'does not send alert notification emails' + end +end + +RSpec.shared_examples 'sends alert notification emails' do |count: 1| + let(:notification_async) { double(NotificationService::Async) } + + specify do + allow(NotificationService).to receive_message_chain(:new, :async).and_return(notification_async) + expect(notification_async).to receive(:prometheus_alerts_fired).exactly(count).times + + subject + end +end + +RSpec.shared_examples 'does not send alert notification emails' do + specify do + expect(NotificationService).not_to receive(:new) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb new file mode 100644 index 00000000000..57d598c0259 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# This shared_example includes the following option: +# - notes: any of [:new_alert, :recovery_alert, :resolve_alert]. +# Represents which notes are expected to be created. +# +# This shared_example requires the following variables: +# - `source` (optional), the monitoring tool or integration name +# expected in the applicable system notes +RSpec.shared_examples 'creates expected system notes for alert' do |*notes| + let(:expected_note_count) { expected_notes.length } + let(:new_notes) { Note.last(expected_note_count).pluck(:note) } + let(:expected_notes) do + { + new_alert: source, + recovery_alert: source, + resolve_alert: 'Resolved' + }.slice(*notes) + end + + it "for #{notes.join(', ')}" do + expect { subject }.to change(Note, :count).by(expected_note_count) + + expected_notes.each_value.with_index do |value, index| + expect(new_notes[index]).to include(value) + end + end +end + +RSpec.shared_examples 'does not create a system note for alert' do + specify do + expect { subject }.not_to change(Note, :count) + end +end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index d9f28a97a0f..a0e084967e5 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -1,111 +1,67 @@ # frozen_string_literal: true -RSpec.shared_examples 'creates an alert management alert' do - it { is_expected.to be_success } +RSpec.shared_examples 'alerts service responds with an error and takes no actions' do |http_status| + include_examples 'alerts service responds with an error', http_status - it 'creates AlertManagement::Alert' do - expect { subject }.to change(AlertManagement::Alert, :count).by(1) - end - - it 'executes the alert service hooks' do - expect_next_instance_of(AlertManagement::Alert) do |alert| - expect(alert).to receive(:execute_services) - end + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not send alert notification emails' +end - subject +RSpec.shared_examples 'alerts service responds with an error' do |http_status| + specify do + expect(subject).to be_error + expect(subject.http_status).to eq(http_status) end end # This shared_example requires the following variables: -# - last_alert_attributes, last created alert -# - project, project that alert created -# - payload_raw, hash representation of payload -# - environment, project's environment -# - fingerprint, fingerprint hash -RSpec.shared_examples 'assigns the alert properties' do - it 'ensures that created alert has all data properly assigned' do - subject - - expect(last_alert_attributes).to match( - project_id: project.id, - title: payload_raw.fetch(:title), - started_at: Time.zone.parse(payload_raw.fetch(:start_time)), - severity: payload_raw.fetch(:severity), - status: AlertManagement::Alert.status_value(:triggered), - events: 1, - domain: domain, - hosts: payload_raw.fetch(:hosts), - payload: payload_raw.with_indifferent_access, - issue_id: nil, - description: payload_raw.fetch(:description), - monitoring_tool: payload_raw.fetch(:monitoring_tool), - service: payload_raw.fetch(:service), - fingerprint: Digest::SHA1.hexdigest(fingerprint), - environment_id: environment.id, - ended_at: nil, - prometheus_alert_id: nil +# - `service`, a service which includes ::IncidentManagement::Settings +RSpec.shared_context 'incident management settings enabled' do + let(:auto_close_incident) { true } + let(:create_issue) { true } + let(:send_email) { true } + + let(:incident_management_setting) do + double( + auto_close_incident?: auto_close_incident, + create_issue?: create_issue, + send_email?: send_email ) end -end -RSpec.shared_examples 'does not an create alert management alert' do - it 'does not create alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) + before do + allow(ProjectServiceWorker).to receive(:perform_async) + allow(service) + .to receive(:incident_management_setting) + .and_return(incident_management_setting) end end -RSpec.shared_examples 'adds an alert management alert event' do - it { is_expected.to be_success } - - it 'does not create an alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) - end - - it 'increases alert events count' do - expect { subject }.to change { alert.reload.events }.by(1) - end - - it 'does not executes the alert service hooks' do - expect(alert).not_to receive(:execute_services) - - subject - end +RSpec.shared_examples 'processes never-before-seen alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert + it_behaves_like 'processes incident issues if enabled' + it_behaves_like 'sends alert notification emails if enabled' end -RSpec.shared_examples 'processes incident issues' do - let(:create_incident_service) { spy } - - before do - allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services) - end - - it 'processes issues' do - expect(IncidentManagement::ProcessAlertWorker) - .to receive(:perform_async) - .with(nil, nil, kind_of(Integer)) - .once - - Sidekiq::Testing.inline! do - expect(subject).to be_success - end - end +RSpec.shared_examples 'processes never-before-seen recovery alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled' end -RSpec.shared_examples 'does not process incident issues' do - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) +RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) - expect(subject).to be_success + expect { subject } + .to change(AlertManagement::Alert, :count).by(1) + .and change(Note, :count).by(1) end -end - -RSpec.shared_examples 'does not process incident issues due to error' do |http_status:| - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - end + it_behaves_like 'processes incident issues' + it_behaves_like 'sends alert notification emails', count: 1 end diff --git a/yarn.lock b/yarn.lock index 81bd4024065..c367c918d3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4557,10 +4557,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -dompurify@^2.2.7: - version "2.2.7" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.7.tgz#a5f055a2a471638680e779bd08fc334962d11fd8" - integrity sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg== +dompurify@^2.2.7, dompurify@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.8.tgz#ce88e395f6d00b6dc53f80d6b2a6fdf5446873c6" + integrity sha512-9H0UL59EkDLgY3dUFjLV6IEUaHm5qp3mxSqWw7Yyx4Zhk2Jn2cmLe+CNPP3xy13zl8Bqg+0NehQzkdMoVhGRww== domutils@^1.5.1: version "1.7.0" |