Commit 0539040d authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'extract_storage_move_creation' into 'master'

Extract storage move logic out of Project

See merge request gitlab-org/gitlab!32707
parents b954a022 ecaf50ec
...@@ -2073,21 +2073,6 @@ class Project < ApplicationRecord ...@@ -2073,21 +2073,6 @@ class Project < ApplicationRecord
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.key?(new_repository_storage_key)
storage_move = repository_storage_moves.create!(
source_storage_name: repository_storage,
destination_storage_name: new_repository_storage_key
)
storage_move.schedule!
self.repository_read_only = true
end
def pushes_since_gc def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end end
......
...@@ -18,6 +18,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -18,6 +18,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord
on: :create, on: :create,
presence: true, presence: true,
inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } } inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
validate :project_repository_writable, on: :create
state_machine initial: :initial do state_machine initial: :initial do
event :schedule do event :schedule do
...@@ -36,7 +37,9 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -36,7 +37,9 @@ class ProjectRepositoryStorageMove < ApplicationRecord
transition [:initial, :scheduled, :started] => :failed transition [:initial, :scheduled, :started] => :failed
end end
after_transition initial: :scheduled do |storage_move, _| after_transition initial: :scheduled do |storage_move|
storage_move.project.update_column(:repository_read_only, true)
storage_move.run_after_commit do storage_move.run_after_commit do
ProjectUpdateRepositoryStorageWorker.perform_async( ProjectUpdateRepositoryStorageWorker.perform_async(
storage_move.project_id, storage_move.project_id,
...@@ -46,6 +49,17 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -46,6 +49,17 @@ class ProjectRepositoryStorageMove < ApplicationRecord
end end
end end
after_transition started: :finished do |storage_move|
storage_move.project.update_columns(
repository_read_only: false,
repository_storage: storage_move.destination_storage_name
)
end
after_transition started: :failed do |storage_move|
storage_move.project.update_column(:repository_read_only, false)
end
state :initial, value: 1 state :initial, value: 1
state :scheduled, value: 2 state :scheduled, value: 2
state :started, value: 3 state :started, value: 3
...@@ -55,4 +69,10 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -55,4 +69,10 @@ class ProjectRepositoryStorageMove < ApplicationRecord
scope :order_created_at_desc, -> { order(created_at: :desc) } scope :order_created_at_desc, -> { order(created_at: :desc) }
scope :with_projects, -> { includes(project: :route) } scope :with_projects, -> { includes(project: :route) }
private
def project_repository_writable
errors.add(:project, _('is read only')) if project&.repository_read_only?
end
end end
...@@ -24,7 +24,7 @@ module Projects ...@@ -24,7 +24,7 @@ module Projects
mark_old_paths_for_archive mark_old_paths_for_archive
repository_storage_move.finish! repository_storage_move.finish!
project.update!(repository_storage: destination_storage_name, repository_read_only: false)
project.leave_pool_repository project.leave_pool_repository
project.track_project_repository project.track_project_repository
end end
...@@ -34,10 +34,7 @@ module Projects ...@@ -34,10 +34,7 @@ module Projects
ServiceResponse.success ServiceResponse.success
rescue StandardError => e rescue StandardError => e
project.transaction do
repository_storage_move.do_fail! repository_storage_move.do_fail!
project.update!(repository_read_only: false)
end
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path) Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path)
......
...@@ -13,8 +13,12 @@ module Projects ...@@ -13,8 +13,12 @@ module Projects
ensure_wiki_exists if enabling_wiki? ensure_wiki_exists if enabling_wiki?
if changing_storage_size? if changing_repository_storage?
project.change_repository_storage(params.delete(:repository_storage)) storage_move = project.repository_storage_moves.build(
source_storage_name: project.repository_storage,
destination_storage_name: params.delete(:repository_storage)
)
storage_move.schedule
end end
yield if block_given? yield if block_given?
...@@ -145,10 +149,11 @@ module Projects ...@@ -145,10 +149,11 @@ module Projects
project.previous_changes.include?(:pages_https_only) project.previous_changes.include?(:pages_https_only)
end end
def changing_storage_size? def changing_repository_storage?
new_repository_storage = params[:repository_storage] new_repository_storage = params[:repository_storage]
new_repository_storage && project.repository.exists? && new_repository_storage && project.repository.exists? &&
project.repository_storage != new_repository_storage &&
can?(current_user, :change_repository_storage, project) can?(current_user, :change_repository_storage, project)
end end
end end
......
...@@ -26101,6 +26101,9 @@ msgstr "" ...@@ -26101,6 +26101,9 @@ msgstr ""
msgid "is not in the group enforcing Group Managed Account" msgid "is not in the group enforcing Group Managed Account"
msgstr "" msgstr ""
msgid "is read only"
msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}." msgid "is too long (%{current_value}). The maximum size is %{max_size}."
msgstr "" msgstr ""
......
...@@ -10,5 +10,9 @@ FactoryBot.define do ...@@ -10,5 +10,9 @@ FactoryBot.define do
trait :scheduled do trait :scheduled do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value } state { ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value }
end end
trait :started do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:started].value }
end
end end
end end
...@@ -30,25 +30,36 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do ...@@ -30,25 +30,36 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do
expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/) expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/)
end end
end end
end
describe 'state transitions' do context 'project repository read-only' do
using RSpec::Parameterized::TableSyntax subject { build(:project_repository_storage_move, project: project) }
context 'when in the default state' do let(:project) { build(:project, repository_read_only: true) }
subject(:storage_move) { create(:project_repository_storage_move, project: project, destination_storage_name: 'test_second_storage') }
it "does not allow the project to be read-only on create" do
expect(subject).not_to be_valid
expect(subject.errors[:project].first).to match(/is read only/)
end
end
end
describe 'state transitions' do
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end end
context 'when in the default state' do
subject(:storage_move) { create(:project_repository_storage_move, project: project, destination_storage_name: 'test_second_storage') }
context 'and transits to scheduled' do context 'and transits to scheduled' do
it 'triggers ProjectUpdateRepositoryStorageWorker' do it 'triggers ProjectUpdateRepositoryStorageWorker' do
expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', storage_move.id) expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', storage_move.id)
storage_move.schedule! storage_move.schedule!
expect(project).to be_repository_read_only
end end
end end
...@@ -59,5 +70,26 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do ...@@ -59,5 +70,26 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do
end end
end end
end end
context 'when started' do
subject(:storage_move) { create(:project_repository_storage_move, :started, project: project, destination_storage_name: 'test_second_storage') }
context 'and transits to finished' do
it 'sets the repository storage and marks the project as writable' do
storage_move.finish!
expect(project.repository_storage).to eq('test_second_storage')
expect(project).not_to be_repository_read_only
end
end
context 'and transits to failed' do
it 'marks the project as writable' do
storage_move.do_fail!
expect(project).not_to be_repository_read_only
end
end
end
end end
end end
...@@ -2837,48 +2837,6 @@ describe Project do ...@@ -2837,48 +2837,6 @@ describe Project do
end end
end end
describe '#change_repository_storage' do
let(:project) { create(:project, :repository) }
let(:read_only_project) { create(:project, :repository, repository_read_only: true) }
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the transfer of the repository to the new storage and locks the project' do
expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', anything)
project.change_repository_storage('test_second_storage')
project.save!
expect(project).to be_repository_read_only
expect(project.repository_storage_moves.last).to have_attributes(
source_storage_name: "default",
destination_storage_name: "test_second_storage"
)
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('test_second_storage')
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(project.repository_storage)
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('unknown') }.to raise_error(ArgumentError)
end
end
describe '#pushes_since_gc' do describe '#pushes_since_gc' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -2466,11 +2466,11 @@ describe API::Projects do ...@@ -2466,11 +2466,11 @@ describe API::Projects do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
it 'returns 500 when repository storage is unknown' do it 'returns 400 when repository storage is unknown' do
put(api("/projects/#{new_project.id}", admin), params: { repository_storage: unknown_storage }) put(api("/projects/#{new_project.id}", admin), params: { repository_storage: unknown_storage })
expect(response).to have_gitlab_http_status(:internal_server_error) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to match('ArgumentError') expect(json_response['message']['repository_storage_moves']).to eq(['is invalid'])
end end
it 'returns 200 when repository storage has changed' do it 'returns 200 when repository storage has changed' do
......
...@@ -16,7 +16,7 @@ describe Projects::UpdateRepositoryStorageService do ...@@ -16,7 +16,7 @@ describe Projects::UpdateRepositoryStorageService do
end end
context 'without wiki and design repository' do context 'without wiki and design repository' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: false) } let(:project) { create(:project, :repository, wiki_enabled: false) }
let(:destination) { 'test_second_storage' } let(:destination) { 'test_second_storage' }
let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) } let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
let!(:checksum) { project.repository.checksum } let!(:checksum) { project.repository.checksum }
...@@ -131,7 +131,7 @@ describe Projects::UpdateRepositoryStorageService do ...@@ -131,7 +131,7 @@ describe Projects::UpdateRepositoryStorageService do
context 'with wiki repository' do context 'with wiki repository' do
include_examples 'moves repository to another storage', 'wiki' do include_examples 'moves repository to another storage', 'wiki' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: true) } let(:project) { create(:project, :repository, wiki_enabled: true) }
let(:repository) { project.wiki.repository } let(:repository) { project.wiki.repository }
let(:destination) { 'test_second_storage' } let(:destination) { 'test_second_storage' }
let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) } let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
...@@ -144,7 +144,7 @@ describe Projects::UpdateRepositoryStorageService do ...@@ -144,7 +144,7 @@ describe Projects::UpdateRepositoryStorageService do
context 'with design repository' do context 'with design repository' do
include_examples 'moves repository to another storage', 'design' do include_examples 'moves repository to another storage', 'design' do
let(:project) { create(:project, :repository, repository_read_only: true) } let(:project) { create(:project, :repository) }
let(:repository) { project.design_repository } let(:repository) { project.design_repository }
let(:destination) { 'test_second_storage' } let(:destination) { 'test_second_storage' }
let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) } let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
......
...@@ -552,6 +552,63 @@ describe Projects::UpdateService do ...@@ -552,6 +552,63 @@ describe Projects::UpdateService do
end end
end end
end end
describe 'when changing repository_storage' do
let(:repository_read_only) { false }
let(:project) { create(:project, :repository, repository_read_only: repository_read_only) }
let(:opts) { { repository_storage: 'test_second_storage' } }
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
shared_examples 'the transfer was not scheduled' do
it 'does not schedule the transfer' do
expect do
update_project(project, user, opts)
end.not_to change(project.repository_storage_moves, :count)
end
end
context 'authenticated as admin' do
let(:user) { create(:admin) }
it 'schedules the transfer of the repository to the new storage and locks the project' do
update_project(project, admin, opts)
expect(project).to be_repository_read_only
expect(project.repository_storage_moves.last).to have_attributes(
state: ::ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value,
source_storage_name: 'default',
destination_storage_name: 'test_second_storage'
)
end
context 'the repository is read-only' do
let(:repository_read_only) { true }
it_behaves_like 'the transfer was not scheduled'
end
context 'the storage has not changed' do
let(:opts) { { repository_storage: 'default' } }
it_behaves_like 'the transfer was not scheduled'
end
context 'the storage does not exist' do
let(:opts) { { repository_storage: 'nonexistent' } }
it_behaves_like 'the transfer was not scheduled'
end
end
context 'authenticated as user' do
let(:user) { create(:user) }
it_behaves_like 'the transfer was not scheduled'
end
end
end end
describe '#run_auto_devops_pipeline?' do describe '#run_auto_devops_pipeline?' do
...@@ -611,25 +668,6 @@ describe Projects::UpdateService do ...@@ -611,25 +668,6 @@ describe Projects::UpdateService do
end end
end end
describe 'repository_storage' do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:opts) { { repository_storage: 'test_second_storage' } }
it 'calls the change repository storage method if the storage changed' do
expect(project).to receive(:change_repository_storage).with('test_second_storage')
update_project(project, admin, 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)
described_class.new(project, user, opts).execute described_class.new(project, user, opts).execute
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