Commit 44d251ed authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '39923-automatically-disable-auto-devops-for-project' into 'master'

Automatically disable Auto DevOps for project upon first pipeline failure

Closes #39923

See merge request gitlab-org/gitlab-ce!21172
parents a286e20d 177d847c
# frozen_string_literal: true
module Emails
module AutoDevops
def autodevops_disabled_email(pipeline, recipient)
@pipeline = pipeline
@project = pipeline.project
add_project_headers
mail(to: recipient,
subject: auto_devops_disabled_subject(@project.name)) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
private
def auto_devops_disabled_subject(project_name)
subject("Auto DevOps pipeline was disabled for #{project_name}")
end
end
end
......@@ -12,6 +12,7 @@ class Notify < BaseMailer
include Emails::Profile
include Emails::Pipelines
include Emails::Members
include Emails::AutoDevops
helper MergeRequestsHelper
helper DiffHelper
......
......@@ -125,6 +125,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message
end
private
def project
......
......@@ -161,6 +161,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
after_transition any => [:failed] do |pipeline|
next unless pipeline.auto_devops_source?
pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
end
end
scope :internal, -> { where(source: internal_sources) }
......
......@@ -407,6 +407,12 @@ class NotificationService
end
end
def autodevops_disabled(pipeline, recipients)
recipients.each do |recipient|
mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
end
end
def pages_domain_verification_succeeded(domain)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
......
# frozen_string_literal: true
module Projects
module AutoDevops
class DisableService < BaseService
def execute
return false unless implicitly_enabled_and_first_pipeline_failure?
disable_auto_devops
end
private
def implicitly_enabled_and_first_pipeline_failure?
project.has_auto_devops_implicitly_enabled? &&
first_pipeline_failure?
end
# We're using `limit` to optimize `auto_devops pipeline` query,
# since we only care about the first element, and using only `.count`
# is an expensive operation. See
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378
# for more context.
def first_pipeline_failure?
auto_devops_pipelines.success.limit(1).count.zero? &&
auto_devops_pipelines.failed.limit(1).count.nonzero?
end
def disable_auto_devops
project.auto_devops_attributes = { enabled: false }
project.save!
end
def auto_devops_pipelines
@auto_devops_pipelines ||= project.pipelines.auto_devops_source
end
end
end
end
%tr
%td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" }
had
= failed.size
failed
#{'build'.pluralize(failed.size)}.
%tr.table-warning
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" }
%tbody
%tr
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" }
%img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" }
= build.stage
%td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
%tr.build-log
- if build.has_trace?
%td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
%pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
= build.trace.html(last_lines: 10).html_safe
- else
%td{ colspan: "2" }
%tr.alert
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" }
Auto DevOps pipeline was disabled for #{@project.name}
%tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" }
The Auto DevOps pipeline failed for pipeline
%a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" }
= "\##{@pipeline.iid}"
and has been disabled for
%a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" }
= @project.name + "."
In order to use the Auto DevOps pipeline with your project, please review the
%a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages,
adjust your project accordingly, and turn on the Auto DevOps pipeline within your
%a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" }
CI/CD project settings.
%tr.pre-section
%td{ style: 'text-align: center;border-bottom:1px solid #ededed' }
%a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' }
Learn more about Auto DevOps
%tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" }
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" }
= "\##{@pipeline.id}"
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" }
= @pipeline.user.name
- else
%td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
API
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
<% failed = @pipeline.statuses.latest.failed -%>
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
<% if build.has_trace? -%>
Trace: <%= build.trace.raw(last_lines: 10) %>
<% end -%>
<% end -%>
......@@ -107,36 +107,5 @@
- else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
- failed = @pipeline.statuses.latest.failed
%tr
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
had
= failed.size
failed
#{'build'.pluralize(failed.size)}.
%tr.table-warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
%img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage
%td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
%tr.build-log
- if build.has_trace?
%td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
%pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
= build.trace.html(last_lines: 10).html_safe
- else
%td{ colspan: "2" }
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
---
- auto_devops:auto_devops_disable
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
......
# frozen_string_literal: true
module AutoDevops
class DisableWorker
include ApplicationWorker
include AutoDevopsQueue
def perform(pipeline_id)
pipeline = Ci::Pipeline.find(pipeline_id)
project = pipeline.project
send_notification_email(pipeline, project) if disable_service(project).execute
end
private
def disable_service(project)
Projects::AutoDevops::DisableService.new(project)
end
def send_notification_email(pipeline, project)
recipients = email_receivers_for(pipeline, project)
return unless recipients.any?
NotificationService.new.autodevops_disabled(pipeline, recipients)
end
def email_receivers_for(pipeline, project)
recipients = [pipeline.user&.email]
recipients << project.owner.email unless project.group
recipients.uniq.compact
end
end
end
# frozen_string_literal: true
#
module AutoDevopsQueue
extend ActiveSupport::Concern
included do
queue_namespace :auto_devops
end
end
---
title: Disable Auto DevOps for project upon first pipeline failure
merge_request: 21172
author:
type: added
......@@ -78,3 +78,4 @@
- [create_note_diff_file, 1]
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
- [auto_devops, 2]
# frozen_string_literal: true
class AddProjectConfigSourceStatusIndexToPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, [:project_id, :status, :config_source]
end
def down
remove_concurrent_index :ci_pipelines, [:project_id, :status, :config_source]
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180826111825) do
ActiveRecord::Schema.define(version: 20180901171833) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -476,6 +476,7 @@ ActiveRecord::Schema.define(version: 20180826111825) do
add_index "ci_pipelines", ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
add_index "ci_pipelines", ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source", using: :btree
add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
......
......@@ -77,6 +77,14 @@ FactoryBot.define do
pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project)
end
end
trait :auto_devops_source do
config_source { Ci::Pipeline.config_sources[:auto_devops_source] }
end
trait :repository_source do
config_source { Ci::Pipeline.config_sources[:repository_source] }
end
end
end
end
......@@ -260,6 +260,10 @@ FactoryBot.define do
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
end
end
# Project with empty repository
......
# frozen_string_literal: true
require 'spec_helper'
describe Emails::AutoDevops do
include EmailSpec::Matchers
describe '#auto_devops_disabled_email' do
let(:owner) { create(:user) }
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:project, :repository, :auto_devops) }
let(:pipeline) { create(:ci_pipeline, :failed, project: project) }
subject { Notify.autodevops_disabled_email(pipeline, owner.email) }
it 'sents email with correct subject' do
is_expected.to have_subject("#{project.name} | Auto DevOps pipeline was disabled for #{project.name}")
end
it 'sents an email to the user' do
recipient = subject.header[:to].addrs.map(&:address).first
expect(recipient).to eq(owner.email)
end
it 'is sent as GitLab email' do
sender = subject.header[:from].addrs[0].address
expect(sender).to match(/gitlab/)
end
end
end
......@@ -1941,4 +1941,28 @@ describe Ci::Pipeline, :mailer do
expect(pipeline.total_size).to eq(5)
end
end
describe '#status' do
context 'when transitioning to failed' do
context 'when pipeline has autodevops as source' do
let(:pipeline) { create(:ci_pipeline, :running, :auto_devops_source) }
it 'calls autodevops disable service' do
expect(AutoDevops::DisableWorker).to receive(:perform_async).with(pipeline.id)
pipeline.drop
end
end
context 'when pipeline has other source' do
let(:pipeline) { create(:ci_pipeline, :running, :repository_source) }
it 'does not call auto devops disable service' do
expect(AutoDevops::DisableWorker).not_to receive(:perform_async)
pipeline.drop
end
end
end
end
end
......@@ -1969,6 +1969,23 @@ describe NotificationService, :mailer do
end
end
context 'Auto DevOps notifications' do
describe '#autodevops_disabled' do
let(:owner) { create(:user) }
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) }
let(:pipeline_user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, :failed, project: project, user: pipeline_user) }
it 'emails project owner and user that triggered the pipeline' do
notification.autodevops_disabled(pipeline, [owner.email, pipeline_user.email])
should_email(owner)
should_email(pipeline_user)
end
end
end
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::AutoDevops::DisableService, '#execute' do
let(:project) { create(:project, :repository, :auto_devops) }
let(:auto_devops) { project.auto_devops }
subject { described_class.new(project).execute }
context 'when Auto DevOps disabled at instance level' do
before do
stub_application_setting(auto_devops_enabled: false)
end
it { is_expected.to be_falsy }
end
context 'when Auto DevOps enabled at instance level' do
before do
stub_application_setting(auto_devops_enabled: true)
end
context 'when Auto DevOps explicitly enabled on project' do
before do
auto_devops.update!(enabled: true)
end
it { is_expected.to be_falsy }
end
context 'when Auto DevOps explicitly disabled on project' do
before do
auto_devops.update!(enabled: false)
end
it { is_expected.to be_falsy }
end
context 'when Auto DevOps is implicitly enabled' do
before do
auto_devops.update!(enabled: nil)
end
context 'when is the first pipeline failure' do
before do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
it 'should disable Auto DevOps for project' do
subject
expect(auto_devops.enabled).to eq(false)
end
end
context 'when it is not the first pipeline failure' do
before do
create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project)
end
it 'should explicitly disable Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to eq(false)
end
end
context 'when an Auto DevOps pipeline has succeeded before' do
before do
create(:ci_pipeline, :success, :auto_devops_source, project: project)
end
it 'should not disable Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to be_nil
end
end
end
context 'when project does not have an Auto DevOps record related' do
let(:project) { create(:project, :repository) }
before do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
it 'should disable Auto DevOps for project' do
subject
auto_devops = project.reload.auto_devops
expect(auto_devops.enabled).to eq(false)
end
it 'should create a ProjectAutoDevops record' do
expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AutoDevops::DisableWorker, '#perform' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, :auto_devops) }
let(:auto_devops) { project.auto_devops }
let(:pipeline) { create(:ci_pipeline, :failed, :auto_devops_source, project: project, user: user) }
subject { described_class.new }
before do
stub_application_setting(auto_devops_enabled: true)
auto_devops.update_attribute(:enabled, nil)
end
it 'disables auto devops for project' do
subject.perform(pipeline.id)
expect(auto_devops.reload.enabled).to eq(false)
end
context 'when project owner is a user' do
let(:owner) { create(:user) }
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) }
it 'sends an email to pipeline user and project owner' do
expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email, owner.email])
subject.perform(pipeline.id)
end
end
context 'when project does not have owner' do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :auto_devops, namespace: group) }
it 'sends an email to pipeline user' do
expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email])
subject.perform(pipeline.id)
end
end
context 'when pipeline is not related to a user and project does not have owner' do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :auto_devops, namespace: group) }
let(:pipeline) { create(:ci_pipeline, :failed, project: project) }
it 'does not send an email' do
expect(NotificationService).not_to receive(:new)
subject.perform(pipeline.id)
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