Rollback project folder move after error in Projects::TransferService

This commit is contained in:
James Edwards-Jones 2017-06-02 14:44:59 +01:00
parent f07aee72be
commit 6db1a28016
3 changed files with 141 additions and 42 deletions

View File

@ -12,87 +12,121 @@ module Projects
TransferError = Class.new(StandardError) TransferError = Class.new(StandardError)
def execute(new_namespace) def execute(new_namespace)
if new_namespace.blank? @new_namespace = new_namespace
if @new_namespace.blank?
raise TransferError, 'Please select a new namespace for your project.' raise TransferError, 'Please select a new namespace for your project.'
end end
unless allowed_transfer?(current_user, project, new_namespace)
unless allowed_transfer?(current_user, project)
raise TransferError, 'Transfer failed, please contact an admin.' raise TransferError, 'Transfer failed, please contact an admin.'
end end
transfer(project, new_namespace)
transfer(project)
true
rescue Projects::TransferService::TransferError => ex rescue Projects::TransferService::TransferError => ex
project.reload project.reload
project.errors.add(:new_namespace, ex.message) project.errors.add(:new_namespace, ex.message)
false false
end end
def transfer(project, new_namespace) private
old_namespace = project.namespace
Project.transaction do def transfer(project)
old_path = project.path_with_namespace @old_path = project.path_with_namespace
old_group = project.group @old_group = project.group
new_path = File.join(new_namespace.try(:full_path) || '', project.path) @new_path = File.join(@new_namespace.try(:full_path) || '', project.path)
@old_namespace = project.namespace
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists?
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
if project.has_container_registry_tags? if project.has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry # We currently don't support renaming repository if it contains tags in container registry
raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
end end
project.expire_caches_before_rename(old_path) attempt_transfer_transaction
end
# Apply new namespace id and visibility level def attempt_transfer_transaction
project.namespace = new_namespace Project.transaction do
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? project.expire_caches_before_rename(@old_path)
project.save!
update_namespace_and_visibility(@new_namespace)
# Notifications # Notifications
project.send_move_instructions(old_path) project.send_move_instructions(@old_path)
# Move main repository # Move main repository
unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) unless move_repo_folder(@old_path, @new_path)
raise TransferError.new('Cannot move project') raise TransferError.new('Cannot move project')
end end
# Move wiki repo also if present # Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
# Move missing group labels to project # Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads # Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
# Move pages # Move pages
Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = old_path project.old_path_with_namespace = @old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) execute_system_hooks
end
rescue Exception # rubocop:disable Lint/RescueException
rollback_side_effects
raise
ensure
refresh_permissions
end end
refresh_permissions(old_namespace, new_namespace) def allowed_transfer?(current_user, project)
@new_namespace &&
true
end
def allowed_transfer?(current_user, project, namespace)
namespace &&
can?(current_user, :change_namespace, project) && can?(current_user, :change_namespace, project) &&
namespace.id != project.namespace_id && @new_namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, @new_namespace)
end end
def refresh_permissions(old_namespace, new_namespace) def update_namespace_and_visibility(to_namespace)
# Apply new namespace id and visibility level
project.namespace = to_namespace
project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
end
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to # This ensures we only schedule 1 job for every user that has access to
# the namespaces. # the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations | user_ids = @old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations @new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute UserProjectAccessChangedService.new(user_ids).execute
end end
def rollback_side_effects
rollback_folder_move
update_namespace_and_visibility(@old_namespace)
end
def rollback_folder_move
move_repo_folder(@new_path, @old_path)
move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki")
end
def move_repo_folder(from_name, to_name)
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
end
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
end end
end end

View File

@ -0,0 +1,4 @@
---
title: Rollback project repo move if there is an error in Projects::TransferService
merge_request: 11877
author:

View File

@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(group) } it { expect(project.namespace).to eq(group) }
end end
context 'when transfer succeeds' do
before do
group.add_owner(user)
end
it 'sends notifications' do
expect_any_instance_of(NotificationService).to receive(:project_was_moved)
transfer_project(project, user, group)
end
it 'executes system hooks' do
expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks)
transfer_project(project, user, group)
end
end
context 'when transfer fails' do
let!(:original_path) { project_path(project) }
def attempt_project_transfer
expect do
transfer_project(project, user, group)
end.to raise_error(ActiveRecord::ActiveRecordError)
end
before do
group.add_owner(user)
expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR")
end
def project_path(project)
File.join(project.repository_storage_path, "#{project.path_with_namespace}.git")
end
def current_path
project_path(project)
end
it 'rolls back repo location' do
attempt_project_transfer
expect(Dir.exist?(original_path)).to be_truthy
expect(original_path).to eq current_path
end
it "doesn't send move notifications" do
expect_any_instance_of(NotificationService).not_to receive(:project_was_moved)
attempt_project_transfer
end
it "doesn't run system hooks" do
expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks)
attempt_project_transfer
end
end
context 'namespace -> no namespace' do context 'namespace -> no namespace' do
before do before do
@result = transfer_project(project, user, nil) @result = transfer_project(project, user, nil)