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. Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased) v 8.11.0 (unreleased)
- Allow projects to be moved between repository storages
- Performance improvement of push rules - Performance improvement of push rules
- Change LdapGroupSyncWorker to use new LDAP group sync classes - Change LdapGroupSyncWorker to use new LDAP group sync classes
- [Elastic][Fix] Commit search breaks for some URLs on gitlab-ce project - [Elastic][Fix] Commit search breaks for some URLs on gitlab-ce project
......
...@@ -184,6 +184,8 @@ class Ability ...@@ -184,6 +184,8 @@ class Ability
# Push abilities on the users team role # Push abilities on the users team role
rules.push(*project_team_rules(project.team, user)) rules.push(*project_team_rules(project.team, user))
rules << :change_repository_storage if user.admin?
owner = user.admin? || owner = user.admin? ||
project.owner == user || project.owner == user ||
(project.group && project.group.has_owner?(user)) (project.group && project.group.has_owner?(user))
......
...@@ -1497,6 +1497,16 @@ class Project < ActiveRecord::Base ...@@ -1497,6 +1497,16 @@ class Project < ActiveRecord::Base
end end
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 private
def default_branch_protected? 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 ...@@ -13,13 +13,20 @@ module Projects
end end
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 if project.repository.exists?
project.change_head(new_branch) 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 end
if project.update_attributes(params.except(:default_branch)) if project.update_attributes(params)
if project.previous_changes.include?('path') if project.previous_changes.include?('path')
project.rename_repo project.rename_repo
end 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 ...@@ -969,6 +969,7 @@ ActiveRecord::Schema.define(version: 20160810153405) do
t.string "repository_storage", default: "default", null: false t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: true, null: false t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki" t.boolean "has_external_wiki"
t.boolean "repository_read_only"
end end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree 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: ...@@ -280,7 +280,8 @@ Parameters:
"group_name": "Gitlab Org", "group_name": "Gitlab Org",
"group_access_level": 10 "group_access_level": 10
} }
] ],
"repository_storage": "default"
} }
``` ```
...@@ -448,6 +449,7 @@ Parameters: ...@@ -448,6 +449,7 @@ Parameters:
- `visibility_level` (optional) - `visibility_level` (optional)
- `import_url` (optional) - `import_url` (optional)
- `public_builds` (optional) - `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
### Create project for user ### Create project for user
...@@ -473,6 +475,7 @@ Parameters: ...@@ -473,6 +475,7 @@ Parameters:
- `visibility_level` (optional) - `visibility_level` (optional)
- `import_url` (optional) - `import_url` (optional)
- `public_builds` (optional) - `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
### Edit project ### Edit project
...@@ -499,6 +502,7 @@ Parameters: ...@@ -499,6 +502,7 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20 - `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional) - `visibility_level` (optional)
- `public_builds` (optional) - `public_builds` (optional)
- `repository_storage` (optional, available only for admins)
On success, method returns 200 with the updated project. If parameters are On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned. invalid, 400 is returned.
......
...@@ -94,6 +94,7 @@ module API ...@@ -94,6 +94,7 @@ module API
expose :shared_with_groups do |project, options| expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options) SharedGroup.represent(project.project_group_links.all, options)
end end
expose :repository_storage, if: lambda { |_project, options| options[:user].try(:admin?) }
end end
class ProjectMember < UserBasic class ProjectMember < UserBasic
......
...@@ -105,6 +105,7 @@ module API ...@@ -105,6 +105,7 @@ module API
# visibility_level (optional) - 0 by default # visibility_level (optional) - 0 by default
# import_url (optional) # import_url (optional)
# public_builds (optional) # public_builds (optional)
# repository_storage (optional)
# Example Request # Example Request
# POST /projects # POST /projects
post do post do
...@@ -123,7 +124,8 @@ module API ...@@ -123,7 +124,8 @@ module API
:public, :public,
:visibility_level, :visibility_level,
:import_url, :import_url,
:public_builds] :public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute @project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved? if @project.saved?
...@@ -155,6 +157,7 @@ module API ...@@ -155,6 +157,7 @@ module API
# visibility_level (optional) # visibility_level (optional)
# import_url (optional) # import_url (optional)
# public_builds (optional) # public_builds (optional)
# repository_storage (optional)
# Example Request # Example Request
# POST /projects/user/:user_id # POST /projects/user/:user_id
post "user/:user_id" do post "user/:user_id" do
...@@ -172,7 +175,8 @@ module API ...@@ -172,7 +175,8 @@ module API
:public, :public,
:visibility_level, :visibility_level,
:import_url, :import_url,
:public_builds] :public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute @project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved? if @project.saved?
...@@ -218,6 +222,7 @@ module API ...@@ -218,6 +222,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20 # public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project # visibility_level (optional) - visibility level of a project
# public_builds (optional) # public_builds (optional)
# repository_storage (optional)
# Example Request # Example Request
# PUT /projects/:id # PUT /projects/:id
put ':id' do put ':id' do
...@@ -234,7 +239,8 @@ module API ...@@ -234,7 +239,8 @@ module API
:shared_runners_enabled, :shared_runners_enabled,
:public, :public,
:visibility_level, :visibility_level,
:public_builds] :public_builds,
:repository_storage]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
authorize_admin_project authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present? authorize! :rename_project, user_project if attrs[:name].present?
......
...@@ -106,6 +106,20 @@ module Gitlab ...@@ -106,6 +106,20 @@ module Gitlab
storage, "#{path}.git", "#{new_path}.git"]) storage, "#{path}.git", "#{new_path}.git"])
end 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 # Fork repository to new namespace
# forked_from_storage - forked-from project's storage path # forked_from_storage - forked-from project's storage path
# path - project path with namespace # path - project path with namespace
......
...@@ -59,6 +59,10 @@ module Gitlab ...@@ -59,6 +59,10 @@ module Gitlab
end end
def push_access_check(changes) 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? if Gitlab::Geo.secondary?
return build_status_object(false, "You can't push code on a secondary GitLab Geo node.") return build_status_object(false, "You can't push code on a secondary GitLab Geo node.")
end end
......
...@@ -27,7 +27,7 @@ namespace :gitlab do ...@@ -27,7 +27,7 @@ namespace :gitlab do
all_dirs.each do |dir_path| all_dirs.each do |dir_path|
if remove_flag if remove_flag
if FileUtils.rm_rf dir_path if FileUtils.rm_rf(dir_path)
puts "Removed...#{dir_path}".color(:red) puts "Removed...#{dir_path}".color(:red)
else else
puts "Cannot remove #{dir_path}".color(:red) puts "Cannot remove #{dir_path}".color(:red)
......
...@@ -44,6 +44,10 @@ FactoryGirl.define do ...@@ -44,6 +44,10 @@ FactoryGirl.define do
project.create_repository project.create_repository
end end
end end
trait :read_only_repository do
repository_read_only true
end
end end
# Project with empty repository # Project with empty repository
......
...@@ -44,6 +44,30 @@ describe Gitlab::Shell, lib: true do ...@@ -44,6 +44,30 @@ describe Gitlab::Shell, lib: true do
end end
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 Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do describe '#add_key' do
it 'normalizes space characters in the key' do it 'normalizes space characters in the key' do
......
...@@ -555,4 +555,15 @@ describe Gitlab::GitAccess, lib: true do ...@@ -555,4 +555,15 @@ describe Gitlab::GitAccess, lib: true do
end end
end 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 end
...@@ -1580,4 +1580,51 @@ describe Project, models: true do ...@@ -1580,4 +1580,51 @@ describe Project, models: true do
expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
end end
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 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 ...@@ -139,6 +139,38 @@ describe Projects::UpdateService, services: true do
end end
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) def update_project(project, user, opts)
Projects::UpdateService.new(project, user, opts).execute Projects::UpdateService.new(project, user, opts).execute
end 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