Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-29 12:09:58 +00:00
parent 38e4bfea58
commit db36dea03b
106 changed files with 1730 additions and 901 deletions

View File

@ -1 +1 @@
f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5
22654ba48106412ca6680366afe6a47389458720

View File

@ -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'

View File

@ -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)

View File

@ -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 } });
},

View File

@ -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]',
},
},
};

View File

@ -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,

View File

@ -1,19 +1,17 @@
/* 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_ITERATIONS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEFAULT_LABEL_CURRENT];
export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEBOUNCE_DELAY = 200;
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
};
export const DEFAULT_MILESTONES = [
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
@ -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 */

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}
}
}
}

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')
= 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')
- @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
- @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 }
- 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 }
= 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 } }
.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' }>

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fix N+1 queries in namespace#any_project_has_container_registry_tags?
merge_request: 59916
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Initialize conversion of ci_build_trace_chunks.build_id to bigint
merge_request: 60346
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Cache project tag list
merge_request: 57031
author:
type: performance

View File

@ -0,0 +1,6 @@
---
title: Fix copy on webhook admin pages for "Issues events" and "Confidential issues
events"
merge_request: 60453
author:
type: changed

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
bdeb78403607d45d5eb779623d0e2aa1acf026f6aced6f1134824a35dfec7e74

View File

@ -0,0 +1 @@
3cd56794ac903d9598863215a34eda62c3dc96bed78bed5b8a99fc522e319b35

View File

@ -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();

View File

@ -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.

View File

@ -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`

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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",

View File

@ -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
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -301,7 +301,7 @@ Example response:
```json
[
{
"id": 8
"id": 8,
"title":null,
"project_id":1,
"action_name":"opened",

View File

@ -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"
}
```

View File

@ -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,

View File

@ -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,

View File

@ -68,7 +68,7 @@ explorer. GraphiQL explorer is available for:
}
}
}
}
}
```
1. Open the [GraphiQL explorer tool](https://gitlab.com/-/graphql-explorer).

View File

@ -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"
}
```

View File

@ -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
}
}
]

View File

@ -84,7 +84,7 @@ Example response:
},
"provider_gcp": null,
"management_project": null
}
},
{
"id": 11,
"name": "cluster-3",

View File

@ -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"
}

View File

@ -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,

View File

@ -68,7 +68,6 @@ Example of response
"status": "pending"
},
"ref": "master",
"artifacts": [],
"runner": null,
"stage": "test",
"status": "failed",

View File

@ -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",

View File

@ -80,7 +80,7 @@ GET /licenses
"Name": "Doe John"
},
"add_ons": {
"GitLab_FileLocks": 1,
"GitLab_FileLocks": 1
}
}
]

View File

@ -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",

View File

@ -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"
}
}
],
]
}
```

View File

@ -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.",

View File

@ -176,7 +176,9 @@ Example responses:
{
"level": "watch"
}
```
```json
{
"level": "custom",
"events": {

View File

@ -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"
}
```

View File

@ -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"
}
]
```

View File

@ -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"
}
```

View File

@ -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"
}
}
```

View File

@ -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"
}
}
```

View File

@ -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"
}
]
}

View File

@ -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"
}
}
]
```

View File

@ -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"
},
}
}
```

View File

@ -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"
}
]

View File

@ -42,7 +42,7 @@ Example response:
"wiki_page_events": true,
"job_events": true,
"comment_on_event_enabled": true
}
},
{
"id": 76,
"title": "Alerts endpoint",

View File

@ -1762,6 +1762,6 @@ Example response:
"source_name": "Group three",
"source_type": "Namespace",
"access_level": "20"
},
}
]
```

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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,18 +14664,15 @@ 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|Replicated data is verified with the secondary node(s) using checksums."
msgstr ""
msgid "Geo|Replication Details"
msgstr ""
msgid "Geo|Replication Details Desktop"
msgstr ""
msgid "Geo|Replication Details Mobile"
msgstr ""
msgid "Geo|Replication details"
msgstr ""
@ -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"

View File

@ -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",

View File

@ -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)

View File

@ -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'],
};

View File

@ -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' }],

View File

@ -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.',
});
});
});

View File

@ -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);
});
});

View File

@ -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

View File

@ -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
expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(11_000)
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 by 20%' do
mock_efficiency(1.1)
it 'with a time efficiency of 110%, it decreases the batch size' do
time_efficiency = 1.1
expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(8_000)
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
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
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

View File

@ -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

View File

@ -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]) }

View File

@ -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))

View File

@ -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
before do
allow(service)
.to receive(:incident_management_setting)
.and_return(incident_management_setting)
end
include_context 'incident management settings enabled'
subject(:execute) { service.execute }
before do
stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false)
end
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,210 +35,48 @@ 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' }
it_behaves_like 'processes new firing alert'
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) }
context 'with resolving payload' do
let(:prometheus_status) { 'resolved' }
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
context 'when auto_resolve_incident set to false' do
let(:auto_close_incident) { false }
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 }
before do
payload['labels']['gitlab_environment_name'] = environment.name
end
it 'sets the environment' do
payload['labels']['gitlab_environment_name'] = environment.name
execute
alert = project.alert_management_alerts.last
expect(alert.environment).to eq(environment)
end
end
context 'prometheus alert given' do
let(:prometheus_alert) { create(:prometheus_alert, project: project) }
let(:alert) { project.alert_management_alerts.last }
before do
payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id
end
it 'sets the prometheus alert and environment' do
payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id
execute
alert = project.alert_management_alerts.last
expect(alert.prometheus_alert).to eq(prometheus_alert)
expect(alert.environment).to eq(prometheus_alert.environment)
end
@ -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

View File

@ -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'
let(:token) { 'invalid-token' }
it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
end
end
context 'with an HTTP Integration' do
let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) }
context 'without HTTP integration' do
let(:integration) { nil }
let(:token) { nil }
subject { service.execute(token, integration) }
it_behaves_like 'notifications are handled correctly' do
let(:source) { integration.name }
end
context 'with deactivated HTTP Integration' do
before do
integration.update!(active: false)
end
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

View File

@ -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
it_behaves_like 'processes one firing and one resolved prometheus alerts'
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)
expect(subject).to be_success
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

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More