diff options
Diffstat (limited to 'spec/lib/backup')
-rw-r--r-- | spec/lib/backup/gitaly_backup_spec.rb | 110 | ||||
-rw-r--r-- | spec/lib/backup/gitaly_rpc_backup_spec.rb | 153 | ||||
-rw-r--r-- | spec/lib/backup/repositories_spec.rb | 191 |
3 files changed, 313 insertions, 141 deletions
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb new file mode 100644 index 00000000000..13567ead842 --- /dev/null +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::GitalyBackup do + let(:progress) do + Tempfile.new('progress').tap do |progress| + progress.unlink + end + end + + after do + progress.close + end + + subject { described_class.new(progress) } + + context 'unknown' do + it 'fails to start unknown' do + expect { subject.start(:unknown) }.to raise_error(::Backup::Error, 'unknown backup type: unknown') + end + end + + context 'create' do + RSpec.shared_examples 'creates a repository backup' do + it 'creates repository bundles', :aggregate_failures do + # Add data to the wiki, design repositories, and snippets, so they will be included in the dump. + create(:wiki_page, container: project) + create(:design, :with_file, issue: create(:issue, project: project)) + project_snippet = create(:project_snippet, :repository, project: project) + personal_snippet = create(:personal_snippet, :repository, author: project.owner) + + subject.start(:create) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.wait + + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle')) + end + + it 'raises when the exit code not zero' do + expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false')) + + subject.start(:create) + expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1') + end + end + + context 'hashed storage' do + let_it_be(:project) { create(:project, :repository) } + + it_behaves_like 'creates a repository backup' + end + + context 'legacy storage' do + let_it_be(:project) { create(:project, :repository, :legacy_storage) } + + it_behaves_like 'creates a repository backup' + end + end + + context 'restore' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) } + let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) } + + def copy_bundle_to_backup_path(bundle_name, destination) + FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination))) + FileUtils.cp(Rails.root.join('spec/fixtures/lib/backup', bundle_name), File.join(Gitlab.config.backup.path, 'repositories', destination)) + end + + it 'restores from repository bundles', :aggregate_failures do + copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle') + copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle') + copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle') + copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle') + copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle') + + subject.start(:restore) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.wait + + collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + + expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) + expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) + expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1']) + end + + it 'raises when the exit code not zero' do + expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false')) + + subject.start(:restore) + expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1') + end + end +end diff --git a/spec/lib/backup/gitaly_rpc_backup_spec.rb b/spec/lib/backup/gitaly_rpc_backup_spec.rb new file mode 100644 index 00000000000..fb442f4a86f --- /dev/null +++ b/spec/lib/backup/gitaly_rpc_backup_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::GitalyRpcBackup do + let(:progress) { spy(:stdout) } + + subject { described_class.new(progress) } + + after do + # make sure we do not leave behind any backup files + FileUtils.rm_rf(File.join(Gitlab.config.backup.path, 'repositories')) + end + + context 'unknown' do + it 'fails to start unknown' do + expect { subject.start(:unknown) }.to raise_error(::Backup::Error, 'unknown backup type: unknown') + end + end + + context 'create' do + RSpec.shared_examples 'creates a repository backup' do + it 'creates repository bundles', :aggregate_failures do + # Add data to the wiki, design repositories, and snippets, so they will be included in the dump. + create(:wiki_page, container: project) + create(:design, :with_file, issue: create(:issue, project: project)) + project_snippet = create(:project_snippet, :repository, project: project) + personal_snippet = create(:personal_snippet, :repository, author: project.owner) + + subject.start(:create) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.wait + + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle')) + end + + context 'failure' do + before do + allow_next_instance_of(Repository) do |repository| + allow(repository).to receive(:bundle_to_disk) { raise 'Fail in tests' } + end + end + + it 'logs an appropriate message', :aggregate_failures do + subject.start(:create) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.wait + + expect(progress).to have_received(:puts).with("[Failed] backing up #{project.full_path} (#{project.disk_path})") + expect(progress).to have_received(:puts).with("Error Fail in tests") + end + end + end + + context 'hashed storage' do + let_it_be(:project) { create(:project, :repository) } + + it_behaves_like 'creates a repository backup' + end + + context 'legacy storage' do + let_it_be(:project) { create(:project, :repository, :legacy_storage) } + + it_behaves_like 'creates a repository backup' + end + end + + context 'restore' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) } + let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) } + + def copy_bundle_to_backup_path(bundle_name, destination) + FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination))) + FileUtils.cp(Rails.root.join('spec/fixtures/lib/backup', bundle_name), File.join(Gitlab.config.backup.path, 'repositories', destination)) + end + + it 'restores from repository bundles', :aggregate_failures do + copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle') + copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle') + copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle') + copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle') + copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle') + + subject.start(:restore) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.wait + + collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + + expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) + expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) + expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1']) + end + + it 'cleans existing repositories', :aggregate_failures do + expect_next_instance_of(DesignManagement::Repository) do |repository| + expect(repository).to receive(:remove) + end + + # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo + expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args| + full_path, container, kwargs = original_args + + repository = method.call(full_path, container, **kwargs) + + expect(repository).to receive(:remove) + + repository + end + + subject.start(:restore) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.enqueue(project, Gitlab::GlRepository::WIKI) + subject.enqueue(project, Gitlab::GlRepository::DESIGN) + subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET) + subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) + subject.wait + end + + context 'failure' do + before do + allow_next_instance_of(Repository) do |repository| + allow(repository).to receive(:create_repository) { raise 'Fail in tests' } + allow(repository).to receive(:create_from_bundle) { raise 'Fail in tests' } + end + end + + it 'logs an appropriate message', :aggregate_failures do + subject.start(:restore) + subject.enqueue(project, Gitlab::GlRepository::PROJECT) + subject.wait + + expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})") + expect(progress).to have_received(:puts).with("Error Fail in tests") + end + end + end +end diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index 7a8cc713e4f..d77b1e0f276 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -3,37 +3,28 @@ require 'spec_helper' RSpec.describe Backup::Repositories do - let(:progress) { StringIO.new } + let(:progress) { spy(:stdout) } + let(:strategy) { spy(:strategy) } - subject { described_class.new(progress) } - - before do - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:progress).and_return(progress) - end - end + subject { described_class.new(progress, strategy: strategy) } describe '#dump' do let_it_be(:projects) { create_list(:project, 5, :repository) } RSpec.shared_examples 'creates repository bundles' do - specify :aggregate_failures do - # Add data to the wiki, design repositories, and snippets, so they will be included in the dump. - create(:wiki_page, container: project) - create(:design, :with_file, issue: create(:issue, project: project)) + it 'calls enqueue for each repository type', :aggregate_failures do project_snippet = create(:project_snippet, :repository, project: project) personal_snippet = create(:personal_snippet, :repository, author: project.owner) subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki' + '.bundle')) - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design' + '.bundle')) - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle')) - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle')) + expect(strategy).to have_received(:start).with(:create) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:wait) end end @@ -53,16 +44,18 @@ RSpec.describe Backup::Repositories do it 'creates the expected number of threads' do expect(Thread).not_to receive(:new) + expect(strategy).to receive(:start).with(:create) projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original + expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT) end + expect(strategy).to receive(:wait) subject.dump(max_concurrency: 1, max_storage_concurrency: 1) end describe 'command failure' do - it 'dump_project raises an error' do - allow(subject).to receive(:dump_project).and_raise(IOError) + it 'enqueue_project raises an error' do + allow(strategy).to receive(:enqueue).with(anything, Gitlab::GlRepository::PROJECT).and_raise(IOError) expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(IOError) end @@ -100,9 +93,11 @@ RSpec.describe Backup::Repositories do .exactly(storage_keys.length * (max_storage_concurrency + 1)).times .and_call_original + expect(strategy).to receive(:start).with(:create) projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original + expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT) end + expect(strategy).to receive(:wait) subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) end @@ -112,17 +107,18 @@ RSpec.describe Backup::Repositories do .exactly(storage_keys.length * (max_storage_concurrency + 1)).times .and_call_original + expect(strategy).to receive(:start).with(:create) projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original + expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT) end + expect(strategy).to receive(:wait) subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency) end describe 'command failure' do - it 'dump_project raises an error' do - allow(subject).to receive(:dump_project) - .and_raise(IOError) + it 'enqueue_project raises an error' do + allow(strategy).to receive(:enqueue).and_raise(IOError) expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(IOError) end @@ -162,61 +158,16 @@ RSpec.describe Backup::Repositories do let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) } let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) } - let(:next_path_to_bundle) do - [ - Rails.root.join('spec/fixtures/lib/backup/project_repo.bundle'), - Rails.root.join('spec/fixtures/lib/backup/wiki_repo.bundle'), - Rails.root.join('spec/fixtures/lib/backup/design_repo.bundle'), - Rails.root.join('spec/fixtures/lib/backup/personal_snippet_repo.bundle'), - Rails.root.join('spec/fixtures/lib/backup/project_snippet_repo.bundle') - ].to_enum - end - - it 'restores repositories from bundles', :aggregate_failures do - allow_next_instance_of(described_class::BackupRestore) do |backup_restore| - allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next) - end - + it 'calls enqueue for each repository type', :aggregate_failures do subject.restore - collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } - - expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) - expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) - expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) - expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) - expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1']) - end - - describe 'command failure' do - before do - expect(Project).to receive(:find_each).and_yield(project) - - allow_next_instance_of(DesignManagement::Repository) do |repository| - allow(repository).to receive(:create_repository) { raise 'Fail in tests' } - end - allow_next_instance_of(Repository) do |repository| - allow(repository).to receive(:create_repository) { raise 'Fail in tests' } - end - end - - context 'hashed storage' do - it 'shows the appropriate error' do - subject.restore - - expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})") - end - end - - context 'legacy storage' do - let_it_be(:project) { create(:project, :legacy_storage) } - - it 'shows the appropriate error' do - subject.restore - - expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})") - end - end + expect(strategy).to have_received(:start).with(:restore) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:wait) end context 'restoring object pools' do @@ -242,78 +193,36 @@ RSpec.describe Backup::Repositories do end end - it 'cleans existing repositories' do - success_response = ServiceResponse.success(message: "Valid Snippet Repo") - allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(success_response) - - expect_next_instance_of(DesignManagement::Repository) do |repository| - expect(repository).to receive(:remove) - end - - # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo - expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args| - full_path, container, kwargs = original_args - - repository = method.call(full_path, container, **kwargs) - - expect(repository).to receive(:remove) - - repository - end - - subject.restore - end - - context 'restoring snippets' do + context 'cleanup snippets' do before do create(:snippet_repository, snippet: personal_snippet) create(:snippet_repository, snippet: project_snippet) - allow_next_instance_of(described_class::BackupRestore) do |backup_restore| - allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next) - end + error_response = ServiceResponse.error(message: "Repository has more than one branch") + allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response) end - context 'when the repository is valid' do - it 'restores the snippet repositories' do - subject.restore - - expect(personal_snippet.snippet_repository.persisted?).to be true - expect(personal_snippet.repository).to exist + it 'shows the appropriate error' do + subject.restore - expect(project_snippet.snippet_repository.persisted?).to be true - expect(project_snippet.repository).to exist - end + expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch") + expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch") end - context 'when repository is invalid' do - before do - error_response = ServiceResponse.error(message: "Repository has more than one branch") - allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response) - end - - it 'shows the appropriate error' do - subject.restore - - expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch") - expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch") - end - - it 'removes the snippets from the DB' do - expect { subject.restore }.to change(PersonalSnippet, :count).by(-1) - .and change(ProjectSnippet, :count).by(-1) - .and change(SnippetRepository, :count).by(-2) - end + it 'removes the snippets from the DB' do + expect { subject.restore }.to change(PersonalSnippet, :count).by(-1) + .and change(ProjectSnippet, :count).by(-1) + .and change(SnippetRepository, :count).by(-2) + end - it 'removes the repository from disk' do - gitlab_shell = Gitlab::Shell.new - shard_name = personal_snippet.repository.shard - path = personal_snippet.disk_path + '.git' + it 'removes the repository from disk' do + gitlab_shell = Gitlab::Shell.new + shard_name = personal_snippet.repository.shard + path = personal_snippet.disk_path + '.git' - subject.restore + subject.restore - expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false - end + expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false end end end |