Commit b94d2478 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 91c27077 db3f0ce4
......@@ -16,12 +16,25 @@ export default {
components: {
GlModal,
},
model: {
prop: 'visible',
event: 'change',
},
props: {
environment: {
type: Object,
required: true,
},
visible: {
type: Boolean,
required: false,
default: false,
},
hasMultipleCommits: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
......@@ -36,25 +49,28 @@ export default {
},
commitShortSha() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'short_id');
if (this.hasMultipleCommits) {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'short_id');
}
return this.environment.commitShortSha;
},
commitUrl() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'commit_path');
},
if (this.hasMultipleCommits) {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'commit_path');
}
commitTitle() {
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'title');
return this.environment.commitUrl;
},
modalText() {
const linkStart = `<a class="commit-sha mr-0" href="${escape(this.commitUrl)}">`;
const commitId = escape(this.commitShortSha);
const linkEnd = '</a>';
const name = escape(this.name);
const name = escape(this.environment.name);
const body = this.environment.isLastDeployment
? s__(
'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
......@@ -82,6 +98,10 @@ export default {
},
methods: {
handleChange(event) {
this.$emit('change', event);
},
onOk() {
eventHub.$emit('rollbackEnvironment', this.environment);
},
......@@ -99,10 +119,12 @@ export default {
<template>
<gl-modal
:title="modalTitle"
:visible="visible"
modal-id="confirm-rollback-modal"
:ok-title="modalActionText"
ok-variant="danger"
@ok="onOk"
@change="handleChange"
>
<p v-html="modalText"></p>
</gl-modal>
......
<script>
import { parseBoolean } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
components: {
ConfirmRollbackModal,
},
props: {
selector: {
type: String,
required: true,
},
},
data() {
return {
environment: null,
retryPath: '',
visible: false,
};
},
mounted() {
eventHub.$on('rollbackEnvironment', () => {
redirectTo(this.retryPath);
});
document.querySelectorAll(this.selector).forEach((button) => {
button.addEventListener('click', (e) => {
e.preventDefault();
const {
environmentName,
commitShortSha,
commitUrl,
isLastDeployment,
retryPath,
} = button.dataset;
this.environment = {
name: environmentName,
commitShortSha,
commitUrl,
isLastDeployment: parseBoolean(isLastDeployment),
};
this.retryPath = retryPath;
this.visible = true;
});
});
},
};
</script>
<template>
<confirm-rollback-modal
v-if="environment"
v-model="visible"
:environment="environment"
:has-multiple-commits="false"
/>
<div v-else></div>
</template>
import Vue from 'vue';
import RollbackModalManager from './components/rollback_modal_manager.vue';
const mountConfirmRollbackModal = (optionalProps) =>
new Vue({
render(h) {
return h(RollbackModalManager, {
props: {
selector: '.js-confirm-rollback-modal-button',
...optionalProps,
},
});
},
}).$mount();
export default (optionalProps = {}) => mountConfirmRollbackModal(optionalProps);
import initConfirmRollBackModal from '~/environments/init_confirm_rollback_modal';
import { initHeader } from '~/environments/mount_show';
initHeader();
initConfirmRollBackModal();
- commit_sha = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha has-tooltip", title: h(deployment.commit_title)
.modal.ws-normal.fade{ tabindex: -1, id: "confirm-rollback-modal-#{deployment.id}" }
.modal-dialog
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
- if deployment.last?
= s_("Environments|Re-deploy environment %{environment_name}?") % {environment_name: @environment.name}
- else
= s_("Environments|Rollback environment %{environment_name}?") % {environment_name: @environment.name}
.modal-body
- if deployment.last?
%p= s_('Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
- else
%p
= s_('Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha, environment_name: @environment.name}
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn gl-button btn-danger', data: { dismiss: 'modal' }
= link_to [:retry, @project, deployment.deployable], method: :post, class: 'btn gl-button btn-danger' do
- if deployment.last?
= s_('Environments|Re-deploy')
- else
= s_('Environments|Rollback')
- if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= button_tag class: 'gl-button btn btn-default btn-icon has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
= button_tag class: 'js-confirm-rollback-modal-button gl-button btn btn-default btn-icon has-tooltip', type: 'button', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) }, title: tooltip do
- if deployment.last?
= sprite_icon('repeat', css_class: 'gl-icon')
- else
= sprite_icon('redo', css_class: 'gl-icon')
= render 'projects/deployments/confirm_rollback_modal', deployment: deployment
......@@ -25,6 +25,9 @@ class ApprovalProjectRule < ApplicationRecord
validates :scanners, inclusion: { in: ::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES }
default_value_for :scanners, allows_nil: false, value: ::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES
validates :vulnerabilities_allowed, numericality: { only_integer: true }
default_value_for :vulnerabilities_allowed, allows_nil: false, value: 0
def applies_to_branch?(branch)
return true if protected_branches.empty?
......
......@@ -804,6 +804,10 @@ module EE
namespace.created_at >= PUBLIC_COST_FACTOR_RELEASE_DAY
end
def vulnerability_report_rule
approval_rules.vulnerability_reports.first
end
private
def github_integration_enabled?
......
......@@ -22,6 +22,10 @@ module Ci
backtrace: error.backtrace
)
error("Failed to update approval rules")
ensure
[:project_rule_vulnerabilities_allowed, :project_vulnerability_rule, :reports].each do |memoization|
clear_memoization(memoization)
end
end
private
......@@ -55,13 +59,19 @@ module Ci
def reports
strong_memoize(:reports) do
project_vulnerability_rules ? pipeline.security_reports(report_types: project_vulnerability_rules) : []
project_vulnerability_rule ? pipeline.security_reports(report_types: project_vulnerability_rule) : []
end
end
def project_vulnerability_rule
strong_memoize(:project_vulnerability_rule) do
pipeline.project.vulnerability_report_rule&.scanners
end
end
def project_vulnerability_rules
strong_memoize(:project_vulnerability_rules) do
pipeline.project.approval_rules.vulnerability_reports.first&.scanners
def project_rule_vulnerabilities_allowed
strong_memoize(:project_rule_vulnerabilities_allowed) do
pipeline.project.vulnerability_report_rule&.vulnerabilities_allowed
end
end
......@@ -76,7 +86,7 @@ module Ci
def merge_requests_approved_security_reports
pipeline.merge_requests_as_head_pipeline.reject do |merge_request|
reports.present? && reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports)
reports.present? && reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports, project_rule_vulnerabilities_allowed)
end
end
......
......@@ -13,7 +13,8 @@ module IncidentManagement
feature_category :incident_management
def perform(escalation_id)
escalation = IncidentManagement::PendingEscalations::Alert.find(escalation_id)
escalation = IncidentManagement::PendingEscalations::Alert.find_by_id(escalation_id)
return unless escalation
IncidentManagement::PendingEscalations::ProcessService.new(escalation).execute
end
......
......@@ -13,7 +13,8 @@ module IncidentManagement
feature_category :incident_management
def perform(alert_id)
alert = ::AlertManagement::Alert.find(alert_id)
alert = ::AlertManagement::Alert.find_by_id(alert_id)
return unless alert
::IncidentManagement::PendingEscalations::CreateService.new(alert).execute
end
......
......@@ -22,8 +22,8 @@ module Gitlab
reports.values.flat_map(&:findings)
end
def violates_default_policy_against?(target_reports)
findings_diff(target_reports).any?(&:unsafe?)
def violates_default_policy_against?(target_reports, vulnerabilities_allowed)
unsafe_findings_count(target_reports) > vulnerabilities_allowed
end
private
......@@ -31,6 +31,10 @@ module Gitlab
def findings_diff(target_reports)
findings - target_reports&.findings.to_a
end
def unsafe_findings_count(target_reports)
findings_diff(target_reports).count(&:unsafe?)
end
end
end
end
......
......@@ -56,8 +56,9 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
describe "#violates_default_policy_against?" do
let(:low_severity_sast) { build(:ci_reports_security_finding, severity: 'low', report_type: :sast) }
let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
let(:vulnerabilities_allowed) { 0 }
subject { security_reports.violates_default_policy_against?(target_reports) }
subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed) }
context 'when the target_reports is `nil`' do
let(:target_reports) { nil }
......@@ -90,6 +91,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
end
it { is_expected.to be(true) }
context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do
let(:vulnerabilities_allowed) { 10000 }
it { is_expected.to be(false) }
end
end
context "when none of the reports have a new unsafe vulnerability" do
......
......@@ -161,17 +161,19 @@ RSpec.describe ApprovalProjectRule do
context "with a `Vulnerability-Check` rule" do
using RSpec::Parameterized::TableSyntax
where(:is_valid, :scanners) do
true | []
true | %w(dast)
true | %w(dast sast)
true | %w(dast dast)
false | %w(dast unknown_scanner)
false | %w(unknown_scanner)
where(:is_valid, :scanners, :vulnerabilities_allowed) do
true | [] | 0
true | %w(dast) | 1
true | %w(dast sast) | 10
true | %w(dast dast) | 100
false | %w(dast unknown_scanner) | 100
false | %w(unknown_scanner) | 100
false | %w(dast sast) | 1.1
false | %w(dast sast) | 'one'
end
with_them do
let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners) }
let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed) }
specify { expect(vulnerability_check_rule.valid?).to be(is_valid) }
end
......
......@@ -2921,4 +2921,27 @@ RSpec.describe Project do
end
end
end
describe '#vulnerability_report_rule' do
subject { project.vulnerability_report_rule }
context 'with vulnerability report rule' do
let!(:rule) { create(:approval_project_rule, :vulnerability_report, project: project) }
it { is_expected.to eql(rule) }
end
context 'without vulnerability report rule' do
let!(:rule) { create(:approval_project_rule, project: project) }
it { is_expected.to be_nil }
end
context 'with multiple rules' do
let!(:rule) { create(:approval_project_rule, project: project) }
let!(:vuln_rule) { create(:approval_project_rule, :vulnerability_report, project: project) }
it { is_expected.to eql(vuln_rule) }
end
end
end
......@@ -20,9 +20,10 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do
context 'with security rules' do
let(:report_approver_rule) { create(:report_approver_rule, merge_request: merge_request, approvals_required: 2) }
let(:scanners) { %w[dependency_scanning] }
let(:vulnerabilities_allowed) { 0 }
before do
create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners)
create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed)
end
context 'when there are security reports' do
......@@ -58,6 +59,15 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
context 'with the minimum number of vulnerabilities allowed greater than the amount from the security reports' do
let(:vulnerabilities_allowed) { 10000 }
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
end
context 'when only low-severity vulnerabilities are present' do
......
......@@ -8,14 +8,27 @@ RSpec.describe IncidentManagement::PendingEscalations::AlertCheckWorker do
let_it_be(:escalation) { create(:incident_management_pending_alert_escalation) }
describe '#perform' do
subject { worker.perform(escalation.id) }
subject { worker.perform(*args) }
it 'processes the escalation' do
process_service = spy(IncidentManagement::PendingEscalations::ProcessService)
context 'with valid escalation' do
let(:args) { [escalation.id.to_s] }
expect(IncidentManagement::PendingEscalations::ProcessService).to receive(:new).with(escalation).and_return(process_service)
subject
expect(process_service).to have_received(:execute)
it 'processes the escalation' do
expect_next_instance_of(IncidentManagement::PendingEscalations::ProcessService, escalation) do |service|
expect(service).to receive(:execute)
end
subject
end
end
context 'without valid escalation' do
let(:args) { [non_existing_record_id] }
it 'does nothing' do
expect(IncidentManagement::PendingEscalations::CreateService).not_to receive(:new)
expect { subject }.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::AlertCreateWorker do
let(:worker) { described_class.new }
let_it_be(:alert) { create(:alert_management_alert) }
describe '#perform' do
subject { worker.perform(*args) }
context 'with valid alert' do
let(:args) { [alert.id.to_s] }
it 'processes the escalation' do
expect_next_instance_of(IncidentManagement::PendingEscalations::CreateService, alert) do |service|
expect(service).to receive(:execute)
end
subject
end
end
context 'without valid alert' do
let(:args) { [non_existing_record_id] }
it 'does nothing' do
expect(IncidentManagement::PendingEscalations::CreateService).not_to receive(:new)
expect { subject }.not_to raise_error
end
end
end
end
......@@ -12626,9 +12626,6 @@ msgstr ""
msgid "Environments|Re-deploy"
msgstr ""
msgid "Environments|Re-deploy environment %{environment_name}?"
msgstr ""
msgid "Environments|Re-deploy environment %{name}?"
msgstr ""
......@@ -12641,9 +12638,6 @@ msgstr ""
msgid "Environments|Rollback environment"
msgstr ""
msgid "Environments|Rollback environment %{environment_name}?"
msgstr ""
msgid "Environments|Rollback environment %{name}?"
msgstr ""
......@@ -12665,15 +12659,9 @@ msgstr ""
msgid "Environments|There was an error fetching the logs. Please try again."
msgstr ""
msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?"
msgstr ""
msgid "Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?"
msgstr ""
msgid "Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
......
......@@ -6,65 +6,83 @@ import eventHub from '~/environments/event_hub';
describe('Confirm Rollback Modal Component', () => {
let environment;
beforeEach(() => {
environment = {
name: 'test',
last_deployment: {
commit: {
short_id: 'abc0123',
},
const envWithLastDeployment = {
name: 'test',
last_deployment: {
commit: {
short_id: 'abc0123',
},
modalId: 'test',
};
});
},
modalId: 'test',
};
it('should show "Rollback" when isLastDeployment is false', () => {
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment: {
...environment,
isLastDeployment: false,
},
},
});
const modal = component.find(GlModal);
const envWithoutLastDeployment = {
name: 'test',
modalId: 'test',
commitShortSha: 'abc0123',
commitUrl: 'test/-/commit/abc0123',
};
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
expect(modal.attributes('ok-title')).toBe('Rollback');
expect(modal.text()).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
});
describe.each`
hasMultipleCommits | environmentData
${true} | ${envWithLastDeployment}
${false} | ${envWithoutLastDeployment}
`('when hasMultipleCommits=$hasMultipleCommits', ({ hasMultipleCommits, environmentData }) => {
beforeEach(() => {
environment = environmentData;
});
it('should show "Re-deploy" when isLastDeployment is true', () => {
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment: {
...environment,
isLastDeployment: true,
it('should show "Rollback" when isLastDeployment is false', () => {
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment: {
...environment,
isLastDeployment: false,
},
hasMultipleCommits,
},
},
});
const modal = component.find(GlModal);
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
expect(modal.attributes('ok-title')).toBe('Rollback');
expect(modal.text()).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
});
const modal = component.find(GlModal);
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
expect(modal.attributes('ok-title')).toBe('Re-deploy');
expect(modal.text()).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
});
it('should show "Re-deploy" when isLastDeployment is true', () => {
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment: {
...environment,
isLastDeployment: true,
},
hasMultipleCommits,
},
});
const modal = component.find(GlModal);
it('should emit the "rollback" event when "ok" is clicked', () => {
environment = { ...environment, isLastDeployment: true };
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment,
},
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
expect(modal.attributes('ok-title')).toBe('Re-deploy');
expect(modal.text()).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
});
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const modal = component.find(GlModal);
modal.vm.$emit('ok');
expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', environment);
it('should emit the "rollback" event when "ok" is clicked', () => {
const env = { ...environmentData, isLastDeployment: true };
const component = shallowMount(ConfirmRollbackModal, {
propsData: {
environment: env,
hasMultipleCommits,
},
});
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const modal = component.find(GlModal);
modal.vm.$emit('ok');
expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', env);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/deployments/_confirm_rollback_modal' do
let(:environment) { create(:environment, :with_review_app) }
let(:deployments) { environment.deployments }
let(:project) { environment.project }
before do
assign(:environment, environment)
assign(:deployments, deployments)
assign(:project, project)
end
context 'when re-deploying last deployment' do
let(:deployment) { deployments.first }
before do
allow(view).to receive(:deployment).and_return(deployment)
end
it 'shows "re-deploy"' do
render
expect(rendered).to have_selector('h4', text: "Re-deploy environment #{environment.name}?")
expect(rendered).to have_selector('p', text: "This action will relaunch the job for commit #{deployment.short_sha}, putting the environment in a previous version. Are you sure you want to continue?")
expect(rendered).to have_selector('a.btn-danger', text: 'Re-deploy')
end
it 'links to re-deploying the environment' do
expected_link = retry_project_job_path(environment.project, deployment.deployable)
render
expect(rendered).to have_selector("a[href='#{expected_link}']", text: 'Re-deploy')
end
end
context 'when rolling back to previous deployment' do
let(:deployment) { create(:deployment, environment: environment) }
before do
allow(view).to receive(:deployment).and_return(deployment)
end
it 'shows "rollback"' do
render
expect(rendered).to have_selector('h4', text: "Rollback environment #{environment.name}?")
expect(rendered).to have_selector('p', text: "This action will run the job defined by #{environment.name} for commit #{deployment.short_sha}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?")
expect(rendered).to have_selector('a.btn-danger', text: 'Rollback')
end
it 'links to re-deploying the environment' do
expected_link = retry_project_job_path(environment.project, deployment.deployable)
render
expect(rendered).to have_selector("a[href='#{expected_link}']", text: 'Rollback')
end
end
end
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