Commit ad1be639 authored by Douwe Maan's avatar Douwe Maan

Merge branch '3730-allow-admins-to-disable-mirroring' into 'master'

Allows admins to disable mirroring

Closes #3730

See merge request gitlab-org/gitlab-ee!3586
parents 4c25ecfd ececcc32
...@@ -128,7 +128,6 @@ ...@@ -128,7 +128,6 @@
- if License.feature_available?(:repository_mirrors) - if License.feature_available?(:repository_mirrors)
= render partial: 'repository_mirrors_form', locals: { f: f } = render partial: 'repository_mirrors_form', locals: { f: f }
= render partial: 'repository_remote_mirrors_form', locals: { f: f }
%fieldset %fieldset
%legend Sign-up Restrictions %legend Sign-up Restrictions
......
---
title: Allow admins to disable mirroring
merge_request: 3586
author:
type: added
class RenameRemoteMirrorAvailableToMirrorAvailable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
rename_column_concurrently :application_settings, :remote_mirror_available, :mirror_available
end
def down
cleanup_concurrent_column_rename :application_settings, :mirror_available, :remote_mirror_available
end
end
class AddPullMirrorAvailableOverriddenToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_column :projects, :pull_mirror_available_overridden, :boolean
end
end
class CleanupRemoteMirrorAvailableRename < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings, :remote_mirror_available, :mirror_available
end
def down
rename_column_concurrently :application_settings, :mirror_available, :remote_mirror_available
end
end
...@@ -162,7 +162,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do ...@@ -162,7 +162,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.integer "circuitbreaker_failure_wait_time", default: 30 t.integer "circuitbreaker_failure_wait_time", default: 30
t.integer "circuitbreaker_failure_reset_time", default: 1800 t.integer "circuitbreaker_failure_reset_time", default: 1800
t.integer "circuitbreaker_storage_timeout", default: 30 t.integer "circuitbreaker_storage_timeout", default: 30
t.boolean "remote_mirror_available", default: true, null: false
t.integer "circuitbreaker_access_retries", default: 3 t.integer "circuitbreaker_access_retries", default: 3
t.integer "circuitbreaker_backoff_threshold", default: 80 t.integer "circuitbreaker_backoff_threshold", default: 80
t.boolean "throttle_unauthenticated_enabled", default: false, null: false t.boolean "throttle_unauthenticated_enabled", default: false, null: false
...@@ -179,6 +178,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do ...@@ -179,6 +178,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_default", default: 55, null: false
t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_medium", default: 30, null: false
t.integer "gitaly_timeout_fast", default: 10, null: false t.integer "gitaly_timeout_fast", default: 10, null: false
t.boolean "mirror_available", default: true, null: false
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -1856,6 +1856,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do ...@@ -1856,6 +1856,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.boolean "resolve_outdated_diff_discussions" t.boolean "resolve_outdated_diff_discussions"
t.boolean "remote_mirror_available_overridden" t.boolean "remote_mirror_available_overridden"
t.boolean "only_mirror_protected_branches" t.boolean "only_mirror_protected_branches"
t.boolean "pull_mirror_available_overridden"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -37,14 +37,14 @@ not selected. ...@@ -37,14 +37,14 @@ not selected.
HTTP, will still be accessible. What GitLab does is restrict access on the HTTP, will still be accessible. What GitLab does is restrict access on the
application level. application level.
## Allow remote mirrors to be setup for projects ## Allow mirrors to be setup for projects
> [Introduced][ee-3130] in Gitlab 10.2. > [Introduced][ee-3586] in Gitlab 10.3.
This option is enabled by default. By disabling it, push mirroring will no longer This option is enabled by default. By disabling it, both pull and push mirroring will no longer
work in every repository and can only be re-enabled on a per-project basis by an admin. work in every repository and can only be re-enabled on a per-project basis by an admin.
![Remote mirror settings](img/remote_mirror_settings.png) ![Mirror settings](img/mirror_settings.png)
[ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696 [ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696
[ee-3130]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3130 [ee-3586]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3586
...@@ -2,7 +2,7 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -2,7 +2,7 @@ class Projects::MirrorsController < Projects::ApplicationController
include RepositorySettingsRedirect include RepositorySettingsRedirect
include SafeMirrorParams include SafeMirrorParams
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_mirror!
before_action :remote_mirror, only: [:update] before_action :remote_mirror, only: [:update]
before_action :check_repository_mirrors_available! before_action :check_repository_mirrors_available!
...@@ -73,12 +73,8 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -73,12 +73,8 @@ class Projects::MirrorsController < Projects::ApplicationController
@remote_mirror = @project.remote_mirrors.first_or_initialize @remote_mirror = @project.remote_mirrors.first_or_initialize
end end
def remote_mirror_attributes
{ remote_mirrors_attributes: %i[url id enabled only_protected_branches] }
end
def mirror_params_attributes def mirror_params_attributes
attributes = [ [
:mirror, :mirror,
:import_url, :import_url,
:username_only_import_url, :username_only_import_url,
...@@ -92,14 +88,15 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -92,14 +88,15 @@ class Projects::MirrorsController < Projects::ApplicationController
password password
ssh_known_hosts ssh_known_hosts
regenerate_ssh_private_key regenerate_ssh_private_key
],
remote_mirrors_attributes: %i[
url
id
enabled
only_protected_branches
] ]
] ]
if can?(current_user, :admin_remote_mirror, project)
attributes << remote_mirror_attributes
end
attributes
end end
def mirror_params def mirror_params
......
...@@ -23,7 +23,7 @@ module EE ...@@ -23,7 +23,7 @@ module EE
:slack_app_secret, :slack_app_secret,
:slack_app_verification_token, :slack_app_verification_token,
:allow_group_owners_to_manage_ldap, :allow_group_owners_to_manage_ldap,
:remote_mirror_available :mirror_available
] ]
end end
......
...@@ -41,7 +41,7 @@ module EE ...@@ -41,7 +41,7 @@ module EE
mirror_max_capacity: Settings.gitlab['mirror_max_capacity'], mirror_max_capacity: Settings.gitlab['mirror_max_capacity'],
mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold'], mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold'],
allow_group_owners_to_manage_ldap: true, allow_group_owners_to_manage_ldap: true,
remote_mirror_available: true mirror_available: true
) )
end end
end end
......
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
before_validation :mark_remote_mirrors_for_removal before_validation :mark_remote_mirrors_for_removal
before_save :set_override_pull_mirror_available, unless: -> { ::Gitlab::CurrentSettings.current_application_settings.mirror_available }
after_save :create_mirror_data, if: ->(project) { project.mirror? && project.mirror_changed? } after_save :create_mirror_data, if: ->(project) { project.mirror? && project.mirror_changed? }
after_save :destroy_mirror_data, if: ->(project) { !project.mirror? && project.mirror_changed? } after_save :destroy_mirror_data, if: ->(project) { !project.mirror? && project.mirror_changed? }
...@@ -91,7 +92,7 @@ module EE ...@@ -91,7 +92,7 @@ module EE
end end
def mirror def mirror
super && feature_available?(:repository_mirrors) super && feature_available?(:repository_mirrors) && pull_mirror_available?
end end
alias_method :mirror?, :mirror alias_method :mirror?, :mirror
...@@ -500,11 +501,21 @@ module EE ...@@ -500,11 +501,21 @@ module EE
def remote_mirror_available? def remote_mirror_available?
remote_mirror_available_overridden || remote_mirror_available_overridden ||
current_application_settings.remote_mirror_available current_application_settings.mirror_available
end
def pull_mirror_available?
pull_mirror_available_overridden ||
current_application_settings.mirror_available
end end
private private
def set_override_pull_mirror_available
self.pull_mirror_available_overridden = read_attribute(:mirror)
true
end
def licensed_feature_available?(feature) def licensed_feature_available?(feature)
@licensed_feature_available ||= Hash.new do |h, feature| @licensed_feature_available ||= Hash.new do |h, feature|
h[feature] = load_licensed_feature_available(feature) h[feature] = load_licensed_feature_available(feature)
......
...@@ -21,7 +21,7 @@ class RemoteMirror < ActiveRecord::Base ...@@ -21,7 +21,7 @@ class RemoteMirror < ActiveRecord::Base
validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? } validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? }
validates :url, addressable_url: true, if: :url_changed? validates :url, addressable_url: true, if: :url_changed?
after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.remote_mirror_available } after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
after_save :refresh_remote, if: :mirror_url_changed? after_save :refresh_remote, if: :mirror_url_changed?
after_update :reset_fields, if: :mirror_url_changed? after_update :reset_fields, if: :mirror_url_changed?
after_destroy :remove_remote after_destroy :remove_remote
......
...@@ -9,6 +9,9 @@ module EE ...@@ -9,6 +9,9 @@ module EE
with_scope :subject with_scope :subject
condition(:related_issues_disabled) { !@subject.feature_available?(:related_issues) } condition(:related_issues_disabled) { !@subject.feature_available?(:related_issues) }
with_scope :subject
condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) }
with_scope :subject with_scope :subject
condition(:deploy_board_disabled) { !@subject.feature_available?(:deploy_board) } condition(:deploy_board_disabled) { !@subject.feature_available?(:deploy_board) }
...@@ -26,8 +29,8 @@ module EE ...@@ -26,8 +29,8 @@ module EE
end end
with_scope :global with_scope :global
condition(:remote_mirror_available) do condition(:mirror_available, score: 0) do
::Gitlab::CurrentSettings.current_application_settings.remote_mirror_available ::Gitlab::CurrentSettings.current_application_settings.mirror_available
end end
rule { admin }.enable :change_repository_storage rule { admin }.enable :change_repository_storage
...@@ -60,7 +63,7 @@ module EE ...@@ -60,7 +63,7 @@ module EE
rule { can?(:developer_access) }.enable :admin_board rule { can?(:developer_access) }.enable :admin_board
rule { (remote_mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror rule { repository_mirrors_enabled & ((mirror_available & can?(:admin_project)) | admin) }.enable :admin_mirror
rule { deploy_board_disabled & ~is_development }.prevent :read_deploy_board rule { deploy_board_disabled & ~is_development }.prevent :read_deploy_board
......
- if Gitlab.com? && License.feature_available?(:repository_mirrors) - if License.feature_available?(:repository_mirrors)
%fieldset %fieldset
%legend Repository mirror settings %legend Repository mirror settings
.form-group
= f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :mirror_available do
= f.check_box :mirror_available
Allow mirrors to be setup for projects
%span.help-block
If disabled, only admins will be able to setup mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
- if Gitlab.com?
.form-group .form-group
= f.label :mirror_max_delay, class: 'control-label col-sm-2' do = f.label :mirror_max_delay, class: 'control-label col-sm-2' do
Maximum delay (Minutes) Maximum delay (Minutes)
......
%fieldset
%legend Repository Remote mirror settings
.form-group
= f.label :remote_mirror_available, 'Enable remote mirror configuration', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :remote_mirror_available do
= f.check_box :remote_mirror_available
Allow remote mirrors to be setup for projects
%span.help-block
If disabled, only admins will be able to setup remote mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository')
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'mirrors' = webpack_bundle_tag 'mirrors'
- if @project.feature_available?(:repository_mirrors) - if can?(current_user, :admin_mirror, @project)
= render 'projects/mirrors/pull' = render 'projects/mirrors/pull'
- if can?(current_user, :admin_remote_mirror, @project) = render 'projects/mirrors/push'
= render 'projects/mirrors/push'
...@@ -31,7 +31,7 @@ class UpdateAllMirrorsWorker ...@@ -31,7 +31,7 @@ class UpdateAllMirrorsWorker
last = projects.last.mirror_data.next_execution_timestamp last = projects.last.mirror_data.next_execution_timestamp
projects.each do |project| projects.each do |project|
next unless project.feature_available?(:repository_mirrors) next unless project.mirror?
capacity -= 1 capacity -= 1
project.import_schedule project.import_schedule
......
...@@ -108,6 +108,7 @@ excluded_attributes: ...@@ -108,6 +108,7 @@ excluded_attributes:
- :storage_version - :storage_version
- :remote_mirror_available_overridden - :remote_mirror_available_overridden
- :only_mirror_protected_branches - :only_mirror_protected_branches
- :pull_mirror_available_overridden
snippets: snippets:
- :expired_at - :expired_at
merge_request_diff: merge_request_diff:
......
...@@ -9,7 +9,7 @@ describe Projects::MirrorsController do ...@@ -9,7 +9,7 @@ describe Projects::MirrorsController do
context 'when remote mirrors are disabled' do context 'when remote mirrors are disabled' do
before do before do
stub_application_setting(remote_mirror_available: false) stub_application_setting(mirror_available: false)
end end
context 'when user is admin' do context 'when user is admin' do
...@@ -46,7 +46,7 @@ describe Projects::MirrorsController do ...@@ -46,7 +46,7 @@ describe Projects::MirrorsController do
expect_any_instance_of(EE::Project).to receive(:force_import_job!) expect_any_instance_of(EE::Project).to receive(:force_import_job!)
expect do expect do
do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } }) do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => url } })
end.to change { RemoteMirror.count }.to(1) end.to change { RemoteMirror.count }.to(1)
end end
...@@ -121,27 +121,59 @@ describe Projects::MirrorsController do ...@@ -121,27 +121,59 @@ describe Projects::MirrorsController do
end end
describe 'setting up a mirror' do describe 'setting up a mirror' do
before do let(:url) { 'http://foo.com' }
sign_in(project.owner) let(:project) { create(:project, :repository) }
end
context 'when project does not have a mirror' do context 'when mirrors are disabled' do
let(:project) { create(:project) } before do
stub_application_setting(mirror_available: false)
end
it 'allows to create a mirror' do context 'when user is admin' do
expect_any_instance_of(EE::Project).to receive(:force_import_job!) let(:admin) { create(:user, :admin) }
expect do it 'creates a new mirror' do
do_put(project, mirror: true, mirror_user_id: project.owner.id, import_url: 'http://foo.com') sign_in(admin)
end.to change { Project.mirror.count }.to(1) expect_any_instance_of(EE::Project).to receive(:force_import_job!)
expect do
do_put(project, mirror: true, mirror_user_id: admin.id, import_url: url)
end.to change { Project.mirror.count }.to(1)
end
end
context 'when user is not an admin' do
it 'does not create a new mirror' do
sign_in(project.owner)
expect do
do_put(project, mirror: true, mirror_user_id: project.owner.id, import_url: url)
end.not_to change { Project.mirror.count }
end
end end
end end
context 'when project has a mirror' do context 'when mirrors are enabled' do
let(:project) { create(:project, :mirror, :import_finished) } before do
sign_in(project.owner)
end
context 'when project does not have a mirror' do
it 'allows to create a mirror' do
expect_any_instance_of(EE::Project).to receive(:force_import_job!)
it 'is able to disable the mirror' do expect do
expect { do_put(project, mirror: false) }.to change { Project.mirror.count }.to(0) do_put(project, mirror: true, mirror_user_id: project.owner.id, import_url: url)
end.to change { Project.mirror.count }.to(1)
end
end
context 'when project has a mirror' do
let(:project) { create(:project, :mirror, :import_finished) }
it 'is able to disable the mirror' do
expect { do_put(project, mirror: false) }.to change { Project.mirror.count }.to(0)
end
end end
end end
end end
...@@ -157,25 +189,6 @@ describe Projects::MirrorsController do ...@@ -157,25 +189,6 @@ describe Projects::MirrorsController do
end end
end end
describe 'forcing an update on a push mirror' do
context 'when remote mirrors are disabled' do
let(:project) { create(:project, :repository, :remote_mirror) }
before do
stub_application_setting(remote_mirror_available: false)
sign_in(project.owner)
end
it 'updates now when overridden' do
project.update(remote_mirror_available_overridden: true)
expect_any_instance_of(EE::Project).to receive(:update_remote_mirrors)
put :update_now, { namespace_id: project.namespace.to_param, project_id: project.to_param, sync_remote: 1 }
end
end
end
describe '#update' do describe '#update' do
let(:project) { create(:project, :repository, :mirror, :remote_mirror) } let(:project) { create(:project, :repository, :mirror, :remote_mirror) }
let(:attributes) { { project: { mirror_user_id: project.owner.id, mirror_trigger_builds: 0 }, namespace_id: project.namespace.to_param, project_id: project.to_param } } let(:attributes) { { project: { mirror_user_id: project.owner.id, mirror_trigger_builds: 0 }, namespace_id: project.namespace.to_param, project_id: project.to_param } }
......
...@@ -451,7 +451,7 @@ describe Project do ...@@ -451,7 +451,7 @@ describe Project do
end end
it 'does nothing when remote mirror is disabled globally and not overridden' do it 'does nothing when remote mirror is disabled globally and not overridden' do
stub_application_setting(remote_mirror_available: false) stub_application_setting(mirror_available: false)
project.remote_mirror_available_overridden = false project.remote_mirror_available_overridden = false
expect_any_instance_of(RemoteMirror).not_to receive(:sync) expect_any_instance_of(RemoteMirror).not_to receive(:sync)
...@@ -1051,7 +1051,7 @@ describe Project do ...@@ -1051,7 +1051,7 @@ describe Project do
context 'when remote mirror global setting is disabled' do context 'when remote mirror global setting is disabled' do
before do before do
stub_application_setting(remote_mirror_available: false) stub_application_setting(mirror_available: false)
end end
it 'returns true when overridden' do it 'returns true when overridden' do
...@@ -1066,6 +1066,32 @@ describe Project do ...@@ -1066,6 +1066,32 @@ describe Project do
end end
end end
describe '#pull_mirror_available?' do
let(:project) { create(:project) }
context 'when mirror global setting is enabled' do
it 'returns true' do
expect(project.pull_mirror_available?).to be(true)
end
end
context 'when mirror global setting is disabled' do
before do
stub_application_setting(mirror_available: false)
end
it 'returns true when overridden' do
project.pull_mirror_available_overridden = true
expect(project.pull_mirror_available?).to be(true)
end
it 'returns false when not overridden' do
expect(project.pull_mirror_available?).to be(false)
end
end
end
describe '#username_only_import_url' do describe '#username_only_import_url' do
where(:import_url, :username, :expected_import_url) do where(:import_url, :username, :expected_import_url) do
'' | 'foo' | '' '' | 'foo' | ''
......
...@@ -10,7 +10,7 @@ describe ProjectPolicy do ...@@ -10,7 +10,7 @@ describe ProjectPolicy do
project.add_developer(developer) project.add_developer(developer)
end end
context 'admin_remote_mirror' do context 'admin_mirror' do
context 'with remote mirror setting enabled' do context 'with remote mirror setting enabled' do
context 'with admin' do context 'with admin' do
subject do subject do
...@@ -18,7 +18,7 @@ describe ProjectPolicy do ...@@ -18,7 +18,7 @@ describe ProjectPolicy do
end end
it do it do
is_expected.to be_allowed(:admin_remote_mirror) is_expected.to be_allowed(:admin_mirror)
end end
end end
...@@ -28,7 +28,7 @@ describe ProjectPolicy do ...@@ -28,7 +28,7 @@ describe ProjectPolicy do
end end
it do it do
is_expected.to be_allowed(:admin_remote_mirror) is_expected.to be_allowed(:admin_mirror)
end end
end end
...@@ -38,14 +38,14 @@ describe ProjectPolicy do ...@@ -38,14 +38,14 @@ describe ProjectPolicy do
end end
it do it do
is_expected.to be_disallowed(:admin_remote_mirror) is_expected.to be_disallowed(:admin_mirror)
end end
end end
end end
context 'with remote mirror setting disabled' do context 'with remote mirror setting disabled' do
before do before do
stub_application_setting(remote_mirror_available: false) stub_application_setting(mirror_available: false)
end end
context 'with admin' do context 'with admin' do
...@@ -54,7 +54,7 @@ describe ProjectPolicy do ...@@ -54,7 +54,7 @@ describe ProjectPolicy do
end end
it do it do
is_expected.to be_allowed(:admin_remote_mirror) is_expected.to be_allowed(:admin_mirror)
end end
end end
...@@ -64,7 +64,59 @@ describe ProjectPolicy do ...@@ -64,7 +64,59 @@ describe ProjectPolicy do
end end
it do it do
is_expected.to be_disallowed(:admin_remote_mirror) is_expected.to be_disallowed(:admin_mirror)
end
end
end
context 'with remote mirrors feature disabled' do
before do
stub_licensed_features(repository_mirrors: false)
end
context 'with admin' do
subject do
described_class.new(admin, project)
end
it do
is_expected.to be_disallowed(:admin_mirror)
end
end
context 'with owner' do
subject do
described_class.new(owner, project)
end
it do
is_expected.to be_disallowed(:admin_mirror)
end
end
end
context 'with remote mirrors feature enabled' do
before do
stub_licensed_features(repository_mirrors: true)
end
context 'with admin' do
subject do
described_class.new(admin, project)
end
it do
is_expected.to be_allowed(:admin_mirror)
end
end
context 'with owner' do
subject do
described_class.new(owner, project)
end
it do
is_expected.to be_allowed(:admin_mirror)
end end
end end
end end
......
...@@ -38,7 +38,7 @@ describe GitPushService do ...@@ -38,7 +38,7 @@ describe GitPushService do
context 'when remote mirror feature is disabled' do context 'when remote mirror feature is disabled' do
before do before do
stub_application_setting(remote_mirror_available: false) stub_application_setting(mirror_available: false)
end end
context 'with remote mirrors global setting overridden' do context 'with remote mirrors global setting overridden' do
......
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