Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
8ba4e8b3
Commit
8ba4e8b3
authored
Jul 25, 2017
by
🌴 Toon Claes 🌴
Committed by
Douwe Maan
Jul 25, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Namespace license checks for Repository Mirrors
parent
cc5a5196
Changes
28
Hide whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
751 additions
and
174 deletions
+751
-174
app/controllers/admin/application_settings_controller.rb
app/controllers/admin/application_settings_controller.rb
+4
-29
app/controllers/ee/admin/application_settings_controller.rb
app/controllers/ee/admin/application_settings_controller.rb
+45
-0
app/controllers/ee/projects/settings/repository_controller.rb
...controllers/ee/projects/settings/repository_controller.rb
+2
-0
app/controllers/ee/projects_controller.rb
app/controllers/ee/projects_controller.rb
+35
-0
app/controllers/projects/application_controller.rb
app/controllers/projects/application_controller.rb
+1
-0
app/controllers/projects/mirrors_controller.rb
app/controllers/projects/mirrors_controller.rb
+1
-0
app/controllers/projects_controller.rb
app/controllers/projects_controller.rb
+3
-21
app/models/ee/project.rb
app/models/ee/project.rb
+9
-7
app/models/license.rb
app/models/license.rb
+5
-1
app/models/remote_mirror.rb
app/models/remote_mirror.rb
+12
-3
app/services/projects/update_mirror_service.rb
app/services/projects/update_mirror_service.rb
+1
-1
app/services/projects/update_remote_mirror_service.rb
app/services/projects/update_remote_mirror_service.rb
+2
-0
app/views/admin/application_settings/_repository_mirrors_form.html.haml
...n/application_settings/_repository_mirrors_form.html.haml
+1
-1
app/views/projects/mirrors/_pull.html.haml
app/views/projects/mirrors/_pull.html.haml
+46
-0
app/views/projects/mirrors/_push.html.haml
app/views/projects/mirrors/_push.html.haml
+38
-0
app/views/projects/mirrors/_show.html.haml
app/views/projects/mirrors/_show.html.haml
+3
-84
app/workers/update_all_mirrors_worker.rb
app/workers/update_all_mirrors_worker.rb
+40
-1
changelogs/unreleased-ee/tc-namespace-license-checks--repository-mirrors.yml
...ed-ee/tc-namespace-license-checks--repository-mirrors.yml
+4
-0
spec/controllers/ee/admin/application_settings_controller_spec.rb
...trollers/ee/admin/application_settings_controller_spec.rb
+81
-0
spec/controllers/ee/projects_controller_spec.rb
spec/controllers/ee/projects_controller_spec.rb
+164
-0
spec/features/projects/mirror_spec.rb
spec/features/projects/mirror_spec.rb
+12
-0
spec/features/projects/settings/ee/push_rules_settings_spec.rb
...features/projects/settings/ee/push_rules_settings_spec.rb
+0
-22
spec/features/projects/settings/ee/repository_mirrors_settings_spec.rb
.../projects/settings/ee/repository_mirrors_settings_spec.rb
+65
-0
spec/models/ee/project_spec.rb
spec/models/ee/project_spec.rb
+56
-0
spec/models/remote_mirror_spec.rb
spec/models/remote_mirror_spec.rb
+12
-0
spec/services/projects/update_mirror_service_spec.rb
spec/services/projects/update_mirror_service_spec.rb
+17
-2
spec/services/projects/update_remote_mirror_service_spec.rb
spec/services/projects/update_remote_mirror_service_spec.rb
+9
-1
spec/workers/update_all_mirrors_worker_spec.rb
spec/workers/update_all_mirrors_worker_spec.rb
+83
-1
No files found.
app/controllers/admin/application_settings_controller.rb
View file @
8ba4e8b3
class
Admin::ApplicationSettingsController
<
Admin
::
ApplicationController
prepend
EE
::
Admin
::
ApplicationSettingsController
before_action
:set_application_setting
def
show
...
...
@@ -58,7 +60,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def
application_setting_params
import_sources
=
params
[
:application_setting
][
:import_sources
]
if
import_sources
.
nil?
params
[
:application_setting
][
:import_sources
]
=
[]
else
...
...
@@ -77,11 +78,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params
.
delete
(
:domain_blacklist_raw
)
if
params
[
:domain_blacklist_file
]
params
.
require
(
:application_setting
).
permit
(
application_setting_params_
ce
<<
application_setting_params_ee
application_setting_params_
attributes
)
end
def
application_setting_params_
ce
def
application_setting_params_
attributes
[
:admin_notification_email
,
:after_sign_out_path
,
...
...
@@ -166,30 +167,4 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
sidekiq_throttling_queues:
[]
]
end
def
application_setting_params_ee
[
:help_text
,
:elasticsearch_url
,
:elasticsearch_indexing
,
:elasticsearch_aws
,
:elasticsearch_aws_access_key
,
:elasticsearch_aws_secret_access_key
,
:elasticsearch_aws_region
,
:elasticsearch_search
,
:repository_size_limit
,
:shared_runners_minutes
,
:geo_status_timeout
,
:elasticsearch_experimental_indexer
,
:check_namespace_plan
,
:mirror_max_delay
,
:mirror_max_capacity
,
:mirror_capacity_threshold
,
:authorized_keys_enabled
,
:slack_app_enabled
,
:slack_app_id
,
:slack_app_secret
,
:slack_app_verification_token
]
end
end
app/controllers/ee/admin/application_settings_controller.rb
0 → 100644
View file @
8ba4e8b3
module
EE
module
Admin
module
ApplicationSettingsController
def
application_setting_params_attributes
attrs
=
super
+
application_setting_params_attributes_ee
attrs
+=
repository_mirrors_params_attributes
if
License
.
feature_available?
(
:repository_mirrors
)
attrs
end
private
def
application_setting_params_attributes_ee
[
:help_text
,
:elasticsearch_url
,
:elasticsearch_indexing
,
:elasticsearch_aws
,
:elasticsearch_aws_access_key
,
:elasticsearch_aws_secret_access_key
,
:elasticsearch_aws_region
,
:elasticsearch_search
,
:repository_size_limit
,
:shared_runners_minutes
,
:geo_status_timeout
,
:elasticsearch_experimental_indexer
,
:check_namespace_plan
,
:authorized_keys_enabled
,
:slack_app_enabled
,
:slack_app_id
,
:slack_app_secret
,
:slack_app_verification_token
]
end
def
repository_mirrors_params_attributes
[
:mirror_max_delay
,
:mirror_max_capacity
,
:mirror_capacity_threshold
]
end
end
end
end
app/controllers/ee/projects/settings/repository_controller.rb
View file @
8ba4e8b3
...
...
@@ -19,6 +19,8 @@ module EE
end
def
remote_mirror
return
unless
project
.
feature_available?
(
:repository_mirrors
)
@remote_mirror
=
@project
.
remote_mirrors
.
first_or_initialize
end
...
...
app/controllers/ee/projects_controller.rb
0 → 100644
View file @
8ba4e8b3
module
EE
module
ProjectsController
def
project_params_attributes
attrs
=
super
+
project_params_ee
attrs
+=
repository_mirrors_params
if
project
&
.
feature_available?
(
:repository_mirrors
)
attrs
end
private
def
project_params_ee
%i[
approvals_before_merge
approver_group_ids
approver_ids
issues_template
merge_method
merge_requests_template
disable_overriding_approvers_per_merge_request
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
def
repository_mirrors_params
%i[
mirror
mirror_trigger_builds
mirror_user_id
]
end
end
end
app/controllers/projects/application_controller.rb
View file @
8ba4e8b3
...
...
@@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController
def
project
return
@project
if
@project
return
nil
unless
params
[
:project_id
]
||
params
[
:id
]
path
=
File
.
join
(
params
[
:namespace_id
],
params
[
:project_id
]
||
params
[
:id
])
auth_proc
=
->
(
project
)
{
!
project
.
pending_delete?
}
...
...
app/controllers/projects/mirrors_controller.rb
View file @
8ba4e8b3
...
...
@@ -5,6 +5,7 @@ class Projects::MirrorsController < Projects::ApplicationController
before_action
:authorize_admin_project!
,
except:
[
:update_now
]
before_action
:authorize_push_code!
,
only:
[
:update_now
]
before_action
:remote_mirror
,
only:
[
:update
]
before_action
:check_repository_mirrors_available!
layout
"project_settings"
...
...
app/controllers/projects_controller.rb
View file @
8ba4e8b3
class
ProjectsController
<
Projects
::
ApplicationController
include
IssuableCollections
include
ExtractsPath
prepend
EE
::
ProjectsController
before_action
:authenticate_user!
,
except:
[
:index
,
:show
,
:activity
,
:refs
]
before_action
:project
,
except:
[
:index
,
:new
,
:create
]
...
...
@@ -297,10 +298,10 @@ class ProjectsController < Projects::ApplicationController
def
project_params
params
.
require
(
:project
)
.
permit
(
project_params_
ce
<<
project_params_ee
)
.
permit
(
project_params_
attributes
)
end
def
project_params_
ce
def
project_params_
attributes
[
:avatar
,
:build_allow_git_fetch
,
...
...
@@ -337,25 +338,6 @@ class ProjectsController < Projects::ApplicationController
]
end
def
project_params_ee
%i[
approvals_before_merge
approvals
approver_group_ids
approver_ids
issues_template
merge_method
merge_requests_template
mirror
mirror_trigger_builds
mirror_user_id
disable_overriding_approvers_per_merge_request
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
def
repo_exists?
project
.
repository_exists?
&&
!
project
.
empty_repo?
&&
project
.
repo
...
...
app/models/ee/project.rb
View file @
8ba4e8b3
...
...
@@ -42,11 +42,6 @@ module EE
scope
:with_shared_runners_limit_enabled
,
->
{
with_shared_runners
.
non_public_only
}
scope
:mirrors_to_sync
,
->
do
mirror
.
joins
(
:mirror_data
).
where
(
"next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')"
,
Time
.
now
)
.
order_by
(
:next_execution_timestamp
).
limit
(
::
Gitlab
::
Mirror
.
available_capacity
)
end
scope
:stuck_mirrors
,
->
do
mirror
.
joins
(
:mirror_data
)
.
where
(
"(import_status = 'started' AND project_mirror_data.last_update_started_at < :limit) OR (import_status = 'scheduled' AND project_mirror_data.last_update_scheduled_at < :limit)"
,
...
...
@@ -84,6 +79,11 @@ module EE
end
end
def
mirror
super
&&
feature_available?
(
:repository_mirrors
)
end
alias_method
:mirror?
,
:mirror
def
mirror_updated?
mirror?
&&
self
.
mirror_last_update_at
end
...
...
@@ -140,7 +140,7 @@ module EE
end
def
has_remote_mirror?
remote_mirrors
.
enabled
.
exists?
feature_available?
(
:repository_mirrors
)
&&
remote_mirrors
.
enabled
.
exists?
end
def
updating_remote_mirror?
...
...
@@ -148,7 +148,9 @@ module EE
end
def
update_remote_mirrors
remote_mirrors
.
each
(
&
:sync
)
return
unless
feature_available?
(
:repository_mirrors
)
remote_mirrors
.
enabled
.
each
(
&
:sync
)
end
def
mark_stuck_remote_mirrors_as_failed!
...
...
app/models/license.rb
View file @
8ba4e8b3
...
...
@@ -28,6 +28,7 @@ class License < ActiveRecord::Base
PROTECTED_REFS_FOR_USERS_FEATURE
=
'GitLab_RefPermissionsForUsers'
.
freeze
PUSH_RULES_FEATURE
=
'GitLab_PushRules'
.
freeze
RELATED_ISSUES_FEATURE
=
'GitLab_RelatedIssues'
.
freeze
REPOSITORY_MIRRORS_FEATURE
=
'GitLab_RepositoryMirrors'
.
freeze
REPOSITORY_SIZE_LIMIT_FEATURE
=
'GitLab_RepositorySizeLimit'
.
freeze
SERVICE_DESK_FEATURE
=
'GitLab_ServiceDesk'
.
freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE
=
'GitLab_VariableEnvironmentScope'
.
freeze
...
...
@@ -64,7 +65,8 @@ class License < ActiveRecord::Base
multiple_issue_assignees:
MULTIPLE_ISSUE_ASSIGNEES_FEATURE
,
multiple_issue_boards:
MULTIPLE_ISSUE_BOARDS_FEATURE
,
protected_refs_for_users:
PROTECTED_REFS_FOR_USERS_FEATURE
,
push_rules:
PUSH_RULES_FEATURE
push_rules:
PUSH_RULES_FEATURE
,
repository_mirrors:
REPOSITORY_MIRRORS_FEATURE
}.
freeze
STARTER_PLAN
=
'starter'
.
freeze
...
...
@@ -93,6 +95,7 @@ class License < ActiveRecord::Base
{
PUSH_RULES_FEATURE
=>
1
},
{
PROTECTED_REFS_FOR_USERS_FEATURE
=>
1
},
{
RELATED_ISSUES_FEATURE
=>
1
},
{
REPOSITORY_MIRRORS_FEATURE
=>
1
},
{
REPOSITORY_SIZE_LIMIT_FEATURE
=>
1
}
].
freeze
...
...
@@ -143,6 +146,7 @@ class License < ActiveRecord::Base
{
OBJECT_STORAGE_FEATURE
=>
1
},
{
PROTECTED_REFS_FOR_USERS_FEATURE
=>
1
},
{
PUSH_RULES_FEATURE
=>
1
},
{
REPOSITORY_MIRRORS_FEATURE
=>
1
},
{
SERVICE_DESK_FEATURE
=>
1
}
].
freeze
...
...
app/models/remote_mirror.rb
View file @
8ba4e8b3
...
...
@@ -77,13 +77,22 @@ class RemoteMirror < ActiveRecord::Base
end
def
sync
return
unless
project
&&
enabled
return
if
project
.
pending_delete?
return
unless
enabled?
return
if
Gitlab
::
Geo
.
secondary?
RepositoryUpdateRemoteMirrorWorker
.
perform_in
(
BACKOFF_DELAY
,
self
.
id
,
Time
.
now
)
if
project
&
.
repository_exists?
RepositoryUpdateRemoteMirrorWorker
.
perform_in
(
BACKOFF_DELAY
,
self
.
id
,
Time
.
now
)
end
def
enabled
return
false
unless
project
&&
super
return
false
unless
project
.
repository_exists?
return
false
if
project
.
pending_delete?
# Sync is only enabled when the license permits it
project
.
feature_available?
(
:repository_mirrors
)
end
alias_method
:enabled?
,
:enabled
def
updated_since?
(
timestamp
)
last_update_started_at
&&
last_update_started_at
>
timestamp
&&
!
update_failed?
end
...
...
app/services/projects/update_mirror_service.rb
View file @
8ba4e8b3
...
...
@@ -5,7 +5,7 @@ module Projects
def
execute
unless
project
.
mirror?
return
error
(
"The project has no mirror to update"
)
return
success
end
unless
can?
(
current_user
,
:push_code_to_protected_branches
,
project
)
...
...
app/services/projects/update_remote_mirror_service.rb
View file @
8ba4e8b3
...
...
@@ -6,6 +6,8 @@ module Projects
@mirror
=
remote_mirror
@errors
=
[]
return
success
unless
remote_mirror
.
enabled?
begin
repository
.
fetch_remote
(
mirror
.
ref_name
,
no_tags:
true
)
...
...
app/views/admin/application_settings/_repository_mirrors_form.html.haml
View file @
8ba4e8b3
-
if
Gitlab
.
com?
-
if
Gitlab
.
com?
&&
License
.
feature_available?
(
:repository_mirrors
)
%fieldset
%legend
Repository mirror settings
.form-group
...
...
app/views/projects/mirrors/_pull.html.haml
0 → 100644
View file @
8ba4e8b3
-
expanded
=
Rails
.
env
.
test?
%section
.settings.project-mirror-settings
.settings-header
%h4
Pull from a remote repository
%button
.btn.js-settings-toggle
=
expanded
?
'Collapse'
:
'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository.
=
link_to
'Read more'
,
help_page_path
(
'workflow/repository_mirroring'
,
anchor:
'pulling-from-a-remote-repository'
),
target:
'_blank'
.settings-content.no-animate
{
class:
(
'expanded'
if
expanded
)
}
=
form_for
@project
,
url:
project_mirror_path
(
@project
)
do
|
f
|
%div
=
form_errors
(
@project
)
%h5
Set up mirror repository
=
render
"shared/mirror_update_button"
-
if
@project
.
mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update
#{
time_ago_with_tooltip
(
@project
.
mirror_last_update_at
)
}
.
-
if
@project
.
mirror_ever_updated_successfully?
Last successful update
#{
time_ago_with_tooltip
(
@project
.
mirror_last_successful_update_at
)
}
.
.panel-body
%pre
:preserve
#{
h
(
@project
.
import_error
.
try
(
:strip
))
}
.form-group
=
f
.
check_box
:mirror
,
class:
"pull-left"
.prepend-left-20
=
f
.
label
:mirror
,
"Mirror repository"
,
class:
"label-light append-bottom-0"
.form-group
=
f
.
label
:import_url
,
"Git repository URL"
,
class:
"label-light"
=
f
.
text_field
:import_url
,
class:
'form-control'
,
placeholder:
'https://username:password@gitlab.company.com/group/project.git'
=
render
"projects/mirrors/instructions"
.form-group
=
f
.
label
:mirror_user_id
,
"Mirror user"
,
class:
"label-light"
=
select_tag
(
'project[mirror_user_id]'
,
options_for_mirror_user
,
class:
"select2 lg"
,
required:
true
)
.help-block
This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.
You can only assign yourself to be the mirror user.
-
if
@project
.
builds_enabled?
=
render
"shared/mirror_trigger_builds_setting"
,
f:
f
=
f
.
submit
'Save changes'
,
class:
'btn btn-create'
,
name:
'update_remote_mirror'
app/views/projects/mirrors/_push.html.haml
0 → 100644
View file @
8ba4e8b3
-
expanded
=
Rails
.
env
.
test?
%section
.settings
.settings-header
%h4
Push to a remote repository
%button
.btn.js-settings-toggle
=
expanded
?
'Collapse'
:
'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
=
link_to
'Read more'
,
help_page_path
(
'workflow/repository_mirroring'
,
anchor:
'pushing-to-a-remote-repository'
),
target:
'_blank'
.settings-content.no-animate
{
class:
(
'expanded'
if
expanded
)
}
=
form_for
@project
,
url:
project_mirror_path
(
@project
)
do
|
f
|
%div
=
form_errors
(
@project
)
=
render
"shared/remote_mirror_update_button"
,
remote_mirror:
@remote_mirror
-
if
@remote_mirror
.
last_error
.
present?
.panel.panel-danger
.panel-heading
The remote repository failed to update
#{
time_ago_with_tooltip
(
@remote_mirror
.
last_update_at
)
}
.
-
if
@remote_mirror
.
last_successful_update_at
Last successful update
#{
time_ago_with_tooltip
(
@remote_mirror
.
last_successful_update_at
)
}
.
.panel-body
%pre
:preserve
#{
h
(
@remote_mirror
.
last_error
.
strip
)
}
=
f
.
fields_for
:remote_mirrors
,
@remote_mirror
do
|
rm_form
|
.form-group
=
rm_form
.
check_box
:enabled
,
class:
"pull-left"
.prepend-left-20
=
rm_form
.
label
:enabled
,
"Remote mirror repository"
,
class:
"label-light append-bottom-0"
%p
.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
=
rm_form
.
label
:url
,
"Git repository URL"
,
class:
"label-light"
=
rm_form
.
text_field
:url
,
class:
"form-control"
,
placeholder:
'https://username:password@gitlab.company.com/group/project.git'
=
render
"projects/mirrors/instructions"
=
f
.
submit
'Save changes'
,
class:
'btn btn-create'
,
name:
'update_remote_mirror'
app/views/projects/mirrors/_show.html.haml
View file @
8ba4e8b3
-
expanded
=
Rails
.
env
.
test?
%section
.settings.project-mirror-settings
.settings-header
%h4
Pull from a remote repository
%button
.btn.js-settings-toggle
=
expanded
?
'Collapse'
:
'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository.
=
link_to
'Read more'
,
help_page_path
(
'workflow/repository_mirroring'
,
anchor:
'pulling-from-a-remote-repository'
),
target:
'_blank'
.settings-content.no-animate
{
class:
(
'expanded'
if
expanded
)
}
=
form_for
@project
,
url:
project_mirror_path
(
@project
)
do
|
f
|
%div
=
form_errors
(
@project
)
%h5
Set up mirror repository
=
render
"shared/mirror_update_button"
-
if
@project
.
mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update
#{
time_ago_with_tooltip
(
@project
.
mirror_last_update_at
)
}
.
-
if
@project
.
mirror_ever_updated_successfully?
Last successful update
#{
time_ago_with_tooltip
(
@project
.
mirror_last_successful_update_at
)
}
.
.panel-body
%pre
:preserve
#{
h
(
@project
.
import_error
.
try
(
:strip
))
}
.form-group
=
f
.
check_box
:mirror
,
class:
"pull-left"
.prepend-left-20
=
f
.
label
:mirror
,
"Mirror repository"
,
class:
"label-light append-bottom-0"
.form-group
=
f
.
label
:import_url
,
"Git repository URL"
,
class:
"label-light"
=
f
.
text_field
:import_url
,
class:
'form-control'
,
placeholder:
'https://username:password@gitlab.company.com/group/project.git'
=
render
"projects/mirrors/instructions"
.form-group
=
f
.
label
:mirror_user_id
,
"Mirror user"
,
class:
"label-light"
=
select_tag
(
'project[mirror_user_id]'
,
options_for_mirror_user
,
class:
"select2 lg"
,
required:
true
)
.help-block
This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.
You can only assign yourself to be the mirror user.
-
if
@project
.
builds_enabled?
=
render
"shared/mirror_trigger_builds_setting"
,
f:
f
=
f
.
submit
'Save changes'
,
class:
'btn btn-create'
,
name:
'update_remote_mirror'
%section
.settings
.settings-header
%h4
Push to a remote repository
%button
.btn.js-settings-toggle
=
expanded
?
'Collapse'
:
'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
=
link_to
'Read more'
,
help_page_path
(
'workflow/repository_mirroring'
,
anchor:
'pushing-to-a-remote-repository'
),
target:
'_blank'
.settings-content.no-animate
{
class:
(
'expanded'
if
expanded
)
}
=
form_for
@project
,
url:
project_mirror_path
(
@project
)
do
|
f
|
%div
=
form_errors
(
@project
)
=
render
"shared/remote_mirror_update_button"
,
remote_mirror:
@remote_mirror
-
if
@remote_mirror
.
last_error
.
present?
.panel.panel-danger
.panel-heading
The remote repository failed to update
#{
time_ago_with_tooltip
(
@remote_mirror
.
last_update_at
)
}
.
-
if
@remote_mirror
.
last_successful_update_at
Last successful update
#{
time_ago_with_tooltip
(
@remote_mirror
.
last_successful_update_at
)
}
.
.panel-body
%pre
:preserve
#{
h
(
@remote_mirror
.
last_error
.
strip
)
}
=
f
.
fields_for
:remote_mirrors
,
@remote_mirror
do
|
rm_form
|
.form-group
=
rm_form
.
check_box
:enabled
,
class:
"pull-left"
.prepend-left-20
=
rm_form
.
label
:enabled
,
"Remote mirror repository"
,
class:
"label-light append-bottom-0"
%p
.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
=
rm_form
.
label
:url
,
"Git repository URL"
,
class:
"label-light"
=
rm_form
.
text_field
:url
,
class:
"form-control"
,
placeholder:
'https://username:password@gitlab.company.com/group/project.git'
=
render
"projects/mirrors/instructions"
=
f
.
submit
'Save changes'
,
class:
'btn btn-create'
,
name:
'update_remote_mirror'
-
if
@project
.
feature_available?
(
:repository_mirrors
)
=
render
'projects/mirrors/pull'
=
render
'projects/mirrors/push'
app/workers/update_all_mirrors_worker.rb
View file @
8ba4e8b3
...
...
@@ -11,7 +11,7 @@ class UpdateAllMirrorsWorker
fail_stuck_mirrors!
Project
.
mirrors_to_sync
.
each
(
&
:import_schedule
)
unless
Gitlab
::
Mirror
.
max_mirror_capacity_reached?
schedule_mirrors!
cancel_lease
(
lease_uuid
)
end
...
...
@@ -22,6 +22,32 @@ class UpdateAllMirrorsWorker
end
end
def
schedule_mirrors!
capacity
=
batch_size
=
Gitlab
::
Mirror
.
available_capacity
# Ignore mirrors that become due for scheduling once work begins, so we
# can't end up in an infinite loop
now
=
Time
.
now
last
=
nil
# Normally, this will complete in 1-2 batches. One batch will be added per
# `batch_size` unlicensed projects in the database.
while
capacity
>
0
projects
=
pull_mirrors_batch
(
freeze_at:
now
,
batch_size:
batch_size
,
offset_at:
last
)
break
if
projects
.
empty?
last
=
projects
.
last
.
mirror_data
.
next_execution_timestamp
projects
.
each
do
|
project
|
next
unless
project
.
feature_available?
(
:repository_mirrors
)
capacity
-=
1
project
.
import_schedule
break
unless
capacity
>
0
end
end
end
private
def
try_obtain_lease
...
...
@@ -31,4 +57,17 @@ class UpdateAllMirrorsWorker
def
cancel_lease
(
uuid
)
::
Gitlab
::
ExclusiveLease
.
cancel
(
LEASE_KEY
,
uuid
)
end
def
pull_mirrors_batch
(
freeze_at
:,
batch_size
:,
offset_at:
nil
)
relation
=
Project
.
mirror
.
joins
(
:mirror_data
)
.
where
(
"next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')"
,
freeze_at
)
.
reorder
(
'project_mirror_data.next_execution_timestamp'
)
.
limit
(
batch_size
)
relation
=
relation
.
where
(
'next_execution_timestamp > ?'
,
offset_at
)
if
offset_at
relation
end
end
changelogs/unreleased-ee/tc-namespace-license-checks--repository-mirrors.yml
0 → 100644
View file @
8ba4e8b3
---
title
:
Namespace license checks for Repository Mirrors
merge_request
:
2328
author
:
spec/controllers/ee/admin/application_settings_controller_spec.rb
0 → 100644
View file @
8ba4e8b3
require
'spec_helper'
describe
Admin
::
ApplicationSettingsController
do
# rubocop:disable RSpec/FilePath
include
StubENV
let
(
:admin
)
{
create
(
:admin
)
}
before
do
stub_env
(
'IN_MEMORY_APPLICATION_SETTINGS'
,
'false'
)
end
describe
'PUT #update'
do
before
do
sign_in
(
admin
)
end
it
'updates the EE specific application settings'
do
settings
=
{
help_text:
'help_text'
,
elasticsearch_url:
'http://my-elastic.search:9200'
,
elasticsearch_indexing:
true
,
elasticsearch_aws:
true
,
elasticsearch_aws_access_key:
'elasticsearch_aws_access_key'
,
elasticsearch_aws_secret_access_key:
'elasticsearch_aws_secret_access_key'
,
elasticsearch_aws_region:
'elasticsearch_aws_region'
,
elasticsearch_search:
true
,
repository_size_limit:
1024
,
shared_runners_minutes:
60
,
geo_status_timeout:
30
,
elasticsearch_experimental_indexer:
true
,
check_namespace_plan:
true
,
authorized_keys_enabled:
true
,
slack_app_enabled:
true
,
slack_app_id:
'slack_app_id'
,
slack_app_secret:
'slack_app_secret'
,
slack_app_verification_token:
'slack_app_verification_token'
}
put
:update
,
application_setting:
settings
expect
(
response
).
to
redirect_to
(
admin_application_settings_path
)
settings
.
except
(
:elasticsearch_url
,
:repository_size_limit
).
each
do
|
setting
,
value
|
expect
(
ApplicationSetting
.
current
.
public_send
(
setting
)).
to
eq
(
value
)
end
expect
(
ApplicationSetting
.
current
.
repository_size_limit
).
to
eq
(
settings
[
:repository_size_limit
].
megabytes
)
expect
(
ApplicationSetting
.
current
.
elasticsearch_url
).
to
contain_exactly
(
settings
[
:elasticsearch_url
])
end
it
'does not update mirror settings when repository mirrors unlicensed'
do
stub_licensed_features
(
repository_mirrors:
false
)
settings
=
{
mirror_max_delay:
12
,
mirror_max_capacity:
2
,
mirror_capacity_threshold:
2
}
settings
.
each
do
|
setting
,
_value
|
expect
do
put
:update
,
application_setting:
settings
end
.
not_to
change
(
ApplicationSetting
.
current
.
reload
,
setting
)
end
end
it
'updates mirror settings when repository mirrors is licensed'
do
stub_licensed_features
(
repository_mirrors:
true
)
settings
=
{
mirror_max_delay:
12
,
mirror_max_capacity:
2
,
mirror_capacity_threshold:
2
}
put
:update
,
application_setting:
settings
settings
.
each
do
|
setting
,
value
|
expect
(
ApplicationSetting
.
current
.
public_send
(
setting
)).
to
eq
(
value
)
end
end
end
end
spec/controllers/ee/projects_controller_spec.rb
0 → 100644
View file @
8ba4e8b3
require
(
'spec_helper'
)
describe
ProjectsController
do
# rubocop:disable RSpec/FilePath
let
(
:project
)
{
create
(
:empty_project
)
}
let
(
:user
)
{
create
(
:user
)
}
before
do
project
.
add_master
(
user
)
sign_in
(
user
)
end
describe
'PUT #update'
do
before
do
controller
.
instance_variable_set
(
:@project
,
project
)
end
it
'updates EE attributes'
do
params
=
{
repository_size_limit:
1024
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
params
.
except
(
:repository_size_limit
).
each
do
|
param
,
value
|
expect
(
project
.
public_send
(
param
)).
to
eq
(
value
)
end
expect
(
project
.
repository_size_limit
).
to
eq
(
params
[
:repository_size_limit
].
megabytes
)
end
it
'updates Merge Request Approvers attributes'
do
params
=
{
approvals_before_merge:
50
,
approver_group_ids:
create
(
:group
).
id
,
approver_ids:
create
(
:user
).
id
,
reset_approvals_on_push:
false
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
expect
(
project
.
approver_groups
.
pluck
(
:group_id
)).
to
contain_exactly
(
params
[
:approver_group_ids
])
expect
(
project
.
approvers
.
pluck
(
:user_id
)).
to
contain_exactly
(
params
[
:approver_ids
])
end
it
'updates Issuable Default Templates attributes'
do
params
=
{
issues_template:
'You got issues?'
,
merge_requests_template:
'I got tissues'
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
params
.
each
do
|
param
,
value
|
expect
(
project
.
public_send
(
param
)).
to
eq
(
value
)
end
end
it
'updates Fast Forward Merge attributes'
do
params
=
{
merge_method: :ff
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
params
.
each
do
|
param
,
value
|
expect
(
project
.
public_send
(
param
)).
to
eq
(
value
)
end
end
it
'updates Fast Forward Merge attributes'
do
params
=
{
merge_method: :ff
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
params
.
each
do
|
param
,
value
|
expect
(
project
.
public_send
(
param
)).
to
eq
(
value
)
end
end
it
'updates Service Desk attributes'
do
allow
(
Gitlab
::
IncomingEmail
).
to
receive
(
:enabled?
)
{
true
}
allow
(
Gitlab
::
IncomingEmail
).
to
receive
(
:supports_wildcard?
)
{
true
}
stub_licensed_features
(
service_desk:
true
)
params
=
{
service_desk_enabled:
true
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
expect
(
response
).
to
have_http_status
(
302
)
expect
(
project
.
service_desk_enabled
).
to
eq
(
true
)
end
context
'repository mirrors licensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
true
)
end
it
'updates repository mirror attributes'
do
params
=
{
mirror:
true
,
mirror_trigger_builds:
true
,
mirror_user_id:
user
.
id
}
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
params
.
each
do
|
param
,
value
|
expect
(
project
.
public_send
(
param
)).
to
eq
(
value
)
end
end
end
context
'repository mirrors unlicensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
false
)
end
it
'does not update repository mirror attributes'
do
params
=
{
mirror:
true
,
mirror_trigger_builds:
true
,
mirror_user_id:
user
.
id
}
params
.
each
do
|
param
,
_value
|
expect
do
put
:update
,
namespace_id:
project
.
namespace
,
id:
project
.
id
,
project:
params
end
.
not_to
change
(
project
,
param
)
end
end
end
end
end
spec/features/projects/mirror_spec.rb
View file @
8ba4e8b3
...
...
@@ -10,6 +10,18 @@ feature 'Project mirror', feature: true do
sign_in
user
end
context
'unlicensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
false
)
end
it
'returns 404'
do
visit
project_mirror_path
(
project
)
expect
(
page
.
status_code
).
to
eq
(
404
)
end
end
context
'with Update now button'
do
let
(
:timestamp
)
{
Time
.
now
}
...
...
spec/features/projects/settings/ee/push_rules_settings_spec.rb
View file @
8ba4e8b3
require
'spec_helper'
describe
'Project settings > [EE] repository'
,
feature:
true
do
include
Select2Helper
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project_empty_repo
)
}
...
...
@@ -44,24 +42,4 @@ describe 'Project settings > [EE] repository', feature: true do
end
end
end
describe
'mirror settings'
,
:js
do
let
(
:user2
)
{
create
(
:user
)
}
before
do
project
.
team
<<
[
user2
,
:master
]
visit
project_settings_repository_path
(
project
)
end
it
'sets mirror user'
do
page
.
within
(
'.project-mirror-settings'
)
do
select2
(
user2
.
id
,
from:
'#project_mirror_user_id'
)
click_button
(
'Save changes'
)
expect
(
find
(
'.select2-chosen'
)).
to
have_content
(
user
.
name
)
end
end
end
end
spec/features/projects/settings/ee/repository_mirrors_settings_spec.rb
0 → 100644
View file @
8ba4e8b3
require
'spec_helper'
describe
'Project settings > [EE] repository'
,
feature:
true
do
include
Select2Helper
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project_empty_repo
)
}
before
do
project
.
add_master
(
user
)
gitlab_sign_in
(
user
)
end
context
'unlicensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
false
)
visit
project_settings_repository_path
(
project
)
end
it
'does not show pull mirror settings'
do
expect
(
page
).
to
have_no_selector
(
'#project_mirror'
)
expect
(
page
).
to
have_no_selector
(
'#project_import_url'
)
expect
(
page
).
to
have_no_selector
(
'#project_mirror_user_id'
,
visible:
false
)
expect
(
page
).
to
have_no_selector
(
'#project_mirror_trigger_builds'
)
end
it
'does not show push mirror settings'
do
expect
(
page
).
to
have_no_selector
(
'#project_remote_mirrors_attributes_0_enabled'
)
expect
(
page
).
to
have_no_selector
(
'#project_remote_mirrors_attributes_0_url'
)
end
end
describe
'mirror settings'
,
:js
do
let
(
:user2
)
{
create
(
:user
)
}
before
do
project
.
team
<<
[
user2
,
:master
]
visit
project_settings_repository_path
(
project
)
end
it
'shows pull mirror settings'
do
expect
(
page
).
to
have_selector
(
'#project_mirror'
)
expect
(
page
).
to
have_selector
(
'#project_import_url'
)
expect
(
page
).
to
have_selector
(
'#project_mirror_user_id'
,
visible:
false
)
expect
(
page
).
to
have_selector
(
'#project_mirror_trigger_builds'
)
end
it
'shows push mirror settings'
do
expect
(
page
).
to
have_selector
(
'#project_remote_mirrors_attributes_0_enabled'
)
expect
(
page
).
to
have_selector
(
'#project_remote_mirrors_attributes_0_url'
)
end
it
'sets mirror user'
do
page
.
within
(
'.project-mirror-settings'
)
do
select2
(
user2
.
id
,
from:
'#project_mirror_user_id'
)
click_button
(
'Save changes'
)
expect
(
find
(
'.select2-chosen'
)).
to
have_content
(
user
.
name
)
end
end
end
end
spec/models/ee/project_spec.rb
View file @
8ba4e8b3
...
...
@@ -217,6 +217,62 @@ describe Project, models: true do
end
end
describe
'#has_remote_mirror?'
do
let
(
:project
)
{
create
(
:empty_project
,
:remote_mirror
,
:import_started
)
}
subject
{
project
.
has_remote_mirror?
}
before
do
allow_any_instance_of
(
RemoteMirror
).
to
receive
(
:refresh_remote
)
end
it
'returns true when a remote mirror is enabled'
do
is_expected
.
to
be_truthy
end
it
'returns false when unlicensed'
do
stub_licensed_features
(
repository_mirrors:
false
)
is_expected
.
to
be_falsy
end
it
'returns false when remote mirror is disabled'
do
project
.
remote_mirrors
.
first
.
update_attributes
(
enabled:
false
)
is_expected
.
to
be_falsy
end
end
describe
'#update_remote_mirrors'
do
let
(
:project
)
{
create
(
:empty_project
,
:remote_mirror
,
:import_started
)
}
delegate
:update_remote_mirrors
,
to: :project
before
do
allow_any_instance_of
(
RemoteMirror
).
to
receive
(
:refresh_remote
)
end
it
'syncs enabled remote mirror'
do
expect_any_instance_of
(
RemoteMirror
).
to
receive
(
:sync
)
update_remote_mirrors
end
it
'does nothing when unlicensed'
do
stub_licensed_features
(
repository_mirrors:
false
)
expect_any_instance_of
(
RemoteMirror
).
not_to
receive
(
:sync
)
update_remote_mirrors
end
it
'does not sync disabled remote mirrors'
do
project
.
remote_mirrors
.
first
.
update_attributes
(
enabled:
false
)
expect_any_instance_of
(
RemoteMirror
).
not_to
receive
(
:sync
)
update_remote_mirrors
end
end
describe
'#any_runners_limit'
do
let
(
:project
)
{
create
(
:empty_project
,
shared_runners_enabled:
shared_runners_enabled
)
}
let
(
:specific_runner
)
{
create
(
:ci_runner
)
}
...
...
spec/models/remote_mirror_spec.rb
View file @
8ba4e8b3
...
...
@@ -96,6 +96,18 @@ describe RemoteMirror do
Timecop
.
return
end
context
'repository mirrors not licensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
false
)
end
it
'does not schedule RepositoryUpdateRemoteMirrorWorker'
do
expect
(
RepositoryUpdateRemoteMirrorWorker
).
not_to
receive
(
:perform_in
)
remote_mirror
.
sync
end
end
context
'with remote mirroring enabled'
do
it
'schedules a RepositoryUpdateRemoteMirrorWorker to run within a certain backoff delay'
do
expect
(
RepositoryUpdateRemoteMirrorWorker
).
to
receive
(
:perform_in
).
with
(
RemoteMirror
::
BACKOFF_DELAY
,
remote_mirror
.
id
,
Time
.
now
)
...
...
spec/services/projects/update_mirror_service_spec.rb
View file @
8ba4e8b3
...
...
@@ -4,6 +4,21 @@ describe Projects::UpdateMirrorService do
let
(
:project
)
{
create
(
:project
,
:mirror
,
import_url:
Project
::
UNKNOWN_IMPORT_URL
)
}
describe
"#execute"
do
context
'unlicensed'
do
before
do
stub_licensed_features
(
repository_mirrors:
false
)
end
it
'does nothing'
do
allow_any_instance_of
(
EE
::
Project
).
to
receive
(
:destroy_mirror_data
)
expect
(
project
).
not_to
receive
(
:fetch_mirror
)
result
=
described_class
.
new
(
project
,
project
.
owner
).
execute
expect
(
result
[
:status
]).
to
eq
(
:success
)
end
end
it
"fetches the upstream repository"
do
expect
(
project
).
to
receive
(
:fetch_mirror
)
...
...
@@ -111,12 +126,12 @@ describe Projects::UpdateMirrorService do
describe
"when is no mirror"
do
let
(
:project
)
{
build_stubbed
(
:project
)
}
it
"
fail
s"
do
it
"
succes
s"
do
expect
(
project
.
mirror?
).
to
eq
(
false
)
result
=
described_class
.
new
(
project
,
build_stubbed
(
:user
)).
execute
expect
(
result
[
:status
]).
to
eq
(
:
error
)
expect
(
result
[
:status
]).
to
eq
(
:
success
)
end
end
end
...
...
spec/services/projects/update_remote_mirror_service_spec.rb
View file @
8ba4e8b3
...
...
@@ -4,7 +4,7 @@ describe Projects::UpdateRemoteMirrorService do
let
(
:project
)
{
create
(
:project
)
}
let
(
:remote_project
)
{
create
(
:forked_project_with_submodules
)
}
let
(
:repository
)
{
project
.
repository
}
let
(
:remote_mirror
)
{
project
.
remote_mirrors
.
create!
(
url:
remote_project
.
http_url_to_repo
)
}
let
(
:remote_mirror
)
{
project
.
remote_mirrors
.
create!
(
url:
remote_project
.
http_url_to_repo
,
enabled:
true
)
}
subject
{
described_class
.
new
(
project
,
project
.
creator
)
}
...
...
@@ -18,6 +18,14 @@ describe Projects::UpdateRemoteMirrorService do
allow
(
gitlab_shell
).
to
receive
(
:push_remote_branches
).
and_return
(
true
)
end
it
'does nothing when unlicensed'
do
stub_licensed_features
(
repository_mirrors:
false
)
expect
(
project
.
repository
).
not_to
receive
(
:fetch_remote
)
subject
.
execute
(
remote_mirror
)
end
it
"fetches the remote repository"
do
expect
(
repository
).
to
receive
(
:fetch_remote
).
with
(
remote_mirror
.
ref_name
,
no_tags:
true
)
do
sync_remote
(
repository
,
remote_mirror
.
ref_name
,
local_branch_names
)
...
...
spec/workers/update_all_mirrors_worker_spec.rb
View file @
8ba4e8b3
require
'
rails
_helper'
require
'
spec
_helper'
describe
UpdateAllMirrorsWorker
do
subject
(
:worker
)
{
described_class
.
new
}
...
...
@@ -23,6 +23,12 @@ describe UpdateAllMirrorsWorker do
worker
.
perform
end
it
'schedules mirrors'
do
expect
(
worker
).
to
receive
(
:schedule_mirrors!
)
worker
.
perform
end
end
describe
'#fail_stuck_mirrors!'
do
...
...
@@ -63,4 +69,80 @@ describe UpdateAllMirrorsWorker do
expect
(
project
.
reload
.
import_error
).
to
eq
'The mirror update took too long to complete.'
end
end
describe
'#schedule_mirrors!'
do
def
schedule_mirrors!
(
capacity
:)
allow
(
Gitlab
::
Mirror
).
to
receive_messages
(
available_capacity:
capacity
)
Sidekiq
::
Testing
.
fake!
do
worker
.
schedule_mirrors!
end
end
def
expect_import_status
(
project
,
status
)
expect
(
project
.
reload
.
import_status
).
to
eq
(
status
)
end
def
expect_import_scheduled
(
*
projects
)
projects
.
each
{
|
project
|
expect_import_status
(
project
,
'scheduled'
)
}
end
def
expect_import_not_scheduled
(
*
projects
)
projects
.
each
{
|
project
|
expect_import_status
(
project
,
'none'
)
}
end
context
'unlicensed'
do
it
'does not schedule when project does not have repository mirrors available'
do
project
=
create
(
:empty_project
,
:mirror
)
stub_licensed_features
(
repository_mirrors:
false
)
schedule_mirrors!
(
capacity:
5
)
expect_import_not_scheduled
(
project
)
end
end
context
'licensed'
do
def
scheduled_mirror
(
at
:,
licensed
:)
namespace
=
create
(
:group
,
:public
,
plan:
(
Namespace
::
BRONZE_PLAN
if
licensed
))
project
=
create
(
:empty_project
,
:public
,
:mirror
,
namespace:
namespace
)
project
.
mirror_data
.
update!
(
next_execution_timestamp:
at
)
project
.
update!
(
visibility_level:
Gitlab
::
VisibilityLevel
::
PRIVATE
)
project
end
before
do
stub_licensed_features
(
repository_mirrors:
true
)
stub_application_setting
(
check_namespace_plan:
true
)
allow
(
Gitlab
).
to
receive_messages
(
com?:
true
)
end
let!
(
:unlicensed_project
)
{
scheduled_mirror
(
at:
4
.
weeks
.
ago
,
licensed:
false
)
}
let!
(
:earliest_project
)
{
scheduled_mirror
(
at:
3
.
weeks
.
ago
,
licensed:
true
)
}
let!
(
:latest_project
)
{
scheduled_mirror
(
at:
2
.
weeks
.
ago
,
licensed:
true
)
}
it
"schedules all available mirrors when capacity is in excess"
do
schedule_mirrors!
(
capacity:
3
)
expect_import_scheduled
(
earliest_project
,
latest_project
)
expect_import_not_scheduled
(
unlicensed_project
)
end
it
"schedules all available mirrors when capacity is sufficient"
do
schedule_mirrors!
(
capacity:
2
)
expect_import_scheduled
(
earliest_project
,
latest_project
)
expect_import_not_scheduled
(
unlicensed_project
)
end
it
'schedules mirrors by next_execution_timestamp when capacity is insufficient'
do
schedule_mirrors!
(
capacity:
1
)
expect_import_scheduled
(
earliest_project
)
expect_import_not_scheduled
(
unlicensed_project
,
latest_project
)
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment