diff options
Diffstat (limited to 'spec/lib/backup')
-rw-r--r-- | spec/lib/backup/files_spec.rb | 390 | ||||
-rw-r--r-- | spec/lib/backup/gitaly_backup_spec.rb | 12 | ||||
-rw-r--r-- | spec/lib/backup/manager_spec.rb | 188 | ||||
-rw-r--r-- | spec/lib/backup/options_spec.rb | 16 | ||||
-rw-r--r-- | spec/lib/backup/targets/database_spec.rb (renamed from spec/lib/backup/database_spec.rb) | 46 | ||||
-rw-r--r-- | spec/lib/backup/targets/files_spec.rb | 403 | ||||
-rw-r--r-- | spec/lib/backup/targets/repositories_spec.rb (renamed from spec/lib/backup/repositories_spec.rb) | 126 | ||||
-rw-r--r-- | spec/lib/backup/targets/target_spec.rb | 36 | ||||
-rw-r--r-- | spec/lib/backup/task_spec.rb | 22 |
9 files changed, 657 insertions, 582 deletions
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb deleted file mode 100644 index 3c96628b4cf..00000000000 --- a/spec/lib/backup/files_spec.rb +++ /dev/null @@ -1,390 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Backup::Files, feature_category: :backup_restore do - let(:progress) { StringIO.new } - let!(:project) { create(:project) } - let(:backup_options) { Backup::Options.new } - - let(:status_0) { double('exit 0', success?: true, exitstatus: 0) } - let(:status_1) { double('exit 1', success?: false, exitstatus: 1) } - let(:status_2) { double('exit 2', success?: false, exitstatus: 2) } - - before do - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - allow(FileUtils).to receive(:mkdir_p).and_return(true) - allow(FileUtils).to receive(:mv).and_return(true) - allow(File).to receive(:exist?).and_return(true) - allow(File).to receive(:realpath).with("/var/gitlab-registry").and_return("/var/gitlab-registry") - allow(File).to receive(:realpath).with("/var/gitlab-registry/..").and_return("/var") - allow(File).to receive(:realpath).with("/var/gitlab-pages").and_return("/var/gitlab-pages") - allow(File).to receive(:realpath).with("/var/gitlab-pages/..").and_return("/var") - - allow_any_instance_of(described_class).to receive(:progress).and_return(progress) - end - - RSpec::Matchers.define :eq_statuslist do |expected| - match do |actual| - actual.map(&:exitstatus) == expected.map(&:exitstatus) - end - - description do - 'be an Array of Process::Status with equal exitstatus against expected' - end - - failure_message do |actual| - "expected #{actual} exitstatuses list to be equal #{expected} exitstatuses list" - end - end - - describe '#restore' do - subject { described_class.new(progress, '/var/gitlab-registry', options: backup_options) } - - let(:timestamp) { Time.utc(2017, 3, 22) } - - around do |example| - travel_to(timestamp) { example.run } - end - - describe 'folders with permission' do - before do - allow(subject).to receive(:run_pipeline!).and_return([[true, true], '']) - allow(subject).to receive(:backup_existing_files).and_return(true) - allow(subject).to receive(:pipeline_succeeded?).and_return(true) - allow(Dir).to receive(:glob).with("/var/gitlab-registry/*", File::FNM_DOTMATCH).and_return(["/var/gitlab-registry/.", "/var/gitlab-registry/..", "/var/gitlab-registry/sample1"]) - end - - it 'moves all necessary files' do - allow(subject).to receive(:backup_existing_files).and_call_original - expect(FileUtils).to receive(:mv).with(["/var/gitlab-registry/sample1"], File.join(Gitlab.config.backup.path, "tmp", "registry.#{Time.now.to_i}")) - subject.restore('registry.tar.gz', 'backup_id') - end - - it 'raises no errors' do - expect { subject.restore('registry.tar.gz', 'backup_id') }.not_to raise_error - end - - it 'calls tar command with unlink' do - expect(subject).to receive(:tar).and_return('blabla-tar') - - expect(subject).to receive(:run_pipeline!).with(["gzip -cd", %w[blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -]], any_args) - expect(subject).to receive(:pipeline_succeeded?).and_return(true) - subject.restore('registry.tar.gz', 'backup_id') - end - - it 'raises an error on failure' do - expect(subject).to receive(:pipeline_succeeded?).and_return(false) - - expect { subject.restore('registry.tar.gz', 'backup_id') }.to raise_error(/Restore operation failed:/) - end - end - - describe 'folders without permissions' do - before do - allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) - allow(subject).to receive(:run_pipeline!).and_return([[true, true], '']) - allow(subject).to receive(:pipeline_succeeded?).and_return(true) - end - - it 'shows error message' do - expect(subject).to receive(:access_denied_error).with("/var/gitlab-registry") - subject.restore('registry.tar.gz', 'backup_id') - end - end - - describe 'folders that are a mountpoint' do - before do - allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY) - allow(subject).to receive(:run_pipeline!).and_return([[true, true], '']) - allow(subject).to receive(:pipeline_succeeded?).and_return(true) - end - - it 'shows error message' do - expect(subject).to receive(:resource_busy_error).with("/var/gitlab-registry") - .and_call_original - - expect { subject.restore('registry.tar.gz', 'backup_id') }.to raise_error(/is a mountpoint/) - end - end - - describe 'with DECOMPRESS_CMD' do - before do - stub_env('DECOMPRESS_CMD', 'tee') - allow(subject).to receive(:pipeline_succeeded?).and_return(true) - end - - it 'passes through tee instead of gzip' do - expect(subject).to receive(:run_pipeline!).with(['tee', anything], any_args).and_return([[true, true], '']) - - expect do - subject.restore('registry.tar.gz', 'backup_id') - end.to output(/Using custom DECOMPRESS_CMD 'tee'/).to_stdout - end - end - end - - describe '#dump' do - subject do - described_class.new(progress, '/var/gitlab-pages', excludes: ['@pages.tmp'], options: backup_options) - end - - before do - allow(subject).to receive(:run_pipeline!).and_return([[true, true], '']) - allow(subject).to receive(:pipeline_succeeded?).and_return(true) - end - - it 'raises no errors' do - expect { subject.dump('registry.tar.gz', 'backup_id') }.not_to raise_error - end - - it 'excludes tmp dirs from archive' do - expect(subject).to receive(:tar).and_return('blabla-tar') - - expect(subject).to receive(:run_pipeline!).with([%w[blabla-tar --exclude=lost+found --exclude=./@pages.tmp -C /var/gitlab-pages -cf - .], 'gzip -c -1'], any_args) - subject.dump('registry.tar.gz', 'backup_id') - end - - it 'raises an error on failure' do - allow(subject).to receive(:run_pipeline!).and_return([[true, true], '']) - expect(subject).to receive(:pipeline_succeeded?).and_return(false) - - expect do - subject.dump('registry.tar.gz', 'backup_id') - end.to raise_error(/Failed to create compressed file/) - end - - describe 'with STRATEGY=copy' do - before do - stub_env('STRATEGY', 'copy') - allow(Gitlab.config.backup).to receive(:path) { '/var/gitlab-backup' } - allow(File).to receive(:realpath).with("/var/gitlab-backup").and_return("/var/gitlab-backup") - end - - it 'excludes tmp dirs from rsync' do - expect(Gitlab::Popen).to receive(:popen) - .with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup]) - .and_return(['', 0]) - - subject.dump('registry.tar.gz', 'backup_id') - end - - it 'retries if rsync fails due to vanishing files' do - expect(Gitlab::Popen).to receive(:popen) - .with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup]) - .and_return(['rsync failed', 24], ['', 0]) - - expect do - subject.dump('registry.tar.gz', 'backup_id') - end.to output(/files vanished during rsync, retrying/).to_stdout - end - - it 'raises an error and outputs an error message if rsync failed' do - allow(Gitlab::Popen).to receive(:popen) - .with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup]) - .and_return(['rsync failed', 1]) - - expect do - subject.dump('registry.tar.gz', 'backup_id') - end.to output(/rsync failed/).to_stdout - .and raise_error(/Failed to create compressed file/) - end - end - - describe 'with COMPRESS_CMD' do - before do - stub_env('COMPRESS_CMD', 'tee') - end - - it 'passes through tee instead of gzip' do - expect(subject).to receive(:run_pipeline!).with([anything, 'tee'], any_args) - expect do - subject.dump('registry.tar.gz', 'backup_id') - end.to output(/Using custom COMPRESS_CMD 'tee'/).to_stdout - end - end - - context 'when GZIP_RSYNCABLE is "yes"' do - before do - stub_env('GZIP_RSYNCABLE', 'yes') - end - - it 'gzips the files with rsyncable option' do - expect(subject).to receive(:run_pipeline!).with([anything, 'gzip --rsyncable -c -1'], any_args) - subject.dump('registry.tar.gz', 'backup_id') - end - end - - context 'when GZIP_RSYNCABLE is not set' do - it 'gzips the files without the rsyncable option' do - expect(subject).to receive(:run_pipeline!).with([anything, 'gzip -c -1'], any_args) - subject.dump('registry.tar.gz', 'backup_id') - end - end - end - - describe '#exclude_dirs' do - subject do - described_class.new(progress, '/var/gitlab-pages', excludes: ['@pages.tmp'], options: backup_options) - end - - it 'prepends a leading dot slash to tar excludes' do - expect(subject.exclude_dirs(:tar)).to eq(['--exclude=lost+found', '--exclude=./@pages.tmp']) - end - - it 'prepends a leading slash and app_files_dir basename to rsync excludes' do - expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/gitlab-pages/@pages.tmp']) - end - end - - describe '#run_pipeline!' do - subject do - described_class.new(progress, '/var/gitlab-registry', options: backup_options) - end - - it 'executes an Open3.pipeline for cmd_list' do - expect(Open3).to receive(:pipeline).with(%w[whew command], %w[another cmd], any_args) - - subject.run_pipeline!([%w[whew command], %w[another cmd]]) - end - - it 'returns an empty output on success pipeline' do - expect(subject.run_pipeline!(%w[true true])[1]).to eq('') - end - - it 'returns the stderr for failed pipeline' do - expect( - subject.run_pipeline!(['echo OMG: failed command present 1>&2; false', 'true'])[1] - ).to match(/OMG: failed/) - end - - it 'returns the success status list on success pipeline' do - expect( - subject.run_pipeline!(%w[true true])[0] - ).to eq_statuslist([status_0, status_0]) - end - - it 'returns the failed status in status list for failed commands in pipeline' do - expect(subject.run_pipeline!(%w[false true true])[0]).to eq_statuslist([status_1, status_0, status_0]) - expect(subject.run_pipeline!(%w[true false true])[0]).to eq_statuslist([status_0, status_1, status_0]) - expect(subject.run_pipeline!(%w[false false true])[0]).to eq_statuslist([status_1, status_1, status_0]) - expect(subject.run_pipeline!(%w[false true false])[0]).to eq_statuslist([status_1, status_0, status_1]) - expect(subject.run_pipeline!(%w[false false false])[0]).to eq_statuslist([status_1, status_1, status_1]) - end - end - - describe '#pipeline_succeeded?' do - subject do - described_class.new(progress, '/var/gitlab-registry', options: backup_options) - end - - it 'returns true if both tar and gzip succeeeded' do - expect( - subject.pipeline_succeeded?(tar_status: status_0, compress_status: status_0, output: 'any_output') - ).to be_truthy - end - - it 'returns false if gzip failed' do - expect( - subject.pipeline_succeeded?(tar_status: status_1, compress_status: status_1, output: 'any_output') - ).to be_falsey - end - - context 'if gzip succeeded and tar failed non-critically' do - before do - allow(subject).to receive(:tar_ignore_non_success?).and_return(true) - end - - it 'returns true' do - expect( - subject.pipeline_succeeded?(tar_status: status_1, compress_status: status_0, output: 'any_output') - ).to be_truthy - end - end - - context 'if gzip succeeded and tar failed in other cases' do - before do - allow(subject).to receive(:tar_ignore_non_success?).and_return(false) - end - - it 'returns false' do - expect( - subject.pipeline_succeeded?(tar_status: status_1, compress_status: status_0, output: 'any_output') - ).to be_falsey - end - end - end - - describe '#tar_ignore_non_success?' do - subject do - described_class.new(progress, '/var/gitlab-registry', options: backup_options) - end - - context 'if `tar` command exits with 1 exitstatus' do - it 'returns true' do - expect( - subject.tar_ignore_non_success?(1, 'any_output') - ).to be_truthy - end - - it 'outputs a warning' do - expect do - subject.tar_ignore_non_success?(1, 'any_output') - end.to output(/Ignoring tar exit status 1/).to_stdout - end - end - - context 'if `tar` command exits with 2 exitstatus with non-critical warning' do - before do - allow(subject).to receive(:noncritical_warning?).and_return(true) - end - - it 'returns true' do - expect( - subject.tar_ignore_non_success?(2, 'any_output') - ).to be_truthy - end - - it 'outputs a warning' do - expect do - subject.tar_ignore_non_success?(2, 'any_output') - end.to output(/Ignoring non-success exit status/).to_stdout - end - end - - context 'if `tar` command exits with any other unlisted error' do - before do - allow(subject).to receive(:noncritical_warning?).and_return(false) - end - - it 'returns false' do - expect( - subject.tar_ignore_non_success?(2, 'any_output') - ).to be_falsey - end - end - end - - describe '#noncritical_warning?' do - subject do - described_class.new(progress, '/var/gitlab-registry', options: backup_options) - end - - it 'returns true if given text matches noncritical warnings list' do - expect( - subject.noncritical_warning?('tar: .: Cannot mkdir: No such file or directory') - ).to be_truthy - - expect( - subject.noncritical_warning?('gtar: .: Cannot mkdir: No such file or directory') - ).to be_truthy - end - - it 'returns false otherwize' do - expect( - subject.noncritical_warning?('unknown message') - ).to be_falsey - end - end -end diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index 058c7f12f63..f063462929d 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -10,9 +10,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do let(:server_side) { false } let(:progress) do - Tempfile.new('progress').tap do |progress| - progress.unlink - end + Tempfile.new('progress').tap(&:unlink) end let(:expected_env) do @@ -62,8 +60,8 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do subject.finish! expect(File).to exist(File.join(destination, project.disk_path, backup_id, '001.bundle')) - expect(File).to exist(File.join(destination, project.disk_path + '.wiki', backup_id, '001.bundle')) - expect(File).to exist(File.join(destination, project.disk_path + '.design', backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, "#{project.disk_path}.wiki", backup_id, '001.bundle')) + expect(File).to exist(File.join(destination, "#{project.disk_path}.design", backup_id, '001.bundle')) expect(File).to exist(File.join(destination, personal_snippet.disk_path, backup_id, '001.bundle')) expect(File).to exist(File.join(destination, project_snippet.disk_path, backup_id, '001.bundle')) end @@ -189,7 +187,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do custom_hooks_path = '#{repo.relative_path}.custom_hooks.tar' TOML - File.write(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path, backup_id + '.toml'), manifest) + File.write(File.join(repo_backup_root, 'manifests', repo.storage, repo.relative_path, "#{backup_id}.toml"), manifest) end it 'restores from repository bundles', :aggregate_failures do @@ -209,7 +207,7 @@ RSpec.describe Backup::GitalyBackup, feature_category: :backup_restore do subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET) subject.finish! - collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + collect_commit_shas = ->(repo) { repo.commits('master', limit: 10).map(&:sha) } expect(collect_commit_shas.call(project.repository)).to match_array(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) expect(collect_commit_shas.call(project.wiki.repository)).to match_array(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 7a8cffe8529..99a484c7a6f 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do let(:progress) { StringIO.new } let(:definitions) { nil } + let(:options) { build(:backup_options, :skip_none) } subject { described_class.new(progress, definitions: definitions) } @@ -22,32 +23,29 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do end describe '#run_create_task' do - let(:enabled) { true } - let(:task) { instance_double(Backup::Task) } + let(:terraform_state) do + Backup::Tasks::TerraformState.new(progress: progress, options: options) + .tap { |state| allow(state).to receive(:target).and_return(target) } + end + + let(:target) { instance_double(Backup::Targets::Target) } let(:definitions) do - { - 'terraform_state' => Backup::Manager::TaskDefinition.new( - task: task, - enabled: enabled, - destination_path: 'terraform_state.tar.gz', - human_name: 'terraform state' - ) - } + { 'terraform_state' => terraform_state } end it 'calls the named task' do - expect(task).to receive(:dump) - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform state ... ') - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform state ... done') + expect(target).to receive(:dump) + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... ') + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... done') subject.run_create_task('terraform_state') end describe 'disabled' do - let(:enabled) { false } - it 'informs the user' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform state ... [DISABLED]') + allow(terraform_state).to receive(:enabled).and_return(false) + + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... [DISABLED]') subject.run_create_task('terraform_state') end @@ -57,7 +55,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do it 'informs the user' do stub_env('SKIP', 'terraform_state') - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform state ... [SKIPPED]') + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... [SKIPPED]') subject.run_create_task('terraform_state') end @@ -65,17 +63,22 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do end describe '#run_restore_task' do - let(:enabled) { true } - let(:pre_restore_warning) { nil } - let(:post_restore_warning) { nil } - let(:definitions) { { 'terraform_state' => Backup::Manager::TaskDefinition.new(task: task, enabled: enabled, human_name: 'terraform state', destination_path: 'terraform_state.tar.gz') } } - let(:backup_information) { { backup_created_at: Time.zone.parse('2019-01-01'), gitlab_version: '12.3' } } - let(:task) do - instance_double(Backup::Task, - pre_restore_warning: pre_restore_warning, - post_restore_warning: post_restore_warning) + let(:terraform_state) do + Backup::Tasks::TerraformState.new(progress: progress, options: options) + .tap { |task| allow(task).to receive(:target).and_return(target) } end + let(:pre_restore_warning) { '' } + let(:post_restore_warning) { '' } + let(:target) do + instance_double(::Backup::Targets::Target, + pre_restore_warning: pre_restore_warning, + post_restore_warning: post_restore_warning) + end + + let(:definitions) { { 'terraform_state' => terraform_state } } + let(:backup_information) { { backup_created_at: Time.zone.parse('2019-01-01'), gitlab_version: '12.3' } } + before do allow_next_instance_of(Backup::Metadata) do |metadata| allow(metadata).to receive(:load_from_file).and_return(backup_information) @@ -83,18 +86,17 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do end it 'calls the named task' do - expect(task).to receive(:restore) - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... ').ordered - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... done').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... done').ordered + expect(target).to receive(:restore) subject.run_restore_task('terraform_state') end describe 'disabled' do - let(:enabled) { false } - it 'informs the user' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... [DISABLED]').ordered + allow(terraform_state).to receive(:enabled).and_return(false) + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... [DISABLED]').ordered subject.run_restore_task('terraform_state') end @@ -104,17 +106,17 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do let(:pre_restore_warning) { 'Watch out!' } it 'displays and waits for the user' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... ').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Watch out!').ordered - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... done').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... done').ordered expect(Gitlab::TaskHelpers).to receive(:ask_to_continue) - expect(task).to receive(:restore) + expect(target).to receive(:restore) subject.run_restore_task('terraform_state') end it 'does not continue when the user quits' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... ').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Watch out!').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Quitting...').ordered expect(Gitlab::TaskHelpers).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError) @@ -129,21 +131,21 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do let(:post_restore_warning) { 'Watch out!' } it 'displays and waits for the user' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... ').ordered - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... done').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... done').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Watch out!').ordered expect(Gitlab::TaskHelpers).to receive(:ask_to_continue) - expect(task).to receive(:restore) + expect(target).to receive(:restore) subject.run_restore_task('terraform_state') end it 'does not continue when the user quits' do - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... ').ordered - expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform state ... done').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered + expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... done').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Watch out!').ordered expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Quitting...').ordered - expect(task).to receive(:restore) + expect(target).to receive(:restore) expect(Gitlab::TaskHelpers).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError) expect do @@ -163,13 +165,20 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do let(:pack_tar_system_options) { { out: [pack_tar_file, 'w', Gitlab.config.backup.archive_permissions] } } let(:pack_tar_cmdline) { ['tar', '-cf', '-', *expected_backup_contents, pack_tar_system_options] } - let(:task1) { instance_double(Backup::Task) } - let(:task2) { instance_double(Backup::Task) } + let(:lfs) do + Backup::Tasks::Lfs.new(progress: progress, options: options) + .tap { |task| allow(task).to receive(:target).and_return(target1) } + end + + let(:pages) do + Backup::Tasks::Pages.new(progress: progress, options: options) + .tap { |task| allow(task).to receive(:target).and_return(target2) } + end + + let(:target1) { instance_double(Backup::Targets::Target) } + let(:target2) { instance_double(Backup::Targets::Target) } let(:definitions) do - { - 'lfs' => Backup::Manager::TaskDefinition.new(task: task1, human_name: 'lfs objects', destination_path: 'lfs.tar.gz'), - 'pages' => Backup::Manager::TaskDefinition.new(task: task2, human_name: 'pages', destination_path: 'pages.tar.gz') - } + { 'lfs' => lfs, 'pages' => pages } end before do @@ -178,8 +187,8 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do allow(Gitlab::BackupLogger).to receive(:info) allow(Kernel).to receive(:system).and_return(true) - allow(task1).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) - allow(task2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) + allow(target1).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) + allow(target2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) end it 'creates a backup tar' do @@ -237,11 +246,11 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do context 'when the destination is optional' do let(:expected_backup_contents) { %w[backup_information.yml lfs.tar.gz] } - let(:definitions) do - { - 'lfs' => Backup::Manager::TaskDefinition.new(task: task1, destination_path: 'lfs.tar.gz'), - 'pages' => Backup::Manager::TaskDefinition.new(task: task2, destination_path: 'pages.tar.gz', destination_optional: true) - } + let(:pages) do + Backup::Tasks::Pages.new(progress: progress, options: options) + .tap do |task| + allow(task).to receive_messages(target: target2, destination_optional: true) + end end it 'executes tar' do @@ -255,17 +264,17 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do context 'many backup files' do let(:files) do - [ - '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar', - '1451510000_2015_12_30_gitlab_backup.tar', - '1450742400_2015_12_22_gitlab_backup.tar', - '1449878400_gitlab_backup.tar', - '1449014400_gitlab_backup.tar', - 'manual_gitlab_backup.tar' + %w[ + 1451606400_2016_01_01_1.2.3_gitlab_backup.tar + 1451520000_2015_12_31_4.5.6_gitlab_backup.tar + 1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar + 1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar + 1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar + 1451510000_2015_12_30_gitlab_backup.tar + 1450742400_2015_12_22_gitlab_backup.tar + 1449878400_gitlab_backup.tar + 1449014400_gitlab_backup.tar + manual_gitlab_backup.tar ] end @@ -295,10 +304,10 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do context 'when no valid file is found' do let(:files) do - [ - '14516064000_2016_01_01_1.2.3_gitlab_backup.tar', - 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar' + %w[ + 14516064000_2016_01_01_1.2.3_gitlab_backup.tar + foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar + 1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar ] end @@ -654,9 +663,9 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do context 'when there are two backup files in the directory and BACKUP variable is not set' do before do allow(Dir).to receive(:glob).and_return( - [ - '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', - '1451520000_2015_12_31_gitlab_backup.tar' + %w[ + 1451606400_2016_01_01_1.2.3_gitlab_backup.tar + 1451520000_2015_12_31_gitlab_backup.tar ] ) end @@ -923,13 +932,20 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do end describe '#restore' do - let(:task1) { instance_double(Backup::Task, pre_restore_warning: nil, post_restore_warning: nil) } - let(:task2) { instance_double(Backup::Task, pre_restore_warning: nil, post_restore_warning: nil) } + let(:lfs) do + Backup::Tasks::Lfs.new(progress: progress, options: options) + .tap { |task| allow(task).to receive(:target).and_return(target1) } + end + + let(:pages) do + Backup::Tasks::Pages.new(progress: progress, options: options) + .tap { |task| allow(task).to receive(:target).and_return(target2) } + end + + let(:target1) { instance_double(Backup::Targets::Target, pre_restore_warning: nil, post_restore_warning: nil) } + let(:target2) { instance_double(Backup::Targets::Target, pre_restore_warning: nil, post_restore_warning: nil) } let(:definitions) do - { - 'lfs' => Backup::Manager::TaskDefinition.new(task: task1, human_name: 'lfs content', destination_path: 'lfs.tar.gz'), - 'pages' => Backup::Manager::TaskDefinition.new(task: task2, human_name: 'pages', destination_path: 'pages.tar.gz') - } + { 'lfs' => lfs, 'pages' => pages } end let(:gitlab_version) { Gitlab::VERSION } @@ -947,8 +963,8 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do Rake.application.rake_require 'tasks/cache' allow(Gitlab::BackupLogger).to receive(:info) - allow(task1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) - allow(task2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) + allow(target1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) + allow(target2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) allow_next_instance_of(Backup::Metadata) do |metadata| allow(metadata).to receive(:load_from_file).and_return(backup_information) end @@ -971,9 +987,9 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do context 'when there are two backup files in the directory and BACKUP variable is not set' do before do allow(Dir).to receive(:glob).and_return( - [ - '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', - '1451520000_2015_12_31_gitlab_backup.tar' + %w[ + 1451606400_2016_01_01_1.2.3_gitlab_backup.tar + 1451520000_2015_12_31_gitlab_backup.tar ] ) end @@ -1047,8 +1063,8 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do end it 'unpacks the BACKUP specified file but uses the backup information backup ID' do - expect(task1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) - expect(task2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) + expect(target1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'lfs.tar.gz'), backup_id) + expect(target2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'pages.tar.gz'), backup_id) subject.restore diff --git a/spec/lib/backup/options_spec.rb b/spec/lib/backup/options_spec.rb index 970eea134dd..0ef10079be0 100644 --- a/spec/lib/backup/options_spec.rb +++ b/spec/lib/backup/options_spec.rb @@ -272,4 +272,20 @@ RSpec.describe Backup::Options, feature_category: :backup_restore do end end end + + describe '#skip_task?' do + tasks = %w[db uploads builds artifacts lfs terraform_state registry pages repositories packages ci_secure_files] + + tasks.each do |task_name| + it "returns true when task #{task_name} is skipped" do + options.skippable_tasks[task_name] = true + + expect(options.skip_task?(task_name)).to be(true) + end + + it "returns false when task #{task_name} has default skip behavior" do + expect(options.skip_task?(task_name)).to be(false) + end + end + end end diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/targets/database_spec.rb index 7e023fda830..204ce62e32f 100644 --- a/spec/lib/backup/database_spec.rb +++ b/spec/lib/backup/targets/database_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Backup::Database, :reestablished_active_record_base, feature_category: :backup_restore do +RSpec.describe Backup::Targets::Database, :reestablished_active_record_base, feature_category: :backup_restore do let(:progress) { StringIO.new } let(:progress_output) { progress.string } let(:backup_id) { 'some_id' } @@ -18,7 +18,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate end end - before(:all) do # rubocop:disable RSpec/BeforeAll + before_all do Rake.application.rake_require 'active_record/railties/databases' Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' @@ -29,11 +29,11 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate describe '#dump', :delete do let(:force) { true } - subject { described_class.new(progress, force: force, options: backup_options) } + subject(:databases) { described_class.new(progress, force: force, options: backup_options) } it 'creates gzipped database dumps' do Dir.mktmpdir do |dir| - subject.dump(dir, backup_id) + databases.dump(dir, backup_id) base_models_for_backup.each_key do |database_name| filename = database_name == 'main' ? 'database.sql.gz' : "#{database_name}_database.sql.gz" @@ -59,7 +59,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(backup_connection).to receive(:release_snapshot!).and_call_original end - subject.dump(dir, backup_id) + databases.dump(dir, backup_id) end end end @@ -81,7 +81,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(backup_connection).not_to receive(:release_snapshot!) end - subject.dump(dir, backup_id) + databases.dump(dir, backup_id) end end end @@ -98,7 +98,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate .to receive(:new).exactly(number_of_databases).times.and_return(timeout_service) expect(timeout_service).to receive(:restore_timeouts).exactly(number_of_databases).times - expect { subject.dump(dir, backup_id) }.to raise_error StandardError + expect { databases.dump(dir, backup_id) }.to raise_error StandardError end end end @@ -111,7 +111,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate it 'will override database.yml configuration' do # Expect an error because we can't connect to test.invalid. expect do - Dir.mktmpdir { |dir| subject.dump(dir, backup_id) } + Dir.mktmpdir { |dir| databases.dump(dir, backup_id) } end.to raise_error(Backup::DatabaseBackupError) expect do @@ -129,19 +129,19 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate let(:force) { true } let(:rake_task) { instance_double(Rake::Task, invoke: true) } - subject { described_class.new(progress, force: force, options: backup_options) } + subject(:databases) { described_class.new(progress, force: force, options: backup_options) } before do allow(Rake::Task).to receive(:[]).with(any_args).and_return(rake_task) - allow(subject).to receive(:pg_restore_cmd).and_return(cmd) + allow(databases).to receive(:pg_restore_cmd).and_return(cmd) end context 'when not forced' do let(:force) { false } it 'warns the user and waits' do - expect(subject).to receive(:sleep) + expect(databases).to receive(:sleep) if one_database_configured? expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) @@ -149,13 +149,13 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) end - subject.restore(backup_dir, backup_id) + databases.restore(backup_dir, backup_id) expect(progress_output).to include('Removing all tables. Press `Ctrl-C` within 5 seconds to abort') end it 'has a pre restore warning' do - expect(subject.pre_restore_warning).not_to be_nil + expect(databases.pre_restore_warning).not_to be_nil end end @@ -167,7 +167,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) end - subject.restore(backup_dir, backup_id) + databases.restore(backup_dir, backup_id) expect(progress_output).to include("Restoring PostgreSQL database") expect(progress_output).to include("[DONE]") @@ -181,7 +181,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate it 'outputs a message about DECOMPRESS_CMD' do expect do - subject.restore(backup_dir, backup_id) + databases.restore(backup_dir, backup_id) end.to output(/Using custom DECOMPRESS_CMD 'tee'/).to_stdout end end @@ -189,7 +189,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate context 'with a corrupted .gz file' do before do - allow(subject).to receive(:file_name).and_return("#{backup_dir}big-image.png") + allow(databases).to receive(:file_name).and_return("#{backup_dir}big-image.png") end it 'raises a backup error' do @@ -199,7 +199,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) end - expect { subject.restore(backup_dir, backup_id) }.to raise_error(Backup::Error) + expect { databases.restore(backup_dir, backup_id) }.to raise_error(Backup::Error) end end @@ -215,17 +215,17 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke) end - subject.restore(backup_dir, backup_id) + databases.restore(backup_dir, backup_id) expect(progress_output).to include("ERRORS") expect(progress_output).not_to include(noise) expect(progress_output).to include(visible_error) - expect(subject.post_restore_warning).not_to be_nil + expect(databases.post_restore_warning).not_to be_nil end end context 'with PostgreSQL settings defined in the environment' do - let(:config) { YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))['test'] } + let(:config) { YAML.load_file(Rails.root.join('config/database.yml'))['test'] } before do stub_env(ENV.to_h.merge({ @@ -244,7 +244,7 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate expect(ENV).to receive(:merge!).with(hash_including { 'PGHOST' => 'test.example.com' }) expect(ENV).not_to receive(:[]=).with('PGPASSWORD', anything) - subject.restore(backup_dir, backup_id) + databases.restore(backup_dir, backup_id) expect(ENV['PGPORT']).to eq(config['port']) if config['port'] expect(ENV['PGUSER']).to eq(config['username']) if config['username'] @@ -267,14 +267,14 @@ RSpec.describe Backup::Database, :reestablished_active_record_base, feature_cate end expect do - subject.restore('db', backup_id) + databases.restore('db', backup_id) end.to raise_error(Backup::Error, /Source database file does not exist/) end end context 'for ci database' do it 'ci database tolerates missing source file' do - expect { subject.restore(backup_dir, backup_id) }.not_to raise_error + expect { databases.restore(backup_dir, backup_id) }.not_to raise_error end end end diff --git a/spec/lib/backup/targets/files_spec.rb b/spec/lib/backup/targets/files_spec.rb new file mode 100644 index 00000000000..d4acd13c4cb --- /dev/null +++ b/spec/lib/backup/targets/files_spec.rb @@ -0,0 +1,403 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::Targets::Files, feature_category: :backup_restore do + let(:progress) { StringIO.new } + let!(:project) { create(:project) } + let(:backup_options) { Backup::Options.new } + let(:backup_basepath) { Pathname(Gitlab.config.backup.path) } + + let(:status_0) { instance_double(Process::Status, success?: true, exitstatus: 0) } + let(:status_1) { instance_double(Process::Status, success?: false, exitstatus: 1) } + let(:status_2) { instance_double(Process::Status, success?: false, exitstatus: 2) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + allow(FileUtils).to receive(:mkdir_p).and_return(true) + allow(FileUtils).to receive(:mv).and_return(true) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:realpath).with("/var/gitlab-registry").and_return("/var/gitlab-registry") + allow(File).to receive(:realpath).with("/var/gitlab-registry/..").and_return("/var") + allow(File).to receive(:realpath).with("/var/gitlab-pages").and_return("/var/gitlab-pages") + allow(File).to receive(:realpath).with("/var/gitlab-pages/..").and_return("/var") + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:progress).and_return(progress) + end + end + + RSpec::Matchers.define :eq_statuslist do |expected| + match do |actual| + actual.map(&:exitstatus) == expected.map(&:exitstatus) + end + + description do + 'be an Array of Process::Status with equal exitstatus against expected' + end + + failure_message do |actual| + "expected #{actual} exitstatuses list to be equal #{expected} exitstatuses list" + end + end + + describe '#restore' do + subject(:files) { described_class.new(progress, '/var/gitlab-registry', options: backup_options) } + + let(:timestamp) { Time.utc(2017, 3, 22) } + + around do |example| + travel_to(timestamp) { example.run } + end + + describe 'folders with permission' do + before do + allow(files).to receive(:run_pipeline!).and_return([[true, true], '']) + allow(files).to receive(:backup_existing_files).and_return(true) + allow(files).to receive(:pipeline_succeeded?).and_return(true) + found_files = %w[/var/gitlab-registry/. /var/gitlab-registry/.. /var/gitlab-registry/sample1] + allow(Dir).to receive(:glob).with("/var/gitlab-registry/*", File::FNM_DOTMATCH).and_return(found_files) + end + + it 'moves all necessary files' do + allow(files).to receive(:backup_existing_files).and_call_original + + tmp_dir = backup_basepath.join('tmp', "registry.#{Time.now.to_i}") + expect(FileUtils).to receive(:mv).with(['/var/gitlab-registry/sample1'], tmp_dir) + + files.restore('registry.tar.gz', 'backup_id') + end + + it 'raises no errors' do + expect { files.restore('registry.tar.gz', 'backup_id') }.not_to raise_error + end + + it 'calls tar command with unlink' do + expect(files).to receive(:tar).and_return('blabla-tar') + + expect(files).to receive(:run_pipeline!).with( + ['gzip -cd', %w[blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -]], + any_args) + expect(files).to receive(:pipeline_succeeded?).and_return(true) + + files.restore('registry.tar.gz', 'backup_id') + end + + it 'raises an error on failure' do + expect(files).to receive(:pipeline_succeeded?).and_return(false) + + expect { files.restore('registry.tar.gz', 'backup_id') }.to raise_error(/Restore operation failed:/) + end + end + + describe 'folders without permissions' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) + allow(files).to receive(:run_pipeline!).and_return([[true, true], '']) + allow(files).to receive(:pipeline_succeeded?).and_return(true) + end + + it 'shows error message' do + expect(files).to receive(:access_denied_error).with("/var/gitlab-registry") + + files.restore('registry.tar.gz', 'backup_id') + end + end + + describe 'folders that are a mountpoint' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY) + allow(files).to receive(:run_pipeline!).and_return([[true, true], '']) + allow(files).to receive(:pipeline_succeeded?).and_return(true) + end + + it 'shows error message' do + expect(files).to receive(:resource_busy_error).with("/var/gitlab-registry") + .and_call_original + + expect { files.restore('registry.tar.gz', 'backup_id') }.to raise_error(/is a mountpoint/) + end + end + + describe 'with DECOMPRESS_CMD' do + before do + stub_env('DECOMPRESS_CMD', 'tee') + allow(files).to receive(:pipeline_succeeded?).and_return(true) + end + + it 'passes through tee instead of gzip' do + expect(files).to receive(:run_pipeline!).with(['tee', anything], any_args).and_return([[true, true], '']) + + expect do + files.restore('registry.tar.gz', 'backup_id') + end.to output(/Using custom DECOMPRESS_CMD 'tee'/).to_stdout + end + end + end + + describe '#dump' do + subject(:files) do + described_class.new(progress, '/var/gitlab-pages', excludes: ['@pages.tmp'], options: backup_options) + end + + before do + allow(files).to receive(:run_pipeline!).and_return([[true, true], '']) + allow(files).to receive(:pipeline_succeeded?).and_return(true) + end + + it 'raises no errors' do + expect { files.dump('registry.tar.gz', 'backup_id') }.not_to raise_error + end + + it 'excludes tmp dirs from archive' do + expect(files).to receive(:tar).and_return('blabla-tar') + + expect(files).to receive(:run_pipeline!).with( + [%w[blabla-tar --exclude=lost+found --exclude=./@pages.tmp -C /var/gitlab-pages -cf - .], 'gzip -c -1'], + any_args) + files.dump('registry.tar.gz', 'backup_id') + end + + it 'raises an error on failure' do + allow(files).to receive(:run_pipeline!).and_return([[true, true], '']) + expect(files).to receive(:pipeline_succeeded?).and_return(false) + + expect do + files.dump('registry.tar.gz', 'backup_id') + end.to raise_error(/Failed to create compressed file/) + end + + describe 'with STRATEGY=copy' do + before do + stub_env('STRATEGY', 'copy') + allow(files).to receive(:backup_basepath).and_return(Pathname('/var/gitlab-backup')) + allow(File).to receive(:realpath).with('/var/gitlab-backup').and_return('/var/gitlab-backup') + end + + it 'excludes tmp dirs from rsync' do + cmd_args = %w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp + /var/gitlab-pages /var/gitlab-backup] + expect(Gitlab::Popen).to receive(:popen).with(cmd_args).and_return(['', 0]) + + files.dump('registry.tar.gz', 'backup_id') + end + + it 'retries if rsync fails due to vanishing files' do + cmd_args = %w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp + /var/gitlab-pages /var/gitlab-backup] + expect(Gitlab::Popen).to receive(:popen).with(cmd_args).and_return(['rsync failed', 24], ['', 0]) + + expect do + files.dump('registry.tar.gz', 'backup_id') + end.to output(/files vanished during rsync, retrying/).to_stdout + end + + it 'raises an error and outputs an error message if rsync failed' do + cmd_args = %w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp + /var/gitlab-pages /var/gitlab-backup] + allow(Gitlab::Popen).to receive(:popen).with(cmd_args).and_return(['rsync failed', 1]) + + expect do + files.dump('registry.tar.gz', 'backup_id') + end.to output(/rsync failed/).to_stdout + .and raise_error(/Failed to create compressed file/) + end + end + + describe 'with COMPRESS_CMD' do + before do + stub_env('COMPRESS_CMD', 'tee') + end + + it 'passes through tee instead of gzip' do + expect(files).to receive(:run_pipeline!).with([anything, 'tee'], any_args) + expect do + files.dump('registry.tar.gz', 'backup_id') + end.to output(/Using custom COMPRESS_CMD 'tee'/).to_stdout + end + end + + context 'when GZIP_RSYNCABLE is "yes"' do + before do + stub_env('GZIP_RSYNCABLE', 'yes') + end + + it 'gzips the files with rsyncable option' do + expect(files).to receive(:run_pipeline!).with([anything, 'gzip --rsyncable -c -1'], any_args) + files.dump('registry.tar.gz', 'backup_id') + end + end + + context 'when GZIP_RSYNCABLE is not set' do + it 'gzips the files without the rsyncable option' do + expect(files).to receive(:run_pipeline!).with([anything, 'gzip -c -1'], any_args) + files.dump('registry.tar.gz', 'backup_id') + end + end + end + + describe '#exclude_dirs' do + subject(:files) do + described_class.new(progress, '/var/gitlab-pages', excludes: ['@pages.tmp'], options: backup_options) + end + + it 'prepends a leading dot slash to tar excludes' do + expect(files.exclude_dirs(:tar)).to eq(%w[--exclude=lost+found --exclude=./@pages.tmp]) + end + + it 'prepends a leading slash and app_files_dir basename to rsync excludes' do + expect(files.exclude_dirs(:rsync)).to eq(%w[--exclude=lost+found --exclude=/gitlab-pages/@pages.tmp]) + end + end + + describe '#run_pipeline!' do + subject(:files) do + described_class.new(progress, '/var/gitlab-registry', options: backup_options) + end + + it 'executes an Open3.pipeline for cmd_list' do + expect(Open3).to receive(:pipeline).with(%w[whew command], %w[another cmd], any_args) + + files.run_pipeline!([%w[whew command], %w[another cmd]]) + end + + it 'returns an empty output on success pipeline' do + expect(files.run_pipeline!(%w[true true])[1]).to eq('') + end + + it 'returns the stderr for failed pipeline' do + expect( + files.run_pipeline!(['echo OMG: failed command present 1>&2; false', 'true'])[1] + ).to match(/OMG: failed/) + end + + it 'returns the success status list on success pipeline' do + expect( + files.run_pipeline!(%w[true true])[0] + ).to eq_statuslist([status_0, status_0]) + end + + it 'returns the failed status in status list for failed commands in pipeline' do + expect(files.run_pipeline!(%w[false true true])[0]).to eq_statuslist([status_1, status_0, status_0]) + expect(files.run_pipeline!(%w[true false true])[0]).to eq_statuslist([status_0, status_1, status_0]) + expect(files.run_pipeline!(%w[false false true])[0]).to eq_statuslist([status_1, status_1, status_0]) + expect(files.run_pipeline!(%w[false true false])[0]).to eq_statuslist([status_1, status_0, status_1]) + expect(files.run_pipeline!(%w[false false false])[0]).to eq_statuslist([status_1, status_1, status_1]) + end + end + + describe '#pipeline_succeeded?' do + subject(:files) do + described_class.new(progress, '/var/gitlab-registry', options: backup_options) + end + + it 'returns true if both tar and gzip succeeeded' do + expect( + files.pipeline_succeeded?(tar_status: status_0, compress_status: status_0, output: 'any_output') + ).to be_truthy + end + + it 'returns false if gzip failed' do + expect( + files.pipeline_succeeded?(tar_status: status_1, compress_status: status_1, output: 'any_output') + ).to be_falsey + end + + context 'if gzip succeeded and tar failed non-critically' do + before do + allow(files).to receive(:tar_ignore_non_success?).and_return(true) + end + + it 'returns true' do + expect( + files.pipeline_succeeded?(tar_status: status_1, compress_status: status_0, output: 'any_output') + ).to be_truthy + end + end + + context 'if gzip succeeded and tar failed in other cases' do + before do + allow(files).to receive(:tar_ignore_non_success?).and_return(false) + end + + it 'returns false' do + expect( + files.pipeline_succeeded?(tar_status: status_1, compress_status: status_0, output: 'any_output') + ).to be_falsey + end + end + end + + describe '#tar_ignore_non_success?' do + subject(:files) do + described_class.new(progress, '/var/gitlab-registry', options: backup_options) + end + + context 'if `tar` command exits with 1 exitstatus' do + it 'returns true' do + expect( + files.tar_ignore_non_success?(1, 'any_output') + ).to be_truthy + end + + it 'outputs a warning' do + expect do + files.tar_ignore_non_success?(1, 'any_output') + end.to output(/Ignoring tar exit status 1/).to_stdout + end + end + + context 'if `tar` command exits with 2 exitstatus with non-critical warning' do + before do + allow(files).to receive(:noncritical_warning?).and_return(true) + end + + it 'returns true' do + expect( + files.tar_ignore_non_success?(2, 'any_output') + ).to be_truthy + end + + it 'outputs a warning' do + expect do + files.tar_ignore_non_success?(2, 'any_output') + end.to output(/Ignoring non-success exit status/).to_stdout + end + end + + context 'if `tar` command exits with any other unlisted error' do + before do + allow(files).to receive(:noncritical_warning?).and_return(false) + end + + it 'returns false' do + expect( + files.tar_ignore_non_success?(2, 'any_output') + ).to be_falsey + end + end + end + + describe '#noncritical_warning?' do + subject(:files) do + described_class.new(progress, '/var/gitlab-registry', options: backup_options) + end + + it 'returns true if given text matches noncritical warnings list' do + expect( + files.noncritical_warning?('tar: .: Cannot mkdir: No such file or directory') + ).to be_truthy + + expect( + files.noncritical_warning?('gtar: .: Cannot mkdir: No such file or directory') + ).to be_truthy + end + + it 'returns false otherwize' do + expect( + files.noncritical_warning?('unknown message') + ).to be_falsey + end + end +end diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/targets/repositories_spec.rb index e63d321495e..0f203e114b2 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/targets/repositories_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Backup::Repositories, feature_category: :backup_restore do - let(:progress) { spy(:stdout) } - let(:strategy) { spy(:strategy) } +RSpec.describe Backup::Targets::Repositories, feature_category: :backup_restore do + let(:progress) { instance_double(StringIO, puts: nil, print: nil) } + let(:strategy) { instance_double(Backup::GitalyBackup, start: nil, enqueue: nil, finish!: nil) } let(:storages) { [] } let(:paths) { [] } let(:skip_paths) { [] } @@ -12,7 +12,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do let(:backup_id) { 'backup_id' } let(:backup_options) { Backup::Options.new } - subject do + subject(:repositories) do described_class.new( progress, strategy: strategy, @@ -31,25 +31,26 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do project_snippet = create(:project_snippet, :repository, project: project) personal_snippet = create(:personal_snippet, :repository, author: project.first_owner) - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + 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(:finish!) end end - context 'hashed storage' do + context 'with hashed storage' do let_it_be(:project) { create(:project_with_design, :repository) } it_behaves_like 'creates repository bundles' end - context 'legacy storage' do + context 'with legacy storage' do let_it_be(:project) { create(:project_with_design, :repository, :legacy_storage) } it_behaves_like 'creates repository bundles' @@ -59,19 +60,19 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do it 'enqueue_project raises an error' do allow(strategy).to receive(:enqueue).with(anything, Gitlab::GlRepository::PROJECT).and_raise(IOError) - expect { subject.dump(destination, backup_id) }.to raise_error(IOError) + expect { repositories.dump(destination, backup_id) }.to raise_error(IOError) end it 'project query raises an error' do allow(Project).to receive_message_chain(:includes, :find_each).and_raise(ActiveRecord::StatementTimeout) - expect { subject.dump(destination, backup_id) }.to raise_error(ActiveRecord::StatementTimeout) + expect { repositories.dump(destination, backup_id) }.to raise_error(ActiveRecord::StatementTimeout) end end it 'avoids N+1 database queries' do control = ActiveRecord::QueryRecorder.new do - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) end create_list(:project, 2, :repository) @@ -82,7 +83,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do # for each project. # We are using 2 projects here. expect do - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) end.not_to exceed_query_limit(control).with_threshold(2) end @@ -102,7 +103,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) excluded_personal_snippet.track_snippet_repository('test_second_storage') - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) @@ -110,7 +111,8 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end @@ -118,7 +120,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do describe 'paths' do let_it_be(:project) { create(:project_with_design, :repository) } - context 'project path' do + context 'with a project path' do let(:paths) { [project.full_path] } it 'calls enqueue for all repositories on the specified project', :aggregate_failures do @@ -126,7 +128,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) @@ -134,12 +136,13 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end - context 'group path' do + context 'with a group path' do let(:paths) { [project.namespace.full_path] } it 'calls enqueue for all repositories on all descendant projects', :aggregate_failures do @@ -147,7 +150,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) @@ -155,7 +158,8 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end @@ -165,14 +169,14 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do let_it_be(:project) { create(:project_with_design, :repository) } let_it_be(:excluded_project) { create(:project, :repository) } - context 'project path' do + context 'with a project path' do let(:skip_paths) { [excluded_project.full_path] } it 'calls enqueue for all repositories on the specified project', :aggregate_failures do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) included_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) @@ -180,19 +184,20 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do expect(strategy).to have_received(:enqueue).with(included_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end - context 'group path' do + context 'with a group path' do let(:skip_paths) { [excluded_project.namespace.full_path] } it 'calls enqueue for all repositories on all descendant projects', :aggregate_failures do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) included_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.dump(destination, backup_id) + repositories.dump(destination, backup_id) expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) @@ -200,7 +205,8 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do expect(strategy).to have_received(:enqueue).with(included_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end @@ -214,23 +220,25 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project, author: project.first_owner) } it 'calls enqueue for each repository type', :aggregate_failures do - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], + backup_id: backup_id) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + 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(:finish!) end - context 'restoring object pools' do + context 'when restoring object pools' do it 'schedules restoring of the pool', :sidekiq_might_not_need_inline do pool_repository = create(:pool_repository, :failed) pool_repository.delete_object_pool - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) pool_repository.reload expect(pool_repository).not_to be_failed @@ -241,14 +249,14 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do pool_repository = create(:pool_repository, state: :obsolete) pool_repository.update_column(:source_project_id, nil) - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) pool_repository.reload expect(pool_repository).to be_obsolete end end - context 'storages' do + context 'for storages' do let(:storages) { %w[default] } before do @@ -262,21 +270,23 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) excluded_personal_snippet.track_snippet_repository('test_second_storage') - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], + backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end - context 'paths' do - context 'project path' do + context 'for paths' do + context 'when project path' do let(:paths) { [project.full_path] } it 'calls enqueue for all repositories on the specified project', :aggregate_failures do @@ -284,20 +294,22 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: nil, backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: nil, + backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end - context 'group path' do + context 'with a group path' do let(:paths) { [project.namespace.full_path] } it 'calls enqueue for all repositories on all descendant projects', :aggregate_failures do @@ -305,59 +317,65 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: nil, backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: nil, + backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end end - context 'skip_paths' do + context 'for skip_paths' do let_it_be(:excluded_project) { create(:project, :repository) } - context 'project path' do + context 'with a project path' do let(:skip_paths) { [excluded_project.full_path] } it 'calls enqueue for all repositories on the specified project', :aggregate_failures do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) included_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], + backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) expect(strategy).to have_received(:enqueue).with(included_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end - context 'group path' do + context 'with a group path' do let(:skip_paths) { [excluded_project.namespace.full_path] } it 'calls enqueue for all repositories on all descendant projects', :aggregate_failures do excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) included_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) - subject.restore(destination, backup_id) + repositories.restore(destination, backup_id) - expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], backup_id: backup_id) + expect(strategy).to have_received(:start).with(:restore, destination, remove_all_repositories: %w[default], + backup_id: backup_id) expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) expect(strategy).to have_received(:enqueue).with(included_personal_snippet, Gitlab::GlRepository::SNIPPET) 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.design_management_repository, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:enqueue).with(project.design_management_repository, + Gitlab::GlRepository::DESIGN) expect(strategy).to have_received(:finish!) end end diff --git a/spec/lib/backup/targets/target_spec.rb b/spec/lib/backup/targets/target_spec.rb new file mode 100644 index 00000000000..f69d95fb382 --- /dev/null +++ b/spec/lib/backup/targets/target_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::Targets::Target, feature_category: :backup_restore do + let(:progress) { StringIO.new } + let(:backup_options) { build(:backup_options) } + + subject(:target) { described_class.new(progress, options: backup_options) } + + describe '#options' do + it 'has an accessor for Backup::Options' do + expect(target.options).to be_a(Backup::Options) + end + end + + describe '#dump' do + it 'must be implemented by the subclass' do + expect { target.dump('some/path', 'backup_id') }.to raise_error(NotImplementedError) + end + end + + describe '#restore' do + it 'must be implemented by the subclass' do + expect { target.restore('some/path', 'backup_id') }.to raise_error(NotImplementedError) + end + end + + describe '#pre_restore_warning' do + it { respond_to :pre_restore_warning } + end + + describe '#pos_restore_warning' do + it { respond_to :pos_restore_warning } + end +end diff --git a/spec/lib/backup/task_spec.rb b/spec/lib/backup/task_spec.rb deleted file mode 100644 index 5ded16cd52b..00000000000 --- a/spec/lib/backup/task_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Backup::Task, feature_category: :backup_restore do - let(:progress) { StringIO.new } - let(:backup_options) { build(:backup_options) } - - subject { described_class.new(progress, options: backup_options) } - - describe '#dump' do - it 'must be implemented by the subclass' do - expect { subject.dump('some/path', 'backup_id') }.to raise_error(NotImplementedError) - end - end - - describe '#restore' do - it 'must be implemented by the subclass' do - expect { subject.restore('some/path', 'backup_id') }.to raise_error(NotImplementedError) - end - end -end |