Commit a6aa6312 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Allow projects to be moved between repository storages asynchronously via API

parent 5d08aa4c
Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased)
- Allow projects to be moved between repository storages
- Performance improvement of push rules
- Change LdapGroupSyncWorker to use new LDAP group sync classes
- [Elastic][Fix] Commit search breaks for some URLs on gitlab-ce project
......
......@@ -184,6 +184,8 @@ class Ability
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
rules << :change_repository_storage if user.admin?
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
......
......@@ -1497,6 +1497,16 @@ class Project < ActiveRecord::Base
end
end
def change_repository_storage(new_repository_storage_key)
return if repository_read_only?
return if repository_storage == new_repository_storage_key
raise ArgumentError unless Gitlab.config.repositories.storages.keys.include?(new_repository_storage_key)
run_after_commit { ProjectUpdateRepositoryStorageWorker.perform_async(id, new_repository_storage_key) }
self.repository_read_only = true
end
private
def default_branch_protected?
......
module Projects
class UpdateRepositoryStorageService < BaseService
include Gitlab::ShellAdapter
def initialize(project)
@project = project
end
def execute(new_repository_storage_key)
new_storage_path = Gitlab.config.repositories.storages[new_repository_storage_key]
result = move_storage(project.path_with_namespace, new_storage_path)
if project.wiki.repository_exists?
result &&= move_storage("#{project.path_with_namespace}.wiki", new_storage_path)
end
if result
mark_old_paths_for_archive
project.update(repository_storage: new_repository_storage_key, repository_read_only: false)
else
project.update(repository_read_only: false)
end
end
private
def move_storage(project_path, new_storage_path)
gitlab_shell.mv_storage(project.repository_storage_path, project_path, new_storage_path)
end
def mark_old_paths_for_archive
old_repository_storage_path = project.repository_storage_path
new_project_path = moved_path(project.path_with_namespace)
# Notice that the block passed to `run_after_commit` will run with `project`
# as its context
project.run_after_commit do
GitlabShellWorker.perform_async(:mv_repository,
old_repository_storage_path,
path_with_namespace,
new_project_path)
if wiki.repository_exists?
GitlabShellWorker.perform_async(:mv_repository,
old_repository_storage_path,
"#{path_with_namespace}.wiki",
"#{new_project_path}.wiki")
end
end
end
def moved_path(path)
"#{path}+#{project.id}+moved+#{Time.now.to_i}"
end
end
end
......@@ -13,13 +13,20 @@ module Projects
end
end
new_branch = params[:default_branch]
new_branch = params.delete(:default_branch)
new_repository_storage = params.delete(:repository_storage)
if project.repository.exists? && new_branch && new_branch != project.default_branch
project.change_head(new_branch)
if project.repository.exists?
if new_branch && new_branch != project.default_branch
project.change_head(new_branch)
end
if new_repository_storage && can?(current_user, :change_repository_storage, project)
project.change_repository_storage(new_repository_storage)
end
end
if project.update_attributes(params.except(:default_branch))
if project.update_attributes(params)
if project.previous_changes.include?('path')
project.rename_repo
end
......
class ProjectUpdateRepositoryStorageWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(project_id, new_repository_storage_key)
project = Project.find(project_id)
::Projects::UpdateRepositoryStorageService.new(project).execute(new_repository_storage_key)
end
end
class AddRepositoryReadOnlyToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects, :repository_read_only, :boolean
end
end
......@@ -969,6 +969,7 @@ ActiveRecord::Schema.define(version: 20160810153405) do
t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki"
t.boolean "repository_read_only"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
......
......@@ -280,7 +280,8 @@ Parameters:
"group_name": "Gitlab Org",
"group_access_level": 10
}
]
],
"repository_storage": "default"
}
```
......@@ -448,6 +449,7 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
### Create project for user
......@@ -473,6 +475,7 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
### Edit project
......@@ -499,6 +502,7 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
......
......@@ -94,6 +94,7 @@ module API
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
expose :repository_storage, if: lambda { |_project, options| options[:user].try(:admin?) }
end
class ProjectMember < UserBasic
......
......@@ -105,6 +105,7 @@ module API
# visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
# repository_storage (optional)
# Example Request
# POST /projects
post do
......@@ -123,7 +124,8 @@ module API
:public,
:visibility_level,
:import_url,
:public_builds]
:public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
......@@ -155,6 +157,7 @@ module API
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
# repository_storage (optional)
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
......@@ -172,7 +175,8 @@ module API
:public,
:visibility_level,
:import_url,
:public_builds]
:public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
......@@ -218,6 +222,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
# public_builds (optional)
# repository_storage (optional)
# Example Request
# PUT /projects/:id
put ':id' do
......@@ -234,7 +239,8 @@ module API
:shared_runners_enabled,
:public,
:visibility_level,
:public_builds]
:public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
......
......@@ -106,6 +106,20 @@ module Gitlab
storage, "#{path}.git", "#{new_path}.git"])
end
# Move repository storage
#
# current_storage - project's current storage path
# path - project path with namespace
# new_storage - new storage path
#
# Ex.
# mv_storage("/path/to/storage", "randx/gitlab-ci", "/new/storage/path")
#
def mv_storage(current_storage, path, new_storage)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-storage',
current_storage, "#{path}.git", new_storage])
end
# Fork repository to new namespace
# forked_from_storage - forked-from project's storage path
# path - project path with namespace
......
......@@ -59,6 +59,10 @@ module Gitlab
end
def push_access_check(changes)
if project.repository_read_only?
return build_status_object(false, 'The repository is temporarily read-only. Please try again later.')
end
if Gitlab::Geo.secondary?
return build_status_object(false, "You can't push code on a secondary GitLab Geo node.")
end
......
......@@ -27,7 +27,7 @@ namespace :gitlab do
all_dirs.each do |dir_path|
if remove_flag
if FileUtils.rm_rf dir_path
if FileUtils.rm_rf(dir_path)
puts "Removed...#{dir_path}".color(:red)
else
puts "Cannot remove #{dir_path}".color(:red)
......
......@@ -44,6 +44,10 @@ FactoryGirl.define do
project.create_repository
end
end
trait :read_only_repository do
repository_read_only true
end
end
# Project with empty repository
......
......@@ -44,6 +44,30 @@ describe Gitlab::Shell, lib: true do
end
end
describe 'projects commands' do
let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' }
before do
allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test')
end
describe 'mv_repository' do
it 'executes the command' do
expect(Gitlab::Utils).to receive(:system_silent).
with([projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git'])
gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path')
end
end
describe 'mv_storage' do
it 'executes the command' do
expect(Gitlab::Utils).to receive(:system_silent).
with([projects_path, 'mv-storage', 'current/storage', 'project/path.git', 'new/storage'])
gitlab_shell.mv_storage('current/storage', 'project/path', 'new/storage')
end
end
end
describe Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do
it 'normalizes space characters in the key' do
......
......@@ -555,4 +555,15 @@ describe Gitlab::GitAccess, lib: true do
end
end
end
context 'when the repository is read only' do
it 'denies push access' do
project = create(:project, :read_only_repository)
project.team << [user, :master]
check = access.check('git-receive-pack')
expect(check).not_to be_allowed
end
end
end
......@@ -1580,4 +1580,51 @@ describe Project, models: true do
expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
end
end
describe '#change_repository_storage' do
let(:project) { create(:project, repository_storage: 'a') }
let(:read_only_project) { create(:project, repository_storage: 'a', repository_read_only: true) }
before do
FileUtils.mkdir('tmp/tests/storage_a')
FileUtils.mkdir('tmp/tests/storage_b')
storages = { 'a' => 'tmp/tests/storage_a', 'b' => 'tmp/tests/storage_b' }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
after do
FileUtils.rm_rf('tmp/tests/storage_a')
FileUtils.rm_rf('tmp/tests/storage_b')
end
it 'schedule the transfer of the repository to the new storage and locks the project' do
expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'b')
project.change_repository_storage('b')
project.save
expect(project).to be_repository_read_only
end
it "doesn't schedule the transfer if the repository is already read-only" do
expect(ProjectUpdateRepositoryStorageWorker).not_to receive(:perform_async)
read_only_project.change_repository_storage('b')
read_only_project.save
end
it "doesn't lock or schedule the transfer if the storage hasn't changed" do
expect(ProjectUpdateRepositoryStorageWorker).not_to receive(:perform_async)
project.change_repository_storage('a')
project.save
expect(project).not_to be_repository_read_only
end
it 'throws an error if an invalid repository storage is provided' do
expect { project.change_repository_storage('c') }.to raise_error
end
end
end
require 'spec_helper'
describe Projects::UpdateRepositoryStorageService, services: true do
subject { described_class.new(project) }
describe "#execute" do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:time) { Time.now }
before do
FileUtils.mkdir('tmp/tests/storage_a')
FileUtils.mkdir('tmp/tests/storage_b')
storages = { 'a' => 'tmp/tests/storage_a', 'b' => 'tmp/tests/storage_b' }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(subject).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(Time).to receive(:now).and_return(time)
end
after do
FileUtils.rm_rf('tmp/tests/storage_a')
FileUtils.rm_rf('tmp/tests/storage_b')
end
context 'without wiki' do
let(:project) { create(:project, repository_storage: 'a', repository_read_only: true, wiki_enabled: false) }
context 'when the move succeeds' do
it 'moves the repository to the new storage and unmarks the repository as read only' do
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', project.path_with_namespace, 'tmp/tests/storage_b').
and_return(true)
expect(GitlabShellWorker).to receive(:perform_async).
with(:mv_repository,
'tmp/tests/storage_a',
project.path_with_namespace,
"#{project.path_with_namespace}+#{project.id}+moved+#{time.to_i}")
subject.execute('b')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('b')
end
end
context 'when the move fails' do
it 'unmarks the repository as read-only without updating the repository storage' do
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', project.path_with_namespace, 'tmp/tests/storage_b').
and_return(false)
expect(GitlabShellWorker).not_to receive(:perform_async)
subject.execute('b')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('a')
end
end
end
context 'with wiki' do
let(:project) { create(:project, repository_storage: 'a', repository_read_only: true, wiki_enabled: true) }
before { project.create_wiki }
context 'when the move succeeds' do
it 'moves the repository and its wiki to the new storage and unmarks the repository as read only' do
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', project.path_with_namespace, 'tmp/tests/storage_b').
and_return(true)
expect(GitlabShellWorker).to receive(:perform_async).
with(:mv_repository,
'tmp/tests/storage_a',
project.path_with_namespace,
"#{project.path_with_namespace}+#{project.id}+moved+#{time.to_i}")
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', "#{project.path_with_namespace}.wiki", 'tmp/tests/storage_b').
and_return(true)
expect(GitlabShellWorker).to receive(:perform_async).
with(:mv_repository,
'tmp/tests/storage_a',
"#{project.path_with_namespace}.wiki",
"#{project.path_with_namespace}+#{project.id}+moved+#{time.to_i}.wiki")
subject.execute('b')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('b')
end
end
context 'when the move of the wiki fails' do
it 'unmarks the repository as read-only without updating the repository storage' do
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', project.path_with_namespace, 'tmp/tests/storage_b').
and_return(true)
expect(gitlab_shell).to receive(:mv_storage).
with('tmp/tests/storage_a', "#{project.path_with_namespace}.wiki", 'tmp/tests/storage_b').
and_return(false)
expect(GitlabShellWorker).not_to receive(:perform_async)
subject.execute('b')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('a')
end
end
end
end
end
......@@ -139,6 +139,38 @@ describe Projects::UpdateService, services: true do
end
end
describe 'repository_storage' do
let(:admin_user) { create(:user, admin: true) }
let(:user) { create(:user) }
let(:project) { create(:project, repository_storage: 'a') }
let(:opts) { { repository_storage: 'b' } }
before do
FileUtils.mkdir('tmp/tests/storage_a')
FileUtils.mkdir('tmp/tests/storage_b')
storages = { 'a' => 'tmp/tests/storage_a', 'b' => 'tmp/tests/storage_b' }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
after do
FileUtils.rm_rf('tmp/tests/storage_a')
FileUtils.rm_rf('tmp/tests/storage_b')
end
it 'calls the change repository storage method if the storage changed' do
expect(project).to receive(:change_repository_storage).with('b')
update_project(project, admin_user, opts).inspect
end
it "doesn't call the change repository storage for non-admin users" do
expect(project).not_to receive(:change_repository_storage)
update_project(project, user, opts).inspect
end
end
def update_project(project, user, opts)
Projects::UpdateService.new(project, user, opts).execute
end
......
require 'spec_helper'
describe ProjectUpdateRepositoryStorageWorker do
let(:project) { create(:project) }
subject { ProjectUpdateRepositoryStorageWorker.new }
describe "#perform" do
it "should call the update repository storage service" do
expect_any_instance_of(Projects::UpdateRepositoryStorageService).
to receive(:execute).with('new_storage')
subject.perform(project.id, 'new_storage')
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