Commit 6f58d694 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 4662edd6 5ff033b9
......@@ -491,6 +491,9 @@ That's all of the required database changes.
self.primary_key = :cool_widget_id
belongs_to :cool_widget, inverse_of: :cool_widget_state
validates :verification_failure, length: { maximum: 255 }
validates :verification_state, :cool_widget, presence: true
end
end
```
......
......@@ -455,6 +455,9 @@ That's all of the required database changes.
self.primary_key = :cool_widget_id
belongs_to :cool_widget, inverse_of: :cool_widget_state
validates :verification_failure, length: { maximum: 255 }
validates :verification_state, :cool_widget, presence: true
end
end
```
......
......@@ -71,6 +71,9 @@ export default {
this.error = false;
this.errorMessages = [];
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
},
},
fields: [
{ key: 'firstName', sortable: true },
......@@ -142,7 +145,7 @@ export default {
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="`${groupIssuesPath}?scope=all&state=opened&crm_contact_id=${data.value}`"
:href="getIssuesPath(groupIssuesPath, data.value)"
/>
</template>
</gl-table>
......
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import createFlash from '~/flash';
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlLoadingIcon,
GlTable,
},
inject: ['groupFullPath'],
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['groupFullPath', 'groupIssuesPath'],
data() {
return { organizations: [] };
return {
error: false,
organizations: [],
};
},
apollo: {
organizations: {
......@@ -26,12 +34,8 @@ export default {
update(data) {
return this.extractOrganizations(data);
},
error(error) {
createFlash({
message: __('Something went wrong. Please try again.'),
error,
captureError: true,
});
error() {
this.error = true;
},
},
},
......@@ -45,20 +49,38 @@ export default {
const organizations = data?.group?.organizations?.nodes || [];
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
},
dismissError() {
this.error = false;
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
},
},
fields: [
{ key: 'name', sortable: true },
{ key: 'defaultRate', sortable: true },
{ key: 'description', sortable: true },
{
key: 'id',
label: __('Issues'),
formatter: (id) => {
return getIdFromGraphQLId(id);
},
},
],
i18n: {
emptyText: s__('Crm|No organizations found'),
issuesButtonLabel: __('View issues'),
errorText: __('Something went wrong. Please try again.'),
},
};
</script>
<template>
<div>
<gl-alert v-if="error" variant="danger" class="gl-my-6" @dismiss="dismissError">
<div>{{ $options.i18n.errorText }}</div>
</gl-alert>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
......@@ -66,6 +88,16 @@ export default {
:fields="$options.fields"
:empty-text="$options.i18n.emptyText"
show-empty
/>
>
<template #cell(id)="data">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, data.value)"
/>
</template>
</gl-table>
</div>
</template>
......@@ -16,10 +16,12 @@ export default () => {
return false;
}
const { groupFullPath, groupIssuesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: { groupFullPath: el.dataset.groupFullPath },
provide: { groupFullPath, groupIssuesPath },
render(createElement) {
return createElement(CrmOrganizationsRoot);
},
......
<script>
import { GlAlert, GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui';
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
......@@ -7,8 +7,9 @@ export default {
name: 'GraphViewSelector',
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlLoadingIcon,
GlSegmentedControl,
GlToggle,
},
props: {
......@@ -96,6 +97,9 @@ export default {
this.hoverTipDismissed = true;
this.$emit('dismissHoverTip');
},
isCurrentType(type) {
return this.segmentSelectedType === type;
},
/*
In both toggle methods, we use setTimeout so that the loading indicator displays,
then the work is done to update the DOM. The process is:
......@@ -110,11 +114,14 @@ export default {
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
*/
toggleView(type) {
this.isSwitcherLoading = true;
setTimeout(() => {
this.$emit('updateViewType', type);
});
setViewType(type) {
if (!this.isCurrentType(type)) {
this.isSwitcherLoading = true;
this.segmentSelectedType = type;
setTimeout(() => {
this.$emit('updateViewType', type);
});
}
},
toggleShowLinksActive(val) {
this.isToggleLoading = true;
......@@ -136,14 +143,16 @@ export default {
size="lg"
/>
<span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
<gl-segmented-control
v-model="segmentSelectedType"
:options="viewTypesList"
:disabled="isSwitcherLoading"
data-testid="pipeline-view-selector"
class="gl-mx-4"
@input="toggleView"
/>
<gl-button-group class="gl-mx-4">
<gl-button
v-for="viewType in viewTypesList"
:key="viewType.value"
:selected="isCurrentType(viewType.value)"
@click="setViewType(viewType.value)"
>
{{ viewType.text }}
</gl-button>
</gl-button-group>
<div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
<gl-toggle
......
......@@ -105,7 +105,7 @@ export default {
<gl-button
v-gl-modal="$options.ADD_USER_MODAL_ID"
data-testid="add-users"
variant="success"
variant="confirm"
>
{{ $options.translations.addUserButtonLabel }}
</gl-button>
......
......@@ -36,6 +36,7 @@
# attempt_group_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# crm_contact_id: integer
# crm_organization_id: integer
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
......@@ -61,6 +62,7 @@ class IssuableFinder
author_id
author_username
crm_contact_id
crm_organization_id
label_name
milestone_title
release_tag
......@@ -141,7 +143,8 @@ class IssuableFinder
items = by_release(items)
items = by_label(items)
items = by_my_reaction_emoji(items)
by_crm_contact(items)
items = by_crm_contact(items)
by_crm_organization(items)
end
def should_filter_negated_args?
......@@ -470,6 +473,10 @@ class IssuableFinder
Issuables::CrmContactFilter.new(params: original_params).filter(items)
end
def by_crm_organization(items)
Issuables::CrmOrganizationFilter.new(params: original_params).filter(items)
end
def or_filters_enabled?
strong_memoize(:or_filters_enabled) do
Feature.enabled?(:or_issuable_queries, feature_flag_scope, default_enabled: :yaml)
......
# frozen_string_literal: true
module Issuables
class CrmOrganizationFilter < BaseFilter
def filter(issuables)
by_crm_organization(issuables)
end
# rubocop: disable CodeReuse/ActiveRecord
def by_crm_organization(issuables)
return issuables if params[:crm_organization_id].blank?
condition = CustomerRelations::IssueContact
.joins(:contact)
.where(contact: { organization_id: params[:crm_organization_id] })
.where(Arel.sql("issue_id = issues.id"))
issuables.where(condition.arel.exists)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
- breadcrumb_title _('Customer Relations Organizations')
- page_title _('Customer Relations Organizations')
#js-crm-organizations-app{ data: { group_full_path: @group.full_path } }
#js-crm-organizations-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group) } }
# frozen_string_literal: true
class CreateUploadStates < Gitlab::Database::Migration[1.0]
VERIFICATION_STATE_INDEX_NAME = "index_upload_states_on_verification_state"
PENDING_VERIFICATION_INDEX_NAME = "index_upload_states_pending_verification"
FAILED_VERIFICATION_INDEX_NAME = "index_upload_states_failed_verification"
NEEDS_VERIFICATION_INDEX_NAME = "index_upload_states_needs_verification"
disable_ddl_transaction!
def up
create_table :upload_states, id: false do |t|
t.datetime_with_timezone :verification_started_at
t.datetime_with_timezone :verification_retry_at
t.datetime_with_timezone :verified_at
t.references :upload, primary_key: true, null: false, foreign_key: { on_delete: :cascade }
t.integer :verification_state, default: 0, limit: 2, null: false
t.integer :verification_retry_count, limit: 2
t.binary :verification_checksum, using: 'verification_checksum::bytea'
t.text :verification_failure, limit: 255
t.index :verification_state, name: VERIFICATION_STATE_INDEX_NAME
t.index :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
t.index :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
t.index :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
end
end
def down
drop_table :upload_states
end
end
853e68aa974f49b7ab9f60acc0191da47598db115748e96752145c3cea89a986
\ No newline at end of file
......@@ -19946,6 +19946,27 @@ CREATE SEQUENCE upcoming_reconciliations_id_seq
ALTER SEQUENCE upcoming_reconciliations_id_seq OWNED BY upcoming_reconciliations.id;
CREATE TABLE upload_states (
verification_started_at timestamp with time zone,
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
upload_id bigint NOT NULL,
verification_state smallint DEFAULT 0 NOT NULL,
verification_retry_count smallint,
verification_checksum bytea,
verification_failure text,
CONSTRAINT check_7396dc8591 CHECK ((char_length(verification_failure) <= 255))
);
CREATE SEQUENCE upload_states_upload_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE upload_states_upload_id_seq OWNED BY upload_states.upload_id;
CREATE TABLE uploads (
id integer NOT NULL,
size bigint NOT NULL,
......@@ -22049,6 +22070,8 @@ ALTER TABLE ONLY u2f_registrations ALTER COLUMN id SET DEFAULT nextval('u2f_regi
ALTER TABLE ONLY upcoming_reconciliations ALTER COLUMN id SET DEFAULT nextval('upcoming_reconciliations_id_seq'::regclass);
ALTER TABLE ONLY upload_states ALTER COLUMN upload_id SET DEFAULT nextval('upload_states_upload_id_seq'::regclass);
ALTER TABLE ONLY uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regclass);
ALTER TABLE ONLY user_agent_details ALTER COLUMN id SET DEFAULT nextval('user_agent_details_id_seq'::regclass);
......@@ -24022,6 +24045,9 @@ ALTER TABLE ONLY u2f_registrations
ALTER TABLE ONLY upcoming_reconciliations
ADD CONSTRAINT upcoming_reconciliations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY upload_states
ADD CONSTRAINT upload_states_pkey PRIMARY KEY (upload_id);
ALTER TABLE ONLY uploads
ADD CONSTRAINT uploads_pkey PRIMARY KEY (id);
......@@ -27582,6 +27608,16 @@ CREATE UNIQUE INDEX index_unit_test_failures_unique_columns ON ci_unit_test_fail
CREATE UNIQUE INDEX index_upcoming_reconciliations_on_namespace_id ON upcoming_reconciliations USING btree (namespace_id);
CREATE INDEX index_upload_states_failed_verification ON upload_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
CREATE INDEX index_upload_states_needs_verification ON upload_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
CREATE INDEX index_upload_states_on_upload_id ON upload_states USING btree (upload_id);
CREATE INDEX index_upload_states_on_verification_state ON upload_states USING btree (verification_state);
CREATE INDEX index_upload_states_pending_verification ON upload_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
CREATE INDEX index_uploads_on_checksum ON uploads USING btree (checksum);
CREATE INDEX index_uploads_on_model_id_and_model_type ON uploads USING btree (model_id, model_type);
......@@ -31095,6 +31131,9 @@ ALTER TABLE ONLY resource_iteration_events
ALTER TABLE ONLY vulnerability_finding_evidence_requests
ADD CONSTRAINT fk_rails_cf0f278cb0 FOREIGN KEY (vulnerability_finding_evidence_supporting_message_id) REFERENCES vulnerability_finding_evidence_supporting_messages(id) ON DELETE CASCADE;
ALTER TABLE ONLY upload_states
ADD CONSTRAINT fk_rails_d00f153613 FOREIGN KEY (upload_id) REFERENCES uploads(id) ON DELETE CASCADE;
ALTER TABLE ONLY epic_metrics
ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE;
......@@ -272,6 +272,12 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_uploads_synced` | Gauge | 14.1 | Number of uploads synced on secondary | `url` |
| `geo_uploads_failed` | Gauge | 14.1 | Number of syncable uploads failed to sync on secondary | `url` |
| `geo_uploads_registry` | Gauge | 14.1 | Number of uploads in the registry | `url` |
| `geo_uploads_checksum_total` | Gauge | 14.6 | Number of uploads tried to checksum on primary | `url` |
| `geo_uploads_checksummed` | Gauge | 14.6 | Number of uploads successfully checksummed on primary | `url` |
| `geo_uploads_checksum_failed` | Gauge | 14.6 | Number of uploads failed to calculate the checksum on primary | `url` |
| `geo_uploads_verification_total` | Gauge | 14.6 | Number of uploads verifications tried on secondary | `url` |
| `geo_uploads_verified` | Gauge | 14.6 | Number of uploads verified on secondary | `url` |
| `geo_uploads_verification_failed` | Gauge | 14.6 | Number of uploads verifications failed on secondary | `url` |
| `gitlab_sli:rails_request_apdex:total` | Counter | 14.4 | The number of request-apdex measurements, [more information the development documentation](../../../development/application_slis/rails_request_apdex.md) | `endpoint_id`, `feature_category`, `request_urgency` |
| `gitlab_sli:rails_request_apdex:success_total` | Counter | 14.4 | The number of succesful requests that met the target duration for their urgency. Devide by `gitlab_sli:rails_requests_apdex:total` to get a success ratio | `endpoint_id`, `feature_category`, `request_urgency` |
......
......@@ -453,6 +453,13 @@ Example response:
"uploads_failed_count": 0,
"uploads_registry_count": null,
"uploads_synced_in_percentage": "0.00%",
"uploads_checksum_total_count": 5,
"uploads_checksummed_count": 5,
"uploads_checksum_failed_count": null,
"uploads_verification_total_count": null,
"uploads_verified_count": null,
"uploads_verification_failed_count": null,
"uploads_verified_in_percentage": "0.00%",
},
{
"geo_node_id": 2,
......@@ -595,6 +602,13 @@ Example response:
"uploads_failed_count": 0,
"uploads_registry_count": null,
"uploads_synced_in_percentage": "0.00%",
"uploads_checksum_total_count": 5,
"uploads_checksummed_count": 5,
"uploads_checksum_failed_count": null,
"uploads_verification_total_count": null,
"uploads_verified_count": null,
"uploads_verification_failed_count": null,
"uploads_verified_in_percentage": "0.00%",
}
]
```
......@@ -734,6 +748,13 @@ Example response:
"uploads_failed_count": 0,
"uploads_registry_count": null,
"uploads_synced_in_percentage": "0.00%",
"uploads_checksum_total_count": 5,
"uploads_checksummed_count": 5,
"uploads_checksum_failed_count": null,
"uploads_verification_total_count": null,
"uploads_verified_count": null,
"uploads_verification_failed_count": null,
"uploads_verified_in_percentage": "0.00%",
}
```
......
......@@ -6,6 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Managed Licenses API **(ULTIMATE)**
WARNING:
"approval" and "blacklisted" approval statuses are deprecated and scheduled to be changed to "allowed" and "denied" in GitLab 15.0.
## List managed licenses
Get all managed licenses for a given project.
......@@ -78,10 +81,10 @@ POST /projects/:id/managed_licenses
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the managed license |
| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated. |
```shell
curl --data "name=MIT&approval_status=blacklisted" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses"
curl --data "name=MIT&approval_status=denied" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses"
```
Example response:
......@@ -125,10 +128,10 @@ PATCH /projects/:id/managed_licenses/:managed_license_id
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
| `approval_status` | string | yes | The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated. |
```shell
curl --request PATCH --data "approval_status=blacklisted" \
curl --request PATCH --data "approval_status=denied" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6"
```
......
# frozen_string_literal: true
module IncidentManagement
class EscalationRulesFinder
# @param project [Project, Array<Project>, Integer(project_id), Array<Integer>, Project::ActiveRecord_Relation]
# Limit rules to within these projects
# @param user [Project, Array<Project>, Integer(user_id), Array<Integer>, User::ActiveRecord_Relation]
# Limit rules to those which notify these users directly
# @param include_removed [Boolean] Include rules which have been deleted in the UI but may correspond to existing pending escalations.
# @param member [GroupMember, ProjectMember] A member which will be disambiguated into project/user params
def initialize(user: nil, project: nil, include_removed: false, member: nil)
@user = user
@project = project
@include_removed = include_removed
disambiguate_member(member)
end
def execute
rules = by_project(IncidentManagement::EscalationRule)
rules = by_user(rules)
with_removed(rules)
end
private
attr_reader :member, :user, :project, :include_removed
def by_project(rules)
return rules unless project
rules.for_project(project)
end
def by_user(rules)
return rules unless user
rules.for_user(user)
end
def with_removed(rules)
return rules if include_removed
rules.not_removed
end
def disambiguate_member(member)
return unless member
raise ArgumentError, 'Member param cannot be used with project or user params' if user || project
raise ArgumentError, 'Member does not correspond to a user' unless member.user
@user = member.user
@project = member.source_type == 'Project' ? member.source_id : member.source.projects
end
end
end
......@@ -30,6 +30,17 @@ module EE
mail(to: user.notification_email_for(@project.group),
subject: subject('Mirror user changed'))
end
def user_escalation_rule_deleted_email(user, project, rules, recipient)
@user = user
@project = project
@rules = rules
mail(to: recipient.notification_email_for(@project.group), subject: subject('User removed from escalation policy')) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
end
end
end
......@@ -11,14 +11,47 @@ module EE
prepended do
include ::Gitlab::SQL::Pattern
include ::Gitlab::Geo::ReplicableModel
include ::Gitlab::Geo::VerificationState
with_replicator ::Geo::UploadReplicator
scope :for_model, ->(model) { where(model_id: model.id, model_type: model.class.name) }
scope :syncable, -> { with_files_stored_locally }
scope :with_verification_state, ->(state) { joins(:upload_state).where(upload_states: { verification_state: verification_state_value(state) }) }
scope :checksummed, -> { joins(:upload_state).where.not(upload_states: { verification_checksum: nil } ) }
scope :not_checksummed, -> { joins(:upload_state).where(upload_states: { verification_checksum: nil } ) }
scope :available_verifiables, -> { joins(:upload_state) }
has_one :upload_state,
autosave: false,
inverse_of: :upload,
class_name: '::Geo::UploadState'
delegate :verification_retry_at, :verification_retry_at=,
:verified_at, :verified_at=,
:verification_checksum, :verification_checksum=,
:verification_failure, :verification_failure=,
:verification_retry_count, :verification_retry_count=,
:verification_state=, :verification_state,
:verification_started_at=, :verification_started_at,
to: :upload_state
after_save :save_verification_details
def verification_state_object
upload_state
end
end
class_methods do
extend ::Gitlab::Utils::Override
override :verification_state_table_class
def verification_state_table_class
::Geo::UploadState
end
# @param primary_key_in [Range, Upload] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<Upload>] everything that should be synced to this node, restricted by primary key
def replicables_for_current_secondary(primary_key_in)
......@@ -77,5 +110,9 @@ module EE
# Keep empty for now. Should be addressed in future
# by https://gitlab.com/gitlab-org/gitlab/issues/33817
end
def upload_state
super || build_upload_state
end
end
end
......@@ -2,6 +2,7 @@
class Geo::UploadRegistry < Geo::BaseRegistry
include ::Geo::ReplicableRegistry
include ::Geo::VerifiableRegistry
extend ::Gitlab::Utils::Override
......@@ -77,4 +78,17 @@ class Geo::UploadRegistry < Geo::BaseRegistry
def project
return upload.model if upload&.model.is_a?(Project)
end
# Returns a synchronization state based on existing attribute values
#
# It takes into account things like if a successful replication has been done
# if there are pending actions or existing errors
#
# @return [Symbol] :synced, :never, or :failed
def synchronization_state
return :synced if success?
return :never if retry_count.nil?
:failed
end
end
# frozen_string_literal: true
module Geo
class UploadState < ApplicationRecord
self.primary_key = :upload_id
belongs_to :upload, inverse_of: :upload_state
validates :verification_failure, length: { maximum: 255 }
validates :verification_state, :upload, presence: true
end
end
......@@ -7,6 +7,7 @@ module IncidentManagement
belongs_to :policy, class_name: 'EscalationPolicy', inverse_of: 'rules', foreign_key: 'policy_id'
belongs_to :oncall_schedule, class_name: 'OncallSchedule', foreign_key: 'oncall_schedule_id', optional: true
belongs_to :user, optional: true
has_one :project, through: :policy, source: :project
enum status: ::IncidentManagement::Escalatable::STATUSES.slice(:acknowledged, :resolved)
......@@ -27,6 +28,10 @@ module IncidentManagement
scope :not_removed, -> { where(is_removed: false) }
scope :removed, -> { where(is_removed: true) }
scope :for_user, -> (user) { where(user: user) }
scope :for_project, -> (project) { where(policy: { project: project }).joins(:policy).references(:policy) }
scope :load_project_with_routes, -> { preload(project: [:route, { namespace: :route }]) }
scope :load_policy, -> { includes(:policy) }
private
......
......@@ -38,6 +38,7 @@ module IncidentManagement
# Note! If changing the order of participants, also change the :with_shift_generation_associations scope.
has_many :active_participants, -> { not_removed.order(id: :asc) }, class_name: 'OncallParticipant', inverse_of: :rotation
has_many :users, through: :participants
has_many :participating_users, through: :active_participants, source: :user
has_many :shifts, class_name: 'OncallShift', inverse_of: :rotation, foreign_key: :rotation_id
validates :name, presence: true, uniqueness: { scope: :oncall_schedule_id }, length: { maximum: NAME_LENGTH }
......
......@@ -34,6 +34,11 @@ module Geo
::Geo::BatchEventCreateWorker.perform_async(events)
end
override :verification_feature_flag_enabled?
def self.verification_feature_flag_enabled?
Feature.enabled?(:geo_upload_verification, default_enabled: :yaml)
end
def carrierwave_uploader
model_record.retrieve_uploader
end
......
......@@ -15,6 +15,7 @@ module EE
cleanup_group_identity(member)
cleanup_group_deletion_schedule(member) if member.source.is_a?(Group)
cleanup_oncall_rotations(member)
cleanup_escalation_rules(member) if member.user
end
private
......@@ -65,6 +66,12 @@ module EE
user
).execute
end
def cleanup_escalation_rules(member)
rules = ::IncidentManagement::EscalationRulesFinder.new(member: member, include_removed: true).execute
::IncidentManagement::EscalationRules::DestroyService.new(escalation_rules: rules, user: member.user).execute
end
end
end
end
......@@ -77,21 +77,41 @@ module EE
end
def oncall_user_removed(rotation, user, async_notification = true)
project_owners_and_participants(rotation, user).each do |recipient|
oncall_user_removed_recipients(rotation, user).each do |recipient|
email = mailer.user_removed_from_rotation_email(user, rotation, [recipient])
async_notification ? email.deliver_later : email.deliver_now
end
end
def user_escalation_rule_deleted(project, user, rules)
user_escalation_rule_deleted_recipients(project, user).map do |recipient|
# Deliver now as rules (& maybe user) are being deleted
mailer.user_escalation_rule_deleted_email(user, project, rules, recipient).deliver_now
end
end
private
def project_owners_and_participants(rotation, user)
project = rotation.project
def oncall_user_removed_recipients(rotation, removed_user)
incident_management_owners(rotation.project)
.including(rotation.participating_users)
.excluding(removed_user)
.uniq
end
def user_escalation_rule_deleted_recipients(project, removed_user)
incident_management_owners(project).excluding(removed_user)
end
owners = project.owner.is_a?(Group) ? project.owner.owners : [project.owner]
member_owners = project.members.owners
def incident_management_owners(project)
return [project.owner] unless project.group
(owners + member_owners + rotation.participants.map(&:user) - [user]).compact.uniq
MembersFinder
.new(project, nil, params: { active_without_invites_and_requests: true })
.execute
.owners
.map(&:user)
end
def add_mr_approvers_email(merge_request, approvers, current_user)
......
......@@ -10,6 +10,7 @@ module EE
result = super(user, options) do |delete_user|
mirror_cleanup(delete_user)
oncall_rotations_cleanup(delete_user)
escalation_rules_cleanup(delete_user)
end
log_audit_event(user) if result.try(:destroyed?)
......@@ -40,6 +41,12 @@ module EE
).execute
end
def escalation_rules_cleanup(user)
rules = ::IncidentManagement::EscalationRulesFinder.new(user: user, include_removed: true).execute
::IncidentManagement::EscalationRules::DestroyService.new(escalation_rules: rules, user: user).execute
end
private
def first_mirror_owner(user, mirror)
......
# frozen_string_literal: true
module IncidentManagement
module EscalationRules
# Permanently deletes escalation rules in bulk. To remove
# a rule but allow it continue notifying for existing
# escalations, prefer updating EscalationRule#is_removed.
class DestroyService
# @param escalation_rules [ActiveRecord::Relation<EscalationRule>] The rules to be deleted
# @param user [User] User corresponding to escalation rules
def initialize(escalation_rules:, user:)
@escalation_rules = escalation_rules
@user = user
end
def execute
preload_associations
send_user_deleted_emails
# Records are already loaded, so `#ids` does not incur extra query & simplifies deletion
IncidentManagement::EscalationRule.id_in(escalation_rules.ids).delete_all # rubocop: disable CodeReuse/ActiveRecord
end
private
attr_reader :escalation_rules, :user
def preload_associations
@escalation_rules = escalation_rules.load_policy.load_project_with_routes
end
def send_user_deleted_emails
escalation_rules
.group_by(&:project)
.each { |project, rules| send_user_rule_deleted_email(project, rules) }
end
def send_user_rule_deleted_email(project, rules)
NotificationService.new.user_escalation_rule_deleted(project, user, rules)
end
end
end
end
%p
= html_escape(_("%{user_name} (%{user_username}) was removed from the following escalation policies in %{project_link}: ")) % { user_name: @user.name, user_username: @user.username, project_link: link_to(@project.name, project_url(@project).html_safe) }
%ul
- @rules.each do |rule|
- policy_link = link_to(rule.policy.name, project_incident_management_escalation_policies_url(@project).html_safe)
%li= html_escape(_("%{policy_link} (notifying after %{elapsed_time} minutes unless %{status})")) % { policy_link: policy_link, elapsed_time: rule.elapsed_time_seconds / 60, status: rule.status }
%p
= html_escape(_("Please review the updated escalation policies for %{project_link}. It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage.")) % { project_link: link_to(@project.name, project_url(@project).html_safe) }
<%= _("%{user_name} (%{user_username}) was removed from the following escalation policies in %{project}:") % { user_name: @user.name, user_username: @user.username, project: @project.name } %>
<% @rules.each do |rule| %>
<%= _("- %{policy_name} (notifying after %{elapsed_time} minutes unless %{status})") % { policy_name: rule.policy.name, elapsed_time: rule.elapsed_time_seconds / 60, status: rule.status } %>
<% end %>
<%= _("Please review the updated escalation policies for %{project}. It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage.") % { project: @project.name } %>
---
name: geo_upload_verification
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65921
rollout_issue_url:
milestone: '14.6'
type: development
group: group::geo
default_enabled: false
# frozen_string_literal: true
class PrepareFileRegistryForVerification < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_column :file_registry, :verified_at, :datetime_with_timezone
add_column :file_registry, :verification_started_at, :datetime_with_timezone
add_column :file_registry, :verification_retry_at, :datetime_with_timezone
add_column :file_registry, :verification_state, :integer, default: 0, null: false, limit: 2
add_column :file_registry, :verification_retry_count, :integer, default: 0, limit: 2, null: false
add_column :file_registry, :verification_checksum, :binary
add_column :file_registry, :verification_checksum_mismatched, :binary
add_column :file_registry, :checksum_mismatch, :boolean, default: false, null: false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20211126312431_add_text_limit_to_file_registry_verification_failure.rb
add_column :file_registry, :verification_failure, :text
# rubocop:enable Migration/AddLimitToTextColumns
end
def down
remove_column :file_registry, :verified_at
remove_column :file_registry, :verification_started_at
remove_column :file_registry, :verification_retry_at
remove_column :file_registry, :verification_state
remove_column :file_registry, :verification_retry_count
remove_column :file_registry, :verification_checksum
remove_column :file_registry, :verification_checksum_mismatched
remove_column :file_registry, :checksum_mismatch
remove_column :file_registry, :verification_failure
end
end
# frozen_string_literal: true
class CreateFileRegistryVerificationIndexies < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_index :file_registry, :verification_retry_at, name: :file_registry_failed_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 3))"
# To optimize performance of UploadRegistry.needs_verification_count
add_concurrent_index :file_registry, :verification_state, name: :file_registry_needs_verification, where: "((state = 2) AND (verification_state = ANY (ARRAY[0, 3])))"
# To optimize performance of UploadRegistry.verification_pending_batch
add_concurrent_index :file_registry, :verified_at, name: :file_registry_pending_verification, order: "NULLS FIRST", where: "((state = 2) AND (verification_state = 0))"
end
def down
remove_concurrent_index :file_registry, :verification_retry_at, name: :file_registry_failed_verification
remove_concurrent_index :file_registry, :verification_state, name: :file_registry_needs_verification
remove_concurrent_index :file_registry, :verified_at, name: :file_registry_pending_verification
end
end
# frozen_string_literal: true
class AddTextLimitToFileRegistryVerificationFailure < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_text_limit :file_registry, :verification_failure, 256
end
def down
remove_text_limit :file_registry, :verification_failure
end
end
e7bcc5f9db03ce5856ee1851de05c3369fd70fce4a14b8eba3c31c66c625f9fd
\ No newline at end of file
b7ce85f62fc9fdc1363ba51becd096313ed0be6c0b1472468ec3de05bb3d93be
\ No newline at end of file
687dc025dd98af01fa9e92fd610cacebf442d8a3039ed4abff27bfcf6453a29a
\ No newline at end of file
......@@ -72,7 +72,17 @@ CREATE TABLE file_registry (
missing_on_primary boolean DEFAULT false NOT NULL,
state smallint DEFAULT 0 NOT NULL,
last_synced_at timestamp with time zone,
last_sync_failure character varying(255)
last_sync_failure character varying(255),
verified_at timestamp with time zone,
verification_started_at timestamp with time zone,
verification_retry_at timestamp with time zone,
verification_state smallint DEFAULT 0 NOT NULL,
verification_retry_count smallint DEFAULT 0 NOT NULL,
verification_checksum bytea,
verification_checksum_mismatched bytea,
checksum_mismatch boolean DEFAULT false NOT NULL,
verification_failure text,
CONSTRAINT check_1886652634 CHECK ((char_length(verification_failure) <= 256))
);
CREATE SEQUENCE file_registry_id_seq
......@@ -472,6 +482,12 @@ ALTER TABLE ONLY snippet_repository_registry
ALTER TABLE ONLY terraform_state_version_registry
ADD CONSTRAINT terraform_state_version_registry_pkey PRIMARY KEY (id);
CREATE INDEX file_registry_failed_verification ON file_registry USING btree (verification_retry_at NULLS FIRST) WHERE ((state = 2) AND (verification_state = 3));
CREATE INDEX file_registry_needs_verification ON file_registry USING btree (verification_state) WHERE ((state = 2) AND (verification_state = ANY (ARRAY[0, 3])));
CREATE INDEX file_registry_pending_verification ON file_registry USING btree (verified_at NULLS FIRST) WHERE ((state = 2) AND (verification_state = 0));
CREATE INDEX idx_project_registry_failed_repositories_partial ON project_registry USING btree (repository_retry_count) WHERE ((repository_retry_count > 0) OR (last_repository_verification_failure IS NOT NULL) OR repository_checksum_mismatch);
CREATE INDEX idx_project_registry_on_repo_checksums_and_failure_partial ON project_registry USING btree (project_id) WHERE ((repository_verification_checksum_sha IS NULL) AND (last_repository_verification_failure IS NULL));
......
......@@ -61,8 +61,8 @@ module API
requires :name, type: String, desc: 'The name of the license'
requires :approval_status,
type: String,
values: %w(approved blacklisted),
desc: 'The approval status of the license. "blacklisted" or "approved".'
values: %w(allowed denied approved blacklisted),
desc: 'The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated.'
end
post ':id/managed_licenses' do
authorize_can_admin!
......@@ -88,8 +88,8 @@ module API
optional :name, type: String, desc: 'The name of the license'
optional :approval_status,
type: String,
values: %w(approved blacklisted),
desc: 'The approval status of the license. "blacklisted" or "approved".'
values: %w(allowed denied approved blacklisted),
desc: 'The approval status of the license. "allowed" or "denied". "blacklisted" and "approved" are deprecated.'
end
patch ':id/managed_licenses/:managed_license_id', requirements: { managed_license_id: /.*/ } do
authorize_can_admin!
......
......@@ -23,5 +23,11 @@ FactoryBot.define do
last_synced_at { 1.day.ago }
retry_count { 0 }
end
trait :verification_succeeded do
verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' }
verification_state { Geo::UploadRegistry.verification_state_value(:verification_succeeded) }
verified_at { 5.days.ago }
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :geo_upload_state, class: '::Geo::UploadState' do
upload
trait(:checksummed) do
verification_checksum { 'abc' }
end
trait(:checksum_failure) do
verification_failure { 'Could not calculate the checksum' }
end
end
end
......@@ -7,5 +7,22 @@ FactoryBot.modify do
mount_point { :file }
uploader { ::IssuableMetricImageUploader.name }
end
trait(:verification_succeeded) do
with_file
verification_checksum { 'abc' }
verification_state { Upload.verification_state_value(:verification_succeeded) }
end
trait(:verification_failed) do
with_file
verification_failure { 'Could not calculate the checksum' }
verification_state { Upload.verification_state_value(:verification_failed) }
end
trait(:verification_pending) do
with_file
verification_state { Upload.verification_state_value(:verification_pending) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::EscalationRulesFinder do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:other_project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:policy) { create(:incident_management_escalation_policy, project: project) }
let_it_be(:other_policy) { create(:incident_management_escalation_policy, project: other_project) }
let_it_be(:schedule_rule) { policy.rules.first }
let_it_be(:schedule_rule_for_other_project) { other_policy.rules.first }
let_it_be(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: policy) }
let_it_be(:rule_for_user) { create(:incident_management_escalation_rule, :with_user, policy: policy, user: user) }
let_it_be(:rule_for_other_user) { create(:incident_management_escalation_rule, :with_user, policy: policy) }
let_it_be(:rule_for_other_policy) { create(:incident_management_escalation_rule, :with_user, policy: other_policy, user: user) }
let_it_be(:project_member) { project.members.first }
let_it_be(:group_member) { create(:group_member, :developer, group: group, user: user) }
let(:params) { {} }
describe '#execute' do
subject(:execute) { described_class.new(**params).execute }
context 'when project is given' do
let(:project_rules) { [rule_for_user, rule_for_other_user, schedule_rule] }
it 'returns the rules in the project for different types of project inputs' do
[
project,
project.id,
[project],
[project.id],
Project.where(id: project.id)
].each do |project_param|
expect(described_class.new(project: project_param).execute).to contain_exactly(*project_rules)
end
end
context 'when removed rules should be included' do
let(:params) { { include_removed: true, project: project } }
it { is_expected.to contain_exactly(removed_rule, *project_rules) }
end
end
context 'when user is given' do
it 'returns the user rules for different types of user inputs' do
[
user,
user.id,
[user],
[user.id],
User.where(id: user.id)
].each do |user_param|
expect(described_class.new(user: user_param).execute).to contain_exactly(
rule_for_user,
rule_for_other_policy
)
end
end
end
context 'when group member is given' do
let(:params) { { member: group_member } }
it { is_expected.to contain_exactly(rule_for_user, rule_for_other_policy) }
context 'when member does not belong to a user' do
let(:params) { { member: create(:group_member, :invited, group: group) } }
specify { expect { execute }.to raise_error(ArgumentError, 'Member does not correspond to a user') }
end
end
context 'when project member is given' do
let(:params) { { member: project_member } }
it { is_expected.to contain_exactly(rule_for_other_user) }
context 'when user is also given' do
let(:params) { { member: project_member, user: user } }
specify { expect { execute }.to raise_error(ArgumentError, 'Member param cannot be used with project or user params') }
end
context 'when project is also given' do
let(:params) { { member: project_member, project: project } }
specify { expect { execute }.to raise_error(ArgumentError, 'Member param cannot be used with project or user params') }
end
end
end
end
......@@ -153,16 +153,16 @@
"replication_slots_used_in_percentage",
"replication_slots_max_retained_wal_bytes",
"uploads_count",
"uploads_synced_count",
"uploads_failed_count",
"uploads_registry_count",
"uploads_synced_in_percentage",
"uploads_checksum_total_count",
"uploads_checksummed_count",
"uploads_checksum_failed_count",
"uploads_verification_failed_count",
"uploads_synced_count",
"uploads_failed_count",
"uploads_registry_count",
"uploads_verification_total_count",
"uploads_verified_count",
"uploads_verification_failed_count",
"uploads_synced_in_percentage",
"uploads_verified_in_percentage",
"git_fetch_event_count_weekly",
"git_push_event_count_weekly",
......@@ -323,16 +323,16 @@
"repositories_verified_in_percentage": { "type": "string" },
"repositories_checksum_mismatch_count": { "type": ["integer", "null"] },
"uploads_count": { "type": ["integer", "null"] },
"uploads_checksum_total_count": { "type": ["integer", "null"] },
"uploads_checksummed_count": { "type": ["integer", "null"] },
"uploads_checksum_failed_count": { "type": ["integer", "null"] },
"uploads_synced_count": { "type": ["integer", "null"] },
"uploads_failed_count": { "type": ["integer", "null"] },
"uploads_registry_count": { "type": ["integer", "null"] },
"uploads_synced_in_percentage": { "type": "string" },
"uploads_checksummed_count": { "type": ["integer", "null"] },
"uploads_checksum_failed_count": { "type": ["integer", "null"] },
"uploads_checksum_total_count": { "type": ["integer", "null"] },
"uploads_verification_failed_count": { "type": ["integer", "null"] },
"uploads_verification_total_count": { "type": ["integer", "null"] },
"uploads_verified_count": { "type": ["integer", "null"] },
"uploads_verification_failed_count": { "type": ["integer", "null"] },
"uploads_synced_in_percentage": { "type": "string" },
"uploads_verified_in_percentage": { "type": "string" },
"wikis_verified_count": { "type": ["integer", "null"] },
"wikis_verification_failed_count": { "type": ["integer", "null"] },
......
......@@ -15,7 +15,7 @@ RSpec.describe Gitlab::UsageDataNonSqlMetrics do
described_class.uncached_data
end
expect(recorder.count).to eq(59)
expect(recorder.count).to eq(63)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'email_spec'
RSpec.describe Emails::Projects do
include EmailSpec::Matchers
describe '#user_escalation_rule_deleted_email' do
let(:rule) { create(:incident_management_escalation_rule, :with_user) }
let(:user) { rule.user }
let(:project) { rule.project }
let(:recipient) { build(:user) }
let(:elapsed_time) { (rule.elapsed_time_seconds / 60).to_s }
subject { Notify.user_escalation_rule_deleted_email(user, project, [rule], recipient) }
it 'has the correct email content', :aggregate_failures do
is_expected.to have_subject("#{project.name} | User removed from escalation policy")
is_expected.to have_body_text(user.name)
is_expected.to have_body_text(user.username)
is_expected.to have_body_text('was removed from the following escalation policies')
is_expected.to have_body_text(rule.policy.name)
is_expected.to have_body_text(elapsed_time)
is_expected.to have_body_text(rule.status.to_s)
is_expected.to have_body_text("Please review the updated escalation policies for")
is_expected.to have_body_text(project.name)
is_expected.to have_body_text("It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage")
end
end
end
......@@ -11,3 +11,14 @@ RSpec.describe Geo::UploadRegistry, :geo, type: :model do
include_examples 'a Geo framework registry'
end
RSpec.describe Geo::UploadRegistry, :geo, type: :model do
let_it_be(:registry) { create(:geo_upload_registry) }
specify 'factory is valid' do
expect(registry).to be_valid
end
include_examples 'a Geo framework registry'
include_examples 'a Geo verifiable registry'
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Geo::UploadState, :geo, type: :model do
it { is_expected.to belong_to(:upload).inverse_of(:upload_state) }
end
......@@ -1302,35 +1302,25 @@ RSpec.describe GeoNodeStatus, :geo do
end
context 'on the secondary' do
it 'calls JobArtifactRegistryFinder#registry_count' do
expect_any_instance_of(Geo::JobArtifactRegistryFinder).to receive(:registry_count).twice
it 'returns data from the deprecated field if it is not defined in the status field' do
subject.write_attribute(:projects_count, 10)
subject.status = {}
subject
expect(subject.projects_count).to eq 10
end
end
context 'backward compatibility when counters stored in separate columns' do
describe '#projects_count' do
it 'returns data from the deprecated field if it is not defined in the status field' do
subject.write_attribute(:projects_count, 10)
subject.status = {}
expect(subject.projects_count).to eq 10
end
it 'sets data in the new status field' do
subject.projects_count = 10
it 'sets data in the new status field' do
subject.projects_count = 10
expect(subject.projects_count).to eq 10
end
expect(subject.projects_count).to eq 10
end
it 'uses column counters when calculates percents using attr_in_percentage' do
subject.write_attribute(:design_repositories_count, 10)
subject.write_attribute(:design_repositories_synced_count, 5)
subject.status = {}
it 'uses column counters when calculates percents using attr_in_percentage' do
subject.write_attribute(:design_repositories_count, 10)
subject.write_attribute(:design_repositories_synced_count, 5)
subject.status = {}
expect(subject.design_repositories_synced_in_percentage).to be_within(0.0001).of(50)
end
expect(subject.design_repositories_synced_in_percentage).to be_within(0.0001).of(50)
end
end
......
......@@ -9,6 +9,7 @@ RSpec.describe IncidentManagement::EscalationRule do
it { is_expected.to belong_to(:policy) }
it { is_expected.to belong_to(:oncall_schedule).optional }
it { is_expected.to belong_to(:user).optional }
it { is_expected.to have_one(:project).through(:policy) }
end
describe 'validations' do
......@@ -51,11 +52,12 @@ RSpec.describe IncidentManagement::EscalationRule do
describe 'scopes' do
let_it_be(:rule) { create(:incident_management_escalation_rule) }
let_it_be(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: rule.policy) }
let_it_be(:other_project_rule) { create(:incident_management_escalation_rule) }
describe '.not_removed' do
subject { described_class.not_removed }
it { is_expected.to contain_exactly(rule) }
it { is_expected.to contain_exactly(rule, other_project_rule) }
end
describe '.removed' do
......@@ -63,5 +65,13 @@ RSpec.describe IncidentManagement::EscalationRule do
it { is_expected.to contain_exactly(removed_rule) }
end
describe '.for_project' do
let(:project) { other_project_rule.project }
subject { described_class.for_project(project) }
it { is_expected.to contain_exactly(other_project_rule) }
end
end
end
......@@ -10,6 +10,7 @@ RSpec.describe IncidentManagement::OncallRotation do
it { is_expected.to have_many(:participants).order(id: :asc).class_name('OncallParticipant').inverse_of(:rotation) }
it { is_expected.to have_many(:active_participants).order(id: :asc).class_name('OncallParticipant').inverse_of(:rotation) }
it { is_expected.to have_many(:users).through(:participants) }
it { is_expected.to have_many(:participating_users).through(:active_participants).source(:user) }
it { is_expected.to have_many(:shifts).class_name('OncallShift').inverse_of(:rotation) }
describe '.active_participants' do
......
......@@ -6,6 +6,13 @@ RSpec.describe Upload do
include EE::GeoHelpers
using RSpec::Parameterized::TableSyntax
it { is_expected.to have_one(:upload_state).inverse_of(:upload).class_name('Geo::UploadState') }
include_examples 'a replicable model with a separate table for verification state' do
let(:verifiable_model_record) { build(:upload) }
let(:unverifiable_model_record) { build(:upload, store: ObjectStorage::Store::REMOTE) }
end
describe '.replicables_for_current_secondary' do
# Selective sync is configured relative to the upload's model. Take care not
# to specify a model_factory that contradicts factory.
......
......@@ -6,6 +6,7 @@ RSpec.describe Geo::UploadReplicator do
let(:model_record) { create(:upload, :with_file) }
include_examples 'a blob replicator'
include_examples 'a verifiable replicator'
describe '.bulk_create_delete_events_async' do
let(:deleted_upload) do
......
......@@ -141,7 +141,7 @@ RSpec.describe API::ManagedLicenses do
post api("/projects/#{project.id}/managed_licenses", maintainer_user),
params: {
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
approval_status: 'allowed'
}
end.to change {project.software_license_policies.count}.by(1)
......@@ -163,6 +163,22 @@ RSpec.describe API::ManagedLicenses do
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'creates managed license from deprecated approval status value' do
expect do
post api("/projects/#{project.id}/managed_licenses", maintainer_user),
params: {
name: 'NEW_LICENSE_NAME',
approval_status: 'approved'
}
end.to change {project.software_license_policies.count}.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('software_license_policy', dir: 'ee')
expect(json_response).to have_key('id')
expect(json_response['name']).to eq('NEW_LICENSE_NAME')
expect(json_response['approval_status']).to eq('approved')
end
end
context 'authorized user with read permissions' do
......@@ -210,7 +226,7 @@ RSpec.describe API::ManagedLicenses do
initial_name = initial_license.name
initial_classification = initial_license.classification
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", maintainer_user),
params: { approval_status: 'blacklisted' }
params: { approval_status: 'denied' }
updated_software_license_policy = project.software_license_policies.reload.first
......@@ -231,6 +247,26 @@ RSpec.describe API::ManagedLicenses do
expect(initial_classification).not_to eq(updated_software_license_policy.classification)
end
it 'updates managed license data with deprecated approval status' do
initial_license = project.software_license_policies.first
initial_id = initial_license.id
patch api("/projects/#{project.id}/managed_licenses/#{software_license_policy.id}", maintainer_user),
params: { approval_status: 'blacklisted' }
updated_software_license_policy = project.software_license_policies.reload.first
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('software_license_policy', dir: 'ee')
# Check that response is equal to the updated object
expect(json_response['id']).to eq(initial_id)
expect(json_response['name']).to eq(updated_software_license_policy.name)
expect(json_response['approval_status']).to eq('blacklisted')
# Check that the approval status was updated
expect(updated_software_license_policy).to be_denied
end
it 'responds with 404 Not Found if requesting non-existing managed license' do
patch api("/projects/#{project.id}/managed_licenses/#{non_existing_record_id}", maintainer_user)
......
......@@ -133,6 +133,46 @@ RSpec.describe Members::DestroyService do
end
end
end
context 'user escalation rules' do
let(:project) { create(:project, group: group) }
let(:project_2) { create(:project, group: group) }
let(:project_1_policy) { create(:incident_management_escalation_policy, project: project) }
let(:project_2_policy) { create(:incident_management_escalation_policy, project: project_2) }
let!(:project_1_rule) { create(:incident_management_escalation_rule, :with_user, user: member_user, policy: project_1_policy) }
let!(:project_2_rule) { create(:incident_management_escalation_rule, :with_user, user: member_user, policy: project_2_policy) }
shared_examples_for 'calls the destroy service' do |scope, *rules|
let(:rules_to_delete) { rules.map { |rule_name| send(rule_name) } }
let(:rules_to_preserve) { IncidentManagement::EscalationRule.all - rules_to_delete }
it "calls the destroy service #{scope}" do
expect(IncidentManagement::EscalationRules::DestroyService)
.to receive(:new)
.with({ escalation_rules: rules_to_delete, user: member_user })
.and_call_original
subject.execute(member)
rules_to_delete.each { |rule| expect { rule.reload }.to raise_error(ActiveRecord::RecordNotFound) }
rules_to_preserve.each { |rule| expect { rule.reload }.not_to raise_error }
end
end
context 'group member is removed' do
let(:other_user) { create(:user, developer_projects: [group]) }
let!(:other_user_rule) { create(:incident_management_escalation_rule, :with_user, user: other_user, policy: project_1_policy) }
let!(:other_namespace_rule) { create(:incident_management_escalation_rule, :with_user, user: member_user) }
include_examples 'calls the destroy service', 'with rules each project in the group', :project_1_rule, :project_2_rule
end
context 'project member is removed' do
let!(:member) { create(:project_member, source: project, user: member_user) }
include_examples 'calls the destroy service', 'with rules for the project', :project_1_rule
end
end
end
context 'when current user is not present' do # ie, when the system initiates the destroy
......
......@@ -895,7 +895,7 @@ RSpec.describe EE::NotificationService, :mailer do
end
end
context 'IncidentManagement::Oncall' do
context 'IncidentManagement' do
let_it_be(:user) { create(:user) }
describe '#notify_oncall_users_of_alert' do
......@@ -939,5 +939,51 @@ RSpec.describe EE::NotificationService, :mailer do
expect { subject.oncall_user_removed(rotation, user, false) }.to change(ActionMailer::Base.deliveries, :size).by(2)
end
end
describe '#user_escalation_rule_deleted' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:rules) { [rule_1, rule_2] }
let!(:rule_1) { create(:incident_management_escalation_rule, :with_user, project: project, user: user) }
let!(:rule_2) { create(:incident_management_escalation_rule, :with_user, :resolved, project: project, user: user) }
it 'immediately sends an email to the project owner' do
expect(Notify).to receive(:user_escalation_rule_deleted_email).with(user, project, rules, project.owner).once.and_call_original
expect(Notify).not_to receive(:user_escalation_rule_deleted_email).with(user, project, rules, user)
expect { subject.user_escalation_rule_deleted(project, user, rules) }.to change(ActionMailer::Base.deliveries, :size).by(1)
end
context 'when project owner is the removed user' do
let(:user) { project.owner }
it 'does not send an email' do
expect(Notify).not_to receive(:user_escalation_rule_deleted_email)
subject.user_escalation_rule_deleted(project, user, rules)
end
end
context 'with a group' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:owner) { create(:user) }
let(:blocked_owner) { create(:user, :blocked) }
before do
group.add_owner(owner)
group.add_owner(blocked_owner)
group.add_owner(user)
end
it 'immediately sends an email to the eligable project owners' do
expect(Notify).to receive(:user_escalation_rule_deleted_email).with(user, project, rules, owner).once.and_call_original
expect(Notify).not_to receive(:user_escalation_rule_deleted_email).with(user, project, rules, blocked_owner)
expect(Notify).not_to receive(:user_escalation_rule_deleted_email).with(user, project, rules, user)
expect { subject.user_escalation_rule_deleted(project, user, rules) }.to change(ActionMailer::Base.deliveries, :size).by(1)
end
end
end
end
end
......@@ -81,6 +81,32 @@ RSpec.describe Users::DestroyService do
end
end
context 'when user has escalation rules' do
let(:project) { create(:project) }
let(:user) { project.owner }
let(:project_policy) { create(:incident_management_escalation_policy, project: project) }
let!(:project_rule) { create(:incident_management_escalation_rule, :with_user, policy: project_policy, user: user) }
let(:group) { create(:group) }
let(:group_project) { create(:project, group: group) }
let(:group_policy) { create(:incident_management_escalation_policy, project: group_project) }
let!(:group_rule) { create(:incident_management_escalation_rule, :with_user, policy: group_policy, user: user) }
let!(:group_owner) { create(:user) }
before do
group.add_developer(user)
group.add_owner(group_owner)
end
it 'deletes the escalation rules and notifies owners of group projects' do
expect { operation }.to change(ActionMailer::Base.deliveries, :size).by(1)
expect { project.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { project_rule.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { group_rule.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
describe 'audit events' do
include_examples 'audit event logging' do
let(:fail_condition!) do
......
......@@ -9,6 +9,7 @@ RSpec.describe Geo::FileUploadService do
before do
stub_current_geo_node(node)
stub_feature_flags(geo_upload_verification: false)
end
describe '#retriever' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::EscalationRules::DestroyService do
let!(:user) { create(:user) }
let(:project) { create(:project) }
let(:policy) { create(:incident_management_escalation_policy, project: project) }
let!(:rule_1) { create(:incident_management_escalation_rule, :with_user, user: user, policy: policy) }
let!(:rule_2) { create(:incident_management_escalation_rule, :with_user, :resolved, user: user, policy: policy) }
let!(:excluded_rule) { create(:incident_management_escalation_rule, :with_user, user: user, policy: policy, elapsed_time_seconds: 60) }
let(:other_project) { create(:project) }
let(:other_policy) { create(:incident_management_escalation_policy, project: other_project) }
let!(:other_project_rule) { create(:incident_management_escalation_rule, :with_user, user: user, policy: other_policy) }
let(:escalation_rules) { IncidentManagement::EscalationRule.id_in([rule_1, rule_2, other_project_rule]) }
let(:notification_service) { NotificationService.new }
let(:service) { described_class.new(escalation_rules: escalation_rules, user: user) }
subject(:execute) { service.execute }
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end
it 'sends an email for each project and deletes the provided escalation rules' do
allow(NotificationService).to receive(:new).and_return(notification_service)
expect(notification_service).to receive(:user_escalation_rule_deleted).with(project, user, [rule_1, rule_2]).once
expect(notification_service).to receive(:user_escalation_rule_deleted).with(other_project, user, [other_project_rule]).once
execute
expect { rule_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { rule_2.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { other_project_rule.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { excluded_rule.reload }.not_to raise_error
end
end
......@@ -14,6 +14,7 @@ RSpec.describe Projects::CleanupService do
describe '#execute' do
before do
stub_current_geo_node(primary)
stub_feature_flags(geo_upload_verification: false)
end
it 'sends a Geo notification about the update on success' do
......
......@@ -12,6 +12,47 @@
RSpec.shared_examples 'a replicable model with a separate table for verification state' do
include EE::GeoHelpers
describe '.with_verification_state' do
let(:verification_model_class) { verifiable_model_record.class }
it 'returns records with given scope' do
expect(verification_model_class.with_verification_state(:verification_succeeded).size).to eq(0)
verifiable_model_record.verification_failed_with_message!('Test')
expect(verification_model_class.with_verification_state(:verification_failed).first).to eq verifiable_model_record
end
end
describe '.checksummed' do
let(:verification_model_class) { verifiable_model_record.class }
it 'returns records with given scope' do
expect(verification_model_class.checksummed.size).to eq(0)
verifiable_model_record.verification_started!
verifiable_model_record.verification_succeeded_with_checksum!('checksum', Time.now)
expect(verification_model_class.checksummed.first).to eq verifiable_model_record
end
end
describe '.not_checksummed' do
let(:verification_model_class) { verifiable_model_record.class }
it 'returns records with given scope' do
verifiable_model_record.verification_started!
verifiable_model_record.verification_failed_with_message!('checksum error')
expect(verification_model_class.not_checksummed.first).to eq verifiable_model_record
verifiable_model_record.verification_started!
verifiable_model_record.verification_succeeded_with_checksum!('checksum', Time.now)
expect(verification_model_class.not_checksummed.size).to eq(0)
end
end
describe '#save_verification_details' do
let(:verification_state_table_class) { verifiable_model_record.class.verification_state_table_class }
......
......@@ -474,6 +474,8 @@ RSpec.shared_examples 'a verifiable replicator' do
end
it 'creates checksum_succeeded event' do
model_record
expect { replicator.handle_after_checksum_succeeded }.to change { ::Geo::Event.count }.by(1)
expect(::Geo::Event.last.event_name).to eq 'checksum_succeeded'
end
......
......@@ -490,6 +490,7 @@ trending_projects: :gitlab_main
u2f_registrations: :gitlab_main
upcoming_reconciliations: :gitlab_main
uploads: :gitlab_main
upload_states: :gitlab_main
user_agent_details: :gitlab_main
user_callouts: :gitlab_main
user_canonical_emails: :gitlab_main
......
......@@ -43,27 +43,27 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
'da_DK' => 52,
'da_DK' => 51,
'de' => 15,
'en' => 100,
'eo' => 0,
'es' => 40,
'es' => 39,
'fil_PH' => 0,
'fr' => 11,
'fr' => 12,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 2,
'ja' => 36,
'ja' => 35,
'ko' => 11,
'nb_NO' => 34,
'nb_NO' => 33,
'nl_NL' => 0,
'pl_PL' => 5,
'pt_BR' => 49,
'ro_RO' => 24,
'ru' => 26,
'ro_RO' => 23,
'ru' => 25,
'tr_TR' => 15,
'uk' => 39,
'zh_CN' => 97,
'uk' => 45,
'zh_CN' => 95,
'zh_HK' => 2,
'zh_TW' => 3
}.freeze
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -838,6 +838,9 @@ msgstr ""
msgid "%{placeholder} is not a valid theme"
msgstr ""
msgid "%{policy_link} (notifying after %{elapsed_time} minutes unless %{status})"
msgstr ""
msgid "%{primary} (%{secondary})"
msgstr ""
......@@ -1035,6 +1038,12 @@ msgstr ""
msgid "%{user_name} (%{user_username}) was removed from %{rotation} in %{schedule} in %{project}. "
msgstr ""
msgid "%{user_name} (%{user_username}) was removed from the following escalation policies in %{project_link}: "
msgstr ""
msgid "%{user_name} (%{user_username}) was removed from the following escalation policies in %{project}:"
msgstr ""
msgid "%{user_name} profile page"
msgstr ""
......@@ -1218,6 +1227,9 @@ msgstr ""
msgid ", or "
msgstr ""
msgid "- %{policy_name} (notifying after %{elapsed_time} minutes unless %{status})"
msgstr ""
msgid "- Available to run jobs."
msgstr ""
......@@ -26198,6 +26210,12 @@ msgstr ""
msgid "Please refer to %{docs_url}"
msgstr ""
msgid "Please review the updated escalation policies for %{project_link}. It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage."
msgstr ""
msgid "Please review the updated escalation policies for %{project}. It is recommended that you reach out to the current on-call responder to ensure continuity of on-call coverage."
msgstr ""
msgid "Please select"
msgstr ""
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment